Fabric Parity Worldgen

This commit is contained in:
Brian Neumann-Fopiano
2026-06-12 01:36:34 -04:00
parent 26d9115904
commit 2bac948edb
34 changed files with 3915 additions and 34 deletions
+1
View File
@@ -18,3 +18,4 @@ TreeGenStuff/
dist/
core/plugins/
adapters/bukkit/plugin/plugins/
adapters/fabric/run/
+62 -17
View File
@@ -49,6 +49,7 @@ repositories {
}
maven { url = uri('https://repo.codemc.org/repository/maven-public/') }
maven { url = uri('https://jitpack.io') }
maven { url = uri('https://hub.spigotmc.org/nexus/content/repositories/snapshots/') }
}
configurations {
@@ -56,6 +57,10 @@ configurations {
canBeConsumed = false
canBeResolved = true
}
devBundle {
canBeConsumed = false
canBeResolved = true
}
}
configurations.named('bundle').configure {
@@ -71,28 +76,43 @@ configurations.named('bundle').configure {
exclude(group: 'io.github.slimjar')
}
configurations.compileClasspath.extendsFrom(configurations.devBundle)
configurations.runtimeClasspath.extendsFrom(configurations.devBundle)
dependencies {
minecraft("com.mojang:minecraft:${minecraftVersion}")
implementation("net.fabricmc:fabric-loader:${fabricLoaderVersion}")
implementation('net.fabricmc.fabric-api:fabric-api-base:2.0.3+ece063234c')
implementation('net.fabricmc.fabric-api:fabric-registry-sync-v0:7.1.0+2fa62b4e4c')
implementation('net.fabricmc.fabric-api:fabric-resource-loader-v1:2.0.10+7c44c7324c')
implementation('net.fabricmc.fabric-api:fabric-lifecycle-events-v1:4.1.0+6d50a0854c')
compileOnly('org.slf4j:slf4j-api:2.0.17')
compileOnly(libs.spigot) {
transitive = false
}
bundle("art.arcane:core:${irisVersion}")
bundle("art.arcane:spi:${irisVersion}")
bundle('com.github.VolmitSoftware:VolmLib:master-SNAPSHOT')
bundle(libs.paralithic)
bundle(libs.lru)
bundle(libs.kotlin.stdlib)
bundle(libs.kotlin.coroutines)
bundle(libs.commons.lang)
bundle(libs.commons.math3)
bundle(libs.caffeine)
bundle(libs.lz4)
bundle(libs.zip)
bundle(libs.sentry)
bundle(libs.oshi)
bundle(libs.byteBuddy.core)
bundle(libs.byteBuddy.agent)
List<Object> shared = [
"art.arcane:core:${irisVersion}".toString(),
"art.arcane:spi:${irisVersion}".toString(),
'com.github.VolmitSoftware:VolmLib:master-SNAPSHOT',
libs.paralithic,
libs.lru,
libs.kotlin.stdlib,
libs.kotlin.coroutines,
libs.commons.lang,
libs.commons.math3,
libs.caffeine,
libs.lz4,
libs.zip,
libs.sentry,
libs.oshi,
libs.byteBuddy.core,
libs.byteBuddy.agent
]
shared.each { Object notation ->
add('bundle', notation)
add('devBundle', notation)
}
}
tasks.named('compileJava', JavaCompile).configure {
@@ -100,6 +120,31 @@ tasks.named('compileJava', JavaCompile).configure {
options.release.set(25)
}
loom {
runs {
server {
String parity = providers.gradleProperty('irisParity').getOrNull()
if (parity != null) {
property('iris.parity', parity)
}
String parityGolden = providers.gradleProperty('irisParityGolden').getOrNull()
if (parityGolden != null) {
property('iris.parity.golden', parityGolden)
}
String parityDeep = providers.gradleProperty('irisParityDeep').getOrNull()
if (parityDeep != null) {
property('iris.parity.deep', parityDeep)
}
String worldCheck = providers.gradleProperty('irisWorldCheck').getOrNull()
if (worldCheck != null) {
property('iris.worldcheck', worldCheck)
}
vmArg('-Xmx8G')
programArgs('nogui')
}
}
}
processResources {
inputs.property('version', project.version)
inputs.property('minecraftVersion', minecraftVersion)
@@ -0,0 +1,58 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.spi.PlatformBiome;
import net.minecraft.world.level.biome.Biome;
import java.util.concurrent.ConcurrentHashMap;
public final class FabricBiome implements PlatformBiome {
private static final ConcurrentHashMap<String, FabricBiome> CACHE = new ConcurrentHashMap<>();
private final Biome biome;
private final String key;
private final String namespace;
private FabricBiome(Biome biome, String key) {
this.biome = biome;
this.key = key;
int colon = key.indexOf(':');
this.namespace = colon >= 0 ? key.substring(0, colon) : "minecraft";
}
public static FabricBiome of(Biome biome, String key) {
return CACHE.computeIfAbsent(key, (String k) -> new FabricBiome(biome, k));
}
@Override
public String key() {
return key;
}
@Override
public String namespace() {
return namespace;
}
@Override
public Object nativeHandle() {
return biome;
}
}
@@ -0,0 +1,36 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBiomeWriter;
import java.util.List;
public final class FabricBiomeWriter implements PlatformBiomeWriter {
@Override
public int biomeIdFor(String key) {
return 0;
}
@Override
public List<PlatformBiome> allBiomes() {
return List.of();
}
}
@@ -0,0 +1,90 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.hunk.Hunk;
public final class FabricBlockBuffer implements Hunk<PlatformBlockState> {
private final PlatformBlockState[] data;
private final PlatformBlockState air;
private final int height;
public FabricBlockBuffer(int height, PlatformBlockState air) {
this.data = new PlatformBlockState[16 * height * 16];
this.air = air;
this.height = height;
}
private int index(int x, int y, int z) {
return (y * 16 + z) * 16 + x;
}
public boolean isAir(int x, int y, int z) {
return data[index(x, y, z)] == null;
}
@Override
public int getWidth() {
return 16;
}
@Override
public int getDepth() {
return 16;
}
@Override
public int getHeight() {
return height;
}
@Override
public void set(int x, int y, int z, PlatformBlockState t) {
if (t == null) {
return;
}
if (x < 0 || y < 0 || z < 0 || x >= 16 || y >= height || z >= 16) {
return;
}
data[index(x, y, z)] = t;
}
@Override
public void setRaw(int x, int y, int z, PlatformBlockState t) {
if (t == null) {
return;
}
data[index(x, y, z)] = t;
}
@Override
public PlatformBlockState getRaw(int x, int y, int z) {
PlatformBlockState state = data[index(x, y, z)];
return state == null ? air : state;
}
@Override
public PlatformBlockState get(int x, int y, int z) {
if (x < 0 || y < 0 || z < 0 || x >= 16 || y >= height || z >= 16) {
return air;
}
return getRaw(x, y, z);
}
}
@@ -0,0 +1,497 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.spi.IrisLogging;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.commands.arguments.blocks.BlockStateParser;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.world.level.EmptyBlockGetter;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.LeavesBlock;
import net.minecraft.world.level.block.PointedDripstoneBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.DripstoneThickness;
import net.minecraft.world.level.block.state.properties.Property;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
public final class FabricBlockResolution {
private static final Set<Block> FOLIAGE = blockSet(
"poppy", "dandelion", "cornflower", "sweet_berry_bush", "crimson_roots", "warped_roots",
"nether_sprouts", "allium", "azure_bluet", "blue_orchid", "oxeye_daisy", "lily_of_the_valley",
"wither_rose", "dark_oak_sapling", "acacia_sapling", "jungle_sapling", "birch_sapling",
"spruce_sapling", "oak_sapling", "orange_tulip", "pink_tulip", "red_tulip", "white_tulip",
"fern", "large_fern", "short_grass", "tall_grass");
private static final Set<Block> DECORANT = decorantSet();
private static final Set<Block> LIT = blockSet(
"glowstone", "amethyst_cluster", "small_amethyst_bud", "medium_amethyst_bud", "large_amethyst_bud",
"end_rod", "soul_sand", "torch", "redstone_torch", "soul_torch", "redstone_wall_torch", "wall_torch",
"soul_wall_torch", "lantern", "candle", "jack_o_lantern", "redstone_lamp", "magma_block", "light",
"shroomlight", "sea_lantern", "soul_lantern", "fire", "soul_fire", "sea_pickle", "brewing_stand",
"redstone_ore");
private static final Set<Block> STORAGE = blockSet(
"chest", "smoker", "trapped_chest", "shulker_box", "white_shulker_box", "orange_shulker_box",
"magenta_shulker_box", "light_blue_shulker_box", "yellow_shulker_box", "lime_shulker_box",
"pink_shulker_box", "gray_shulker_box", "light_gray_shulker_box", "cyan_shulker_box",
"purple_shulker_box", "blue_shulker_box", "brown_shulker_box", "green_shulker_box",
"red_shulker_box", "black_shulker_box", "barrel", "dispenser", "dropper", "hopper", "furnace",
"blast_furnace");
private static final Set<Block> STORAGE_CHEST = storageChestSet();
private static final Set<Block> DEEPSLATE = blockSet(
"deepslate", "deepslate_bricks", "deepslate_brick_slab", "deepslate_brick_stairs",
"deepslate_brick_wall", "deepslate_tile_slab", "deepslate_tiles", "deepslate_tile_stairs",
"deepslate_tile_wall", "cracked_deepslate_tiles", "deepslate_coal_ore", "deepslate_iron_ore",
"deepslate_copper_ore", "deepslate_diamond_ore", "deepslate_emerald_ore", "deepslate_gold_ore",
"deepslate_lapis_ore", "deepslate_redstone_ore");
private static final Map<Block, Block> NORMAL_TO_DEEPSLATE = oreMap(false);
private static final Map<Block, Block> DEEPSLATE_TO_NORMAL = oreMap(true);
private static final Set<Block> FOLIAGE_PLANTABLE_STATE = blockSet(
"grass_block", "moss_block", "rooted_dirt", "dirt", "coarse_dirt", "podzol");
private static final Set<Block> FOLIAGE_PLANTABLE_MATERIAL = blockSet(
"grass_block", "moss_block", "dirt", "tall_grass", "tall_seagrass", "large_fern", "sunflower",
"peony", "lilac", "rose_bush", "rooted_dirt", "coarse_dirt", "podzol");
private static final Set<Block> PLACE_ONTO_LEAVES = blockSet(
"acacia_leaves", "birch_leaves", "dark_oak_leaves", "jungle_leaves", "oak_leaves", "spruce_leaves");
private static final BlockState AIR = Blocks.AIR.defaultBlockState();
private static long lastWarnMs;
private FabricBlockResolution() {
}
record Parsed(BlockState state, Map<Property<?>, Comparable<?>> properties) {
}
private static Set<Block> blockSet(String... ids) {
Set<Block> blocks = new HashSet<>();
for (String id : ids) {
Identifier identifier = Identifier.parse("minecraft:" + id);
if (BuiltInRegistries.BLOCK.containsKey(identifier)) {
blocks.add(BuiltInRegistries.BLOCK.getValue(identifier));
}
}
return blocks;
}
private static Set<Block> decorantSet() {
Set<Block> blocks = blockSet(
"short_grass", "tall_grass", "fern", "large_fern", "cornflower", "sunflower", "chorus_flower",
"poppy", "dandelion", "oxeye_daisy", "orange_tulip", "pink_tulip", "red_tulip", "white_tulip",
"lilac", "dead_bush", "sweet_berry_bush", "rose_bush", "wither_rose", "allium", "blue_orchid",
"lily_of_the_valley", "crimson_fungus", "warped_fungus", "red_mushroom", "brown_mushroom",
"crimson_roots", "azure_bluet", "weeping_vines", "weeping_vines_plant", "warped_roots",
"nether_sprouts", "twisting_vines", "twisting_vines_plant", "sugar_cane", "wheat", "potatoes",
"carrots", "beetroots", "nether_wart", "sea_pickle", "seagrass", "tall_seagrass",
"acacia_button", "birch_button", "crimson_button", "dark_oak_button", "jungle_button",
"oak_button", "polished_blackstone_button", "spruce_button", "stone_button", "warped_button",
"torch", "soul_torch", "glow_lichen", "vine", "sculk_vein");
blocks.addAll(FOLIAGE);
return blocks;
}
private static Set<Block> storageChestSet() {
Set<Block> blocks = new HashSet<>(STORAGE);
blocks.remove(Blocks.SMOKER);
blocks.remove(Blocks.FURNACE);
blocks.remove(Blocks.BLAST_FURNACE);
return blocks;
}
private static Map<Block, Block> oreMap(boolean deepslateToNormal) {
String[][] pairs = {
{"coal_ore", "deepslate_coal_ore"},
{"emerald_ore", "deepslate_emerald_ore"},
{"diamond_ore", "deepslate_diamond_ore"},
{"copper_ore", "deepslate_copper_ore"},
{"gold_ore", "deepslate_gold_ore"},
{"iron_ore", "deepslate_iron_ore"},
{"lapis_ore", "deepslate_lapis_ore"},
{"redstone_ore", "deepslate_redstone_ore"}
};
Map<Block, Block> map = new HashMap<>();
for (String[] pair : pairs) {
Block normal = BuiltInRegistries.BLOCK.getValue(Identifier.parse("minecraft:" + pair[0]));
Block deep = BuiltInRegistries.BLOCK.getValue(Identifier.parse("minecraft:" + pair[1]));
if (deepslateToNormal) {
map.put(deep, normal);
} else {
map.put(normal, deep);
}
}
return map;
}
private static boolean shouldWarn() {
long now = System.currentTimeMillis();
if (now - lastWarnMs >= 1000) {
lastWarnMs = now;
return true;
}
return false;
}
public static BlockState airState() {
return AIR;
}
public static FabricBlockState getAir() {
return FabricBlockState.of(AIR, null);
}
public static FabricBlockState get(String bdxf) {
Parsed parsed = resolveGet(bdxf);
return FabricBlockState.of(parsed.state(), parsed.properties());
}
public static FabricBlockState getNoCompat(String bdxf) {
Parsed parsed = resolveNoCompat(bdxf);
return FabricBlockState.of(parsed.state(), parsed.properties());
}
public static FabricBlockState getOrNull(String bdxf) {
return getOrNull(bdxf, false);
}
public static FabricBlockState getOrNull(String bdxf, boolean warn) {
Parsed parsed = resolveOrNull(bdxf, warn);
return parsed == null ? null : FabricBlockState.of(parsed.state(), parsed.properties());
}
static Parsed resolveGet(String bdxf) {
Parsed parsed = resolveOrNull(bdxf, false);
if (parsed != null) {
return parsed;
}
IrisLogging.error("Can't find block data for " + bdxf);
return resolveNoCompat("STONE");
}
static Parsed resolveNoCompat(String bdxf) {
Parsed parsed = resolveOrNull(bdxf, true);
if (parsed != null) {
return parsed;
}
return new Parsed(AIR, null);
}
static Parsed resolveOrNull(String bdxf, boolean warn) {
try {
String bd = bdxf.trim();
if (bd.startsWith("minecraft:cauldron[level=")) {
bd = bd.replaceAll("\\Q:cauldron[\\E", ":water_cauldron[");
}
if (bd.equals("minecraft:grass_path")) {
return new Parsed(Blocks.DIRT_PATH.defaultBlockState(), null);
}
Parsed bdx = parseBlockData(bd, warn);
if (bdx == null) {
if (warn && shouldWarn()) {
IrisLogging.warn("Unknown Block Data '" + bd + "'");
}
return new Parsed(AIR, null);
}
return bdx;
} catch (Throwable e) {
e.printStackTrace();
if (warn && shouldWarn()) {
IrisLogging.warn("Unknown Block Data '" + bdxf + "'");
}
}
return null;
}
public static FabricBlockState strictParse(String key) {
Parsed parsed = parseStrict(key);
return FabricBlockState.of(parsed.state(), parsed.properties());
}
private static Parsed parseStrict(String key) {
StringReader reader = new StringReader(key);
BlockStateParser.BlockResult result;
try {
result = BlockStateParser.parseForBlock(BuiltInRegistries.BLOCK, reader, false);
} catch (CommandSyntaxException e) {
throw new IllegalArgumentException("Could not parse data: " + key, e);
}
if (reader.canRead()) {
throw new IllegalArgumentException("Could not parse remainder: " + reader.getRemaining());
}
return new Parsed(result.blockState(), result.properties());
}
private static Parsed createBlockData(String s, boolean warn) {
try {
return parseStrict(s);
} catch (IllegalArgumentException e) {
if (s.contains("[")) {
return createBlockData(s.split("\\Q[\\E")[0], warn);
}
}
if (warn) {
IrisLogging.warn("Can't find block data for " + s);
}
return null;
}
private static Parsed materialBlockData(String ix) {
if (ix.contains("[") || ix.contains(":")) {
return null;
}
Identifier identifier = Identifier.tryParse("minecraft:" + ix.toLowerCase(Locale.ROOT));
if (identifier == null || !BuiltInRegistries.BLOCK.containsKey(identifier)) {
return null;
}
return new Parsed(BuiltInRegistries.BLOCK.getValue(identifier).defaultBlockState(), null);
}
private static Parsed parseBlockData(String ix, boolean warn) {
try {
Parsed bx = createBlockData(ix.toLowerCase(), warn);
if (bx == null) {
bx = createBlockData("minecraft:" + ix.toLowerCase(), warn);
}
if (bx == null) {
bx = materialBlockData(ix);
}
if (bx == null) {
if (warn && shouldWarn()) {
IrisLogging.warn("Unknown Block Data: " + ix);
}
return null;
}
if (bx.state().getBlock() instanceof LeavesBlock) {
BlockState mutated = bx.state().setValue(LeavesBlock.PERSISTENT, shouldPreventLeafDecay());
bx = new Parsed(mutated, bx.properties());
}
return bx;
} catch (Throwable e) {
String block = ix.contains(":") ? ix.split(":")[1].toLowerCase() : ix.toLowerCase();
String state = block.contains("[") ? block.split("\\Q[\\E")[1].split("\\Q]\\E")[0] : "";
Map<String, String> stateMap = new HashMap<>();
if (!state.equals("")) {
Arrays.stream(state.split(",")).forEach((String s) -> stateMap.put(s.split("=")[0], s.split("=")[1]));
}
block = block.split("\\Q[\\E")[0];
switch (block) {
case "cauldron" -> block = "water_cauldron";
case "grass_path" -> block = "dirt_path";
case "concrete" -> block = "white_concrete";
case "wool" -> block = "white_wool";
case "beetroots" -> {
if (stateMap.containsKey("age")) {
String updated = stateMap.get("age");
switch (updated) {
case "7" -> updated = "3";
case "3", "4", "5" -> updated = "2";
case "1", "2" -> updated = "1";
}
stateMap.put("age", updated);
}
}
}
Map<String, String> newStates = new HashMap<>();
for (String key : stateMap.keySet()) {
createBlockData(block + "[" + key + "=" + stateMap.get(key) + "]", false);
newStates.put(key, stateMap.get(key));
}
String joined = newStates.entrySet().stream()
.map((Map.Entry<String, String> entry) -> entry.getKey() + "=" + entry.getValue())
.collect(Collectors.joining(","));
if (!joined.equals("")) {
joined = "[" + joined + "]";
}
String newBlock = block + joined;
IrisLogging.debug("Converting " + ix + " to " + newBlock);
try {
return createBlockData(newBlock, false);
} catch (Throwable e1) {
IrisLogging.reportError(e1);
}
return null;
}
}
private static boolean shouldPreventLeafDecay() {
return IrisSettings.get().getGenerator().isPreventLeafDecay();
}
public static BlockState toDeepSlateOre(BlockState block, BlockState ore) {
Block key = ore.getBlock();
if (isDeepSlate(block)) {
Block mapped = NORMAL_TO_DEEPSLATE.get(key);
if (mapped != null) {
return mapped.defaultBlockState();
}
} else {
Block mapped = DEEPSLATE_TO_NORMAL.get(key);
if (mapped != null) {
return mapped.defaultBlockState();
}
}
return ore;
}
public static boolean isDeepSlate(BlockState state) {
return DEEPSLATE.contains(state.getBlock());
}
public static boolean isOre(BlockState state) {
return BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath().endsWith("_ore");
}
public static boolean isAir(BlockState state) {
if (state == null) {
return true;
}
Block block = state.getBlock();
return block == Blocks.AIR || block == Blocks.CAVE_AIR || block == Blocks.VOID_AIR;
}
public static boolean isSolid(BlockState state) {
if (state == null) {
return false;
}
return isSolidMaterial(state.getBlock());
}
private static boolean isSolidMaterial(Block block) {
return block.defaultBlockState().blocksMotion();
}
public static boolean isOccluding(BlockState state) {
return state.getBlock().defaultBlockState().isRedstoneConductor(EmptyBlockGetter.INSTANCE, BlockPos.ZERO);
}
public static boolean isFluid(BlockState state) {
Block block = state.getBlock();
return block == Blocks.WATER || block == Blocks.LAVA;
}
public static boolean isWater(BlockState state) {
return state.getBlock() == Blocks.WATER;
}
public static boolean isWaterLogged(BlockState state) {
return state.hasProperty(BlockStateProperties.WATERLOGGED) && state.getValue(BlockStateProperties.WATERLOGGED);
}
public static boolean isLit(BlockState state) {
return LIT.contains(state.getBlock());
}
public static boolean isUpdatable(BlockState state) {
return isStorage(state)
|| (state.getBlock() instanceof PointedDripstoneBlock
&& state.getValue(PointedDripstoneBlock.THICKNESS) == DripstoneThickness.TIP);
}
public static boolean isFoliage(BlockState state) {
return FOLIAGE.contains(state.getBlock());
}
public static boolean isFoliagePlantable(BlockState state) {
return FOLIAGE_PLANTABLE_STATE.contains(state.getBlock());
}
public static boolean isDecorant(BlockState state) {
return DECORANT.contains(state.getBlock());
}
public static boolean isStorage(BlockState state) {
return STORAGE.contains(state.getBlock());
}
public static boolean isStorageChest(BlockState state) {
return STORAGE_CHEST.contains(state.getBlock());
}
public static boolean isVineBlock(BlockState state) {
Block block = state.getBlock();
return block == Blocks.VINE || block == Blocks.SCULK_VEIN || block == Blocks.GLOW_LICHEN;
}
public static boolean hasTileEntity(BlockState state) {
return state.getBlock().defaultBlockState().hasBlockEntity();
}
public static boolean canPlaceOnto(Block mat, Block onto) {
if ((onto == Blocks.CRIMSON_NYLIUM || onto == Blocks.WARPED_NYLIUM)
&& (mat == Blocks.CRIMSON_FUNGUS || mat == Blocks.CRIMSON_ROOTS
|| mat == Blocks.WARPED_FUNGUS || mat == Blocks.WARPED_ROOTS)) {
return true;
}
if (FOLIAGE.contains(mat)) {
if (!FOLIAGE_PLANTABLE_MATERIAL.contains(onto)) {
return false;
}
}
if (onto == Blocks.AIR || onto == Blocks.CAVE_AIR || onto == Blocks.VOID_AIR) {
return false;
}
if (onto == Blocks.GRASS_BLOCK && mat == Blocks.DEAD_BUSH) {
return false;
}
if (onto == Blocks.DIRT_PATH) {
if (!isSolidMaterial(mat)) {
return false;
}
}
if (PLACE_ONTO_LEAVES.contains(onto)) {
return isSolidMaterial(mat);
}
return true;
}
}
@@ -0,0 +1,308 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.spi.PlatformBlockState;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.Property;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
public final class FabricBlockState implements PlatformBlockState {
private static final ConcurrentHashMap<String, FabricBlockState> CACHE = new ConcurrentHashMap<>();
private final BlockState state;
private final Map<Property<?>, Comparable<?>> parsedProperties;
private final String key;
private final String namespace;
private volatile Boolean air;
private volatile Boolean solid;
private volatile Boolean occluding;
private volatile Boolean fluid;
private volatile Boolean water;
private volatile Boolean foliage;
private volatile Boolean decorant;
private FabricBlockState(BlockState state, Map<Property<?>, Comparable<?>> parsedProperties, String key) {
this.state = state;
this.parsedProperties = parsedProperties;
this.key = key;
this.namespace = parseNamespace(key);
}
public static FabricBlockState of(BlockState state, Map<Property<?>, Comparable<?>> parsedProperties) {
String key = serialize(state);
return CACHE.computeIfAbsent(key, (String k) -> new FabricBlockState(state, parsedProperties, k));
}
public static String serialize(BlockState state) {
StringBuilder result = new StringBuilder(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString());
if (!state.isSingletonState()) {
result.append('[');
result.append(state.getValues()
.map((Property.Value<?> value) -> value.property().getName() + "=" + value.valueName())
.collect(Collectors.joining(",")));
result.append(']');
}
return result.toString();
}
public BlockState handle() {
return state;
}
Map<Property<?>, Comparable<?>> parsedProperties() {
return parsedProperties;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof FabricBlockState blockState)) {
return false;
}
return state.equals(blockState.state);
}
@Override
public int hashCode() {
return key.hashCode();
}
private static String parseNamespace(String key) {
String base = key;
int bracket = base.indexOf('[');
if (bracket >= 0) {
base = base.substring(0, bracket);
}
int colon = base.indexOf(':');
return colon >= 0 ? base.substring(0, colon) : "minecraft";
}
private static String mergeProperty(String key, String name, String value) {
int bracket = key.indexOf('[');
if (bracket < 0) {
return key + "[" + name + "=" + value + "]";
}
String base = key.substring(0, bracket);
String body = key.substring(bracket + 1, key.lastIndexOf(']'));
LinkedHashMap<String, String> properties = new LinkedHashMap<>();
for (String entry : body.split(",")) {
int equals = entry.indexOf('=');
if (equals < 0) {
continue;
}
properties.put(entry.substring(0, equals).trim(), entry.substring(equals + 1).trim());
}
properties.put(name, value);
StringBuilder merged = new StringBuilder(base).append('[');
boolean first = true;
for (Map.Entry<String, String> property : properties.entrySet()) {
if (!first) {
merged.append(',');
}
merged.append(property.getKey()).append('=').append(property.getValue());
first = false;
}
return merged.append(']').toString();
}
@Override
public String key() {
return key;
}
@Override
public String namespace() {
return namespace;
}
@Override
public boolean isAir() {
Boolean cached = air;
if (cached == null) {
cached = FabricBlockResolution.isAir(state);
air = cached;
}
return cached;
}
@Override
public boolean isSolid() {
Boolean cached = solid;
if (cached == null) {
cached = FabricBlockResolution.isSolid(state);
solid = cached;
}
return cached;
}
@Override
public boolean isOccluding() {
Boolean cached = occluding;
if (cached == null) {
cached = FabricBlockResolution.isOccluding(state);
occluding = cached;
}
return cached;
}
@Override
public boolean isCustom() {
return false;
}
@Override
public boolean isFluid() {
Boolean cached = fluid;
if (cached == null) {
cached = FabricBlockResolution.isFluid(state);
fluid = cached;
}
return cached;
}
@Override
public boolean isWater() {
Boolean cached = water;
if (cached == null) {
cached = FabricBlockResolution.isWater(state);
water = cached;
}
return cached;
}
@Override
public boolean isWaterLogged() {
return FabricBlockResolution.isWaterLogged(state);
}
@Override
public boolean isLit() {
return FabricBlockResolution.isLit(state);
}
@Override
public boolean isUpdatable() {
return FabricBlockResolution.isUpdatable(state);
}
@Override
public boolean isFoliage() {
Boolean cached = foliage;
if (cached == null) {
cached = FabricBlockResolution.isFoliage(state);
foliage = cached;
}
return cached;
}
@Override
public boolean isFoliagePlantable() {
return FabricBlockResolution.isFoliagePlantable(state);
}
@Override
public boolean isDecorant() {
Boolean cached = decorant;
if (cached == null) {
cached = FabricBlockResolution.isDecorant(state);
decorant = cached;
}
return cached;
}
@Override
public boolean isStorage() {
return FabricBlockResolution.isStorage(state);
}
@Override
public boolean isStorageChest() {
return FabricBlockResolution.isStorageChest(state);
}
@Override
public boolean isOre() {
return FabricBlockResolution.isOre(state);
}
@Override
public boolean isDeepSlate() {
return FabricBlockResolution.isDeepSlate(state);
}
@Override
public boolean isVineBlock() {
return FabricBlockResolution.isVineBlock(state);
}
@Override
public boolean canPlaceOnto(PlatformBlockState onto) {
return FabricBlockResolution.canPlaceOnto(state.getBlock(), ((BlockState) onto.nativeHandle()).getBlock());
}
@Override
public boolean matches(PlatformBlockState other) {
if (!(other instanceof FabricBlockState blockState)) {
return false;
}
if (state.getBlock() != blockState.state.getBlock()) {
return false;
}
if (state.equals(blockState.state)) {
return true;
}
if (blockState.parsedProperties == null) {
return false;
}
BlockState merged = state;
for (Map.Entry<Property<?>, Comparable<?>> entry : blockState.parsedProperties.entrySet()) {
merged = applyProperty(merged, entry.getKey(), entry.getValue());
}
return merged.equals(state);
}
@SuppressWarnings("unchecked")
private static <T extends Comparable<T>> BlockState applyProperty(BlockState state, Property<?> property, Comparable<?> value) {
return state.setValue((Property<T>) property, (T) value);
}
@Override
public boolean hasTileEntity() {
return FabricBlockResolution.hasTileEntity(state);
}
@Override
public PlatformBlockState withProperty(String name, String value) {
String merged = mergeProperty(key, name, value);
return FabricBlockResolution.strictParse(merged);
}
@Override
public Object nativeHandle() {
return state;
}
}
@@ -0,0 +1,117 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.engine.decorator.DecoratorPlatformHooks;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.hunk.Hunk;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.EmptyBlockGetter;
import net.minecraft.world.level.block.SupportType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BooleanProperty;
import net.minecraft.world.level.block.state.properties.Property;
import java.util.LinkedHashMap;
import java.util.Map;
public final class FabricDecoratorHooks implements DecoratorPlatformHooks.FaceFixer, DecoratorPlatformHooks.SurfaceSturdiness {
private static final Direction[] CARTESIAN = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST, Direction.UP, Direction.DOWN};
private static final String[] FACE_NAMES = {"north", "east", "south", "west", "up", "down"};
@Override
public PlatformBlockState fixFaces(PlatformBlockState state, Hunk<PlatformBlockState> hunk, int rX, int rZ, int x, int y, int z, EngineMantle mantle) {
FabricBlockState fabric = (FabricBlockState) state;
BlockState cloned = fabric.handle();
Map<String, BooleanProperty> allowed = faceProperties(cloned);
for (Map.Entry<String, BooleanProperty> entry : allowed.entrySet()) {
if (cloned.getValue(entry.getValue())) {
cloned = cloned.setValue(entry.getValue(), Boolean.FALSE);
}
}
boolean found = false;
for (Direction f : CARTESIAN) {
int yy = y + f.getStepY();
PlatformBlockState rs = null;
if (mantle != null) {
rs = mantle.getMantle().get(x + f.getStepX(), yy, z + f.getStepZ(), PlatformBlockState.class);
}
BlockState r = rs == null ? (BlockState) EngineMantle.AIR.nativeHandle() : (BlockState) rs.nativeHandle();
if (isFaceSturdy(r, f.getOpposite())) {
BooleanProperty property = allowed.get(f.getSerializedName());
if (property != null) {
found = true;
cloned = cloned.setValue(property, Boolean.TRUE);
}
continue;
}
int xx = rX + f.getStepX();
int zz = rZ + f.getStepZ();
if (xx < 0 || xx > 15 || zz < 0 || zz > 15 || yy < 0 || yy > hunk.getHeight()) {
continue;
}
r = (BlockState) hunk.get(xx, yy, zz).nativeHandle();
if (isFaceSturdy(r, f.getOpposite())) {
BooleanProperty property = allowed.get(f.getSerializedName());
if (property != null) {
found = true;
cloned = cloned.setValue(property, Boolean.TRUE);
}
}
}
if (!found) {
String fallback = allowed.containsKey("down") ? "down" : "up";
BooleanProperty property = allowed.get(fallback);
if (property != null) {
cloned = cloned.setValue(property, Boolean.TRUE);
}
}
return FabricBlockState.of(cloned, fabric.parsedProperties());
}
@Override
public boolean canGoOn(PlatformBlockState surface) {
return isFaceSturdy((BlockState) surface.nativeHandle(), Direction.UP);
}
private static boolean isFaceSturdy(BlockState state, Direction face) {
return state.isFaceSturdy(EmptyBlockGetter.INSTANCE, BlockPos.ZERO, face, SupportType.FULL);
}
private static Map<String, BooleanProperty> faceProperties(BlockState state) {
Map<String, BooleanProperty> properties = new LinkedHashMap<>();
for (String name : FACE_NAMES) {
for (Property<?> property : state.getProperties()) {
if (property.getName().equals(name) && property instanceof BooleanProperty bool) {
properties.put(name, bool);
}
}
}
return properties;
}
}
@@ -0,0 +1,142 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.engine.decorator.DecoratorPlatformHooks;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineWorldManager;
import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
import art.arcane.iris.engine.framework.MeteredCache;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.engine.object.BlockDataMergeSupport;
import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.server.MinecraftServer;
import org.bukkit.Chunk;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import java.util.function.Supplier;
public final class FabricEngineBootstrap {
private static final Object LOCK = new Object();
private static volatile FabricPlatform platform;
private FabricEngineBootstrap() {
}
public static MinecraftServer currentServer() {
Object instance = FabricLoader.getInstance().getGameInstance();
return instance instanceof MinecraftServer server ? server : null;
}
public static FabricPlatform bind() {
return bind(FabricEngineBootstrap::currentServer);
}
public static FabricPlatform bind(Supplier<MinecraftServer> server) {
FabricPlatform bound = platform;
if (bound != null) {
return bound;
}
synchronized (LOCK) {
if (platform != null) {
return platform;
}
FabricPlatform created = new FabricPlatform(server);
IrisPlatforms.bind(created);
IrisObjectRotation.bindFallbackRotator(new FabricStateRotator());
BlockDataMergeSupport.bindFallbackMerger(new FabricStateMerger());
TileData.bindFallbackReader(new FabricTileReader(server));
FabricDecoratorHooks decoratorHooks = new FabricDecoratorHooks();
DecoratorPlatformHooks.bind(decoratorHooks, decoratorHooks);
IrisServices.register(PreservationRegistry.class, new InertPreservation());
IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> new InertWorldManager());
platform = created;
return created;
}
}
private static final class InertPreservation implements PreservationRegistry {
@Override
public void register(Thread thread) {
}
@Override
public void registerCache(MeteredCache cache) {
}
@Override
public void dereference() {
}
}
private static final class InertWorldManager implements EngineWorldManager {
@Override
public void close() {
}
@Override
public int getEntityCount() {
return 0;
}
@Override
public int getChunkCount() {
return 0;
}
@Override
public double getEntitySaturation() {
return 0;
}
@Override
public void onTick() {
}
@Override
public void onSave() {
}
@Override
public void onBlockBreak(BlockBreakEvent e) {
}
@Override
public void onBlockPlace(BlockPlaceEvent e) {
}
@Override
public void onChunkLoad(Chunk e, boolean generated) {
}
@Override
public void onChunkUnload(Chunk e) {
}
@Override
public void teleportAsync(PlayerTeleportEvent e) {
}
}
}
@@ -0,0 +1,58 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.spi.PlatformEntityType;
import net.minecraft.world.entity.EntityType;
import java.util.concurrent.ConcurrentHashMap;
public final class FabricEntityType implements PlatformEntityType {
private static final ConcurrentHashMap<String, FabricEntityType> CACHE = new ConcurrentHashMap<>();
private final EntityType<?> type;
private final String key;
private final String namespace;
private FabricEntityType(EntityType<?> type, String key) {
this.type = type;
this.key = key;
int colon = key.indexOf(':');
this.namespace = colon >= 0 ? key.substring(0, colon) : "minecraft";
}
public static FabricEntityType of(EntityType<?> type, String key) {
return CACHE.computeIfAbsent(key, (String k) -> new FabricEntityType(type, k));
}
@Override
public String key() {
return key;
}
@Override
public String namespace() {
return namespace;
}
@Override
public Object nativeHandle() {
return type;
}
}
@@ -0,0 +1,58 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.spi.PlatformItem;
import net.minecraft.world.item.Item;
import java.util.concurrent.ConcurrentHashMap;
public final class FabricItem implements PlatformItem {
private static final ConcurrentHashMap<String, FabricItem> CACHE = new ConcurrentHashMap<>();
private final Item item;
private final String key;
private final String namespace;
private FabricItem(Item item, String key) {
this.item = item;
this.key = key;
int colon = key.indexOf(':');
this.namespace = colon >= 0 ? key.substring(0, colon) : "minecraft";
}
public static FabricItem of(Item item, String key) {
return CACHE.computeIfAbsent(key, (String k) -> new FabricItem(item, k));
}
@Override
public String key() {
return key;
}
@Override
public String namespace() {
return namespace;
}
@Override
public Object nativeHandle() {
return item;
}
}
@@ -0,0 +1,393 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.hunk.Hunk;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.server.MinecraftServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public final class FabricParityProbe {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final String DIMENSION_KEY = "overworld";
private static final long SEED = 1337L;
private static final int BIOME_STEP = 4;
private static final String DEFAULT_EXPECTED = "922bbcbe766d";
private static final List<Throwable> REPORTED = Collections.synchronizedList(new ArrayList<>());
private FabricParityProbe() {
}
public static void schedule(String config) {
Thread thread = new Thread(() -> waitAndRun(config), "Iris Parity Probe");
thread.setDaemon(true);
thread.start();
}
private static void waitAndRun(String config) {
long start = System.currentTimeMillis();
MinecraftServer server = null;
while (System.currentTimeMillis() - start < 600000L) {
Object instance = FabricLoader.getInstance().getGameInstance();
if (instance instanceof MinecraftServer candidate && candidate.isReady()) {
server = candidate;
break;
}
try {
Thread.sleep(250L);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
if (server == null) {
LOGGER.error("[parity] server did not become ready within 10 minutes");
return;
}
boolean match = false;
try {
match = run(server, config);
} catch (Throwable e) {
LOGGER.error("[parity] probe failed", e);
}
LOGGER.info("[parity] shutting down dev server (result={})", match ? "MATCH" : "MISMATCH");
server.halt(false);
}
private static boolean run(MinecraftServer server, String config) throws Exception {
String packPath = config;
int radius = 8;
int lastColon = config.lastIndexOf(':');
if (lastColon > 0) {
String tail = config.substring(lastColon + 1);
try {
radius = Integer.parseInt(tail);
packPath = config.substring(0, lastColon);
} catch (NumberFormatException ignored) {
}
}
File packSource = new File(packPath);
if (!packSource.isDirectory()) {
LOGGER.error("[parity] pack folder not found: {}", packSource.getAbsolutePath());
return false;
}
FabricEngineBootstrap.bind();
FabricPlatform.errorSink(REPORTED::add);
File workRoot = Files.createTempDirectory("iris-parity").toFile();
File pack = clonePack(packSource, workRoot);
LOGGER.info("[parity] pack: {}", packSource.getAbsolutePath());
LOGGER.info("[parity] work copy: {}", pack.getAbsolutePath());
LOGGER.info("[parity] radius: {} ({} chunks)", radius, (2 * radius + 1) * (2 * radius + 1));
IrisData data = IrisData.get(pack);
IrisDimension dimension = data.getDimensionLoader().load(DIMENSION_KEY);
if (dimension == null) {
LOGGER.error("[parity] dimension '{}' did not load from {}", DIMENSION_KEY, pack.getAbsolutePath());
return false;
}
IrisWorld world = IrisWorld.builder()
.name("parity")
.seed(SEED)
.worldFolder(new File(workRoot, "world"))
.minHeight(dimension.getMinHeight())
.maxHeight(dimension.getMaxHeight())
.build();
EngineTarget target = new EngineTarget(world, dimension, data);
Engine engine = new IrisEngine(target, false);
settle();
int minY = dimension.getMinHeight();
int maxY = dimension.getMaxHeight();
int height = maxY - minY;
LOGGER.info("[parity] engine up: dim={} seed={} minY={} maxY={}", engine.getDimension().getLoadKey(), engine.getSeedManager().getSeed(), minY, maxY);
Map<String, String> goldenChunks = new HashMap<>();
String goldenCombined = null;
int goldenRadius = -1;
String goldenPath = System.getProperty("iris.parity.golden");
if (goldenPath != null) {
List<String> goldenLines = Files.readAllLines(Path.of(goldenPath), StandardCharsets.UTF_8);
for (String line : goldenLines) {
if (line.startsWith("#")) {
int eq = line.indexOf('=');
if (eq > 0) {
String metaKey = line.substring(1, eq);
String metaValue = line.substring(eq + 1);
if (metaKey.equals("combined")) {
goldenCombined = metaValue;
}
if (metaKey.equals("radius")) {
goldenRadius = Integer.parseInt(metaValue.trim());
}
}
} else if (!line.isBlank()) {
int second = line.indexOf(' ', line.indexOf(' ') + 1);
goldenChunks.put(line.substring(0, second), line);
}
}
LOGGER.info("[parity] golden: {} ({} chunks, combined={})", goldenPath, goldenChunks.size(), goldenCombined);
}
PlatformBlockState airState = IrisPlatforms.get().registries().air();
List<int[]> targets = orderedTargets(0, 0, radius);
Map<Long, String> lines = new TreeMap<>();
List<String> mismatches = new ArrayList<>();
int failed = 0;
for (int[] at : targets) {
int cx = at[0];
int cz = at[1];
drainReported();
FabricBlockBuffer blocks = new FabricBlockBuffer(height, airState);
Hunk<PlatformBiome> biomes = Hunk.newArrayHunk(16, height, 16);
List<Throwable> failures = new ArrayList<>();
try {
engine.generate(cx << 4, cz << 4, blocks, biomes, false);
} catch (Throwable e) {
failures.add(e);
}
failures.addAll(drainReported());
if (!failures.isEmpty()) {
failed++;
LOGGER.error("[parity] chunk {},{} FAILED ({} error(s))", cx, cz, failures.size());
for (Throwable failure : failures) {
LOGGER.error("[parity] chunk {},{} error", cx, cz, failure);
}
continue;
}
String line = hashChunk(cx, cz, blocks, biomes, height);
lines.put(chunkKey(cx, cz), line);
String key = cx + " " + cz;
String golden = goldenChunks.get(key);
if (golden != null && !golden.equals(line)) {
mismatches.add(key);
LOGGER.warn("[parity] chunk {} MISMATCH", key);
LOGGER.warn("[parity] golden: {}", golden);
LOGGER.warn("[parity] actual: {}", line);
if (mismatches.size() == 1) {
diffDeep(cx, cz, blocks, height, minY);
}
}
}
List<String> body = new ArrayList<>(lines.values());
String combined = combinedHash(body);
String expected = goldenCombined != null && (goldenRadius == radius || goldenRadius < 0)
? goldenCombined.substring(0, 12)
: DEFAULT_EXPECTED;
boolean combinedMatch = combined.startsWith(expected);
boolean chunkMatch = mismatches.isEmpty() && failed == 0;
boolean match = goldenChunks.isEmpty() ? combinedMatch : (chunkMatch && (radius != 8 || combinedMatch));
if (!goldenChunks.isEmpty()) {
LOGGER.info("[parity] per-chunk: {}/{} matched golden ({} failed)", body.size() - mismatches.size(), body.size(), failed);
}
LOGGER.info("[parity] combined={} expected={} {} ({}/{})",
combined.substring(0, 12), expected, match ? "MATCH" : "MISMATCH", body.size() - mismatches.size(), targets.size());
return match;
}
private static void diffDeep(int cx, int cz, FabricBlockBuffer blocks, int height, int minY) {
String deepDir = System.getProperty("iris.parity.deep");
if (deepDir == null) {
return;
}
try {
Path goldenDump = Path.of(deepDir, cx + "_" + cz + ".txt");
if (!Files.exists(goldenDump)) {
LOGGER.warn("[parity] no deep dump for chunk {},{} at {}", cx, cz, goldenDump);
return;
}
List<String> golden = Files.readAllLines(goldenDump, StandardCharsets.UTF_8);
List<String> actual = new ArrayList<>();
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < height; y++) {
String state = blocks.get(x, y, z).key();
if (!state.equals("minecraft:air") && !state.equals("minecraft:cave_air") && !state.equals("minecraft:void_air")) {
actual.add(x + " " + (y + minY) + " " + z + " " + state);
}
}
}
}
int shown = 0;
int max = Math.max(golden.size(), actual.size());
for (int i = 0; i < max && shown < 20; i++) {
String g = i < golden.size() ? golden.get(i) : "<missing>";
String a = i < actual.size() ? actual.get(i) : "<missing>";
if (!g.equals(a)) {
LOGGER.warn("[parity] deep diff line {}: golden='{}' actual='{}'", i, g, a);
shown++;
}
}
File out = new File(IrisPlatforms.get().dataFolder("parity"), "deep-" + cx + "_" + cz + ".txt");
Files.write(out.toPath(), actual, StandardCharsets.UTF_8);
LOGGER.warn("[parity] full actual dump: {}", out.getAbsolutePath());
} catch (Throwable e) {
LOGGER.warn("[parity] deep diff failed", e);
}
}
private static List<int[]> orderedTargets(int centerX, int centerZ, int radius) {
List<int[]> targets = new ArrayList<>();
for (int dx = -radius; dx <= radius; dx++) {
for (int dz = -radius; dz <= radius; dz++) {
targets.add(new int[]{centerX + dx, centerZ + dz});
}
}
targets.sort(Comparator.comparingInt((int[] t) -> {
int ox = t[0] - centerX;
int oz = t[1] - centerZ;
return ox * ox + oz * oz;
}));
return targets;
}
private static String hashChunk(int chunkX, int chunkZ, FabricBlockBuffer blocks, Hunk<PlatformBiome> biomes, int height) {
MessageDigest blockDigest = sha256();
MessageDigest biomeDigest = sha256();
Map<PlatformBlockState, byte[]> blockCache = new HashMap<>();
Map<PlatformBiome, byte[]> biomeCache = new HashMap<>();
byte[] plains = "minecraft:plains\n".getBytes(StandardCharsets.UTF_8);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < height; y++) {
PlatformBlockState state = blocks.get(x, y, z);
byte[] bytes = blockCache.computeIfAbsent(state, (PlatformBlockState s) -> (s.key() + "\n").getBytes(StandardCharsets.UTF_8));
blockDigest.update(bytes);
}
}
}
for (int x = 0; x < 16; x += BIOME_STEP) {
for (int z = 0; z < 16; z += BIOME_STEP) {
for (int y = 0; y < height; y += BIOME_STEP) {
PlatformBiome biome = biomes.get(x, y, z);
byte[] bytes = biome == null
? plains
: biomeCache.computeIfAbsent(biome, (PlatformBiome b) -> (b.key() + "\n").getBytes(StandardCharsets.UTF_8));
biomeDigest.update(bytes);
}
}
}
return chunkX + " " + chunkZ + " "
+ HexFormat.of().formatHex(blockDigest.digest()) + " "
+ HexFormat.of().formatHex(biomeDigest.digest());
}
private static String combinedHash(List<String> body) {
MessageDigest digest = sha256();
for (String line : body) {
digest.update((line + "\n").getBytes(StandardCharsets.UTF_8));
}
return HexFormat.of().formatHex(digest.digest());
}
private static long chunkKey(int x, int z) {
return (((long) x) << 32) ^ (z & 0xFFFFFFFFL);
}
private static File clonePack(File source, File workRoot) throws Exception {
File destination = new File(workRoot, source.getName());
Process clone = new ProcessBuilder("cp", "-Rc", source.getAbsolutePath(), destination.getAbsolutePath())
.inheritIO()
.start();
if (clone.waitFor() != 0) {
Process copy = new ProcessBuilder("cp", "-R", source.getAbsolutePath(), destination.getAbsolutePath())
.inheritIO()
.start();
if (copy.waitFor() != 0) {
throw new IllegalStateException("Failed to copy pack to " + destination.getAbsolutePath());
}
}
return destination;
}
private static void settle() throws InterruptedException {
long quietSince = System.currentTimeMillis();
long start = quietSince;
while (System.currentTimeMillis() - start < 15000L) {
List<Throwable> batch = drainReported();
if (batch.isEmpty()) {
if (System.currentTimeMillis() - quietSince >= 1500L) {
break;
}
} else {
for (Throwable error : batch) {
LOGGER.warn("[parity] engine-init reported error (non-fatal)", error);
}
quietSince = System.currentTimeMillis();
}
Thread.sleep(50L);
}
}
private static List<Throwable> drainReported() {
synchronized (REPORTED) {
List<Throwable> drained = new ArrayList<>(REPORTED);
REPORTED.clear();
return drained;
}
}
private static MessageDigest sha256() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
}
@@ -0,0 +1,191 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.LogLevel;
import art.arcane.iris.spi.PlatformBiomeWriter;
import art.arcane.iris.spi.PlatformCapabilities;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.spi.PlatformScheduler;
import art.arcane.iris.spi.PlatformStructureHooks;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import net.minecraft.server.MinecraftServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.function.Consumer;
import java.util.function.Supplier;
public final class FabricPlatform implements IrisPlatform {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static volatile Consumer<Throwable> ERROR_SINK = null;
private final Supplier<MinecraftServer> server;
private final FabricRegistries registries;
private final FabricScheduler scheduler;
private final FabricCapabilities capabilities;
private final FabricStructureHooks structureHooks;
private final FabricBiomeWriter biomeWriter;
public FabricPlatform(Supplier<MinecraftServer> server) {
this.server = server;
this.registries = new FabricRegistries(server);
this.scheduler = new FabricScheduler();
this.capabilities = new FabricCapabilities();
this.structureHooks = new FabricStructureHooks(server);
this.biomeWriter = new FabricBiomeWriter();
}
public static void errorSink(Consumer<Throwable> sink) {
ERROR_SINK = sink;
}
public MinecraftServer server() {
return server.get();
}
@Override
public String platformName() {
return "fabric";
}
@Override
public String minecraftVersion() {
return FabricLoader.getInstance().getModContainer("minecraft")
.map((ModContainer container) -> container.getMetadata().getVersion().getFriendlyString())
.orElse("unknown");
}
@Override
public PlatformRegistries registries() {
return registries;
}
@Override
public PlatformScheduler scheduler() {
return scheduler;
}
@Override
public PlatformCapabilities capabilities() {
return capabilities;
}
@Override
public PlatformStructureHooks structureHooks() {
return structureHooks;
}
@Override
public PlatformBiomeWriter biomeWriter() {
return biomeWriter;
}
@Override
public File dataFolder() {
File folder = FabricLoader.getInstance().getConfigDir().resolve("iris").toFile();
folder.mkdirs();
return folder;
}
@Override
public File dataFile(String... path) {
File file = new File(dataFolder(), String.join(File.separator, path));
file.getParentFile().mkdirs();
return file;
}
@Override
public File pluginJar() {
return FabricLoader.getInstance().getModContainer("irisworldgen")
.flatMap((ModContainer container) -> container.getOrigin().getPaths().stream().findFirst())
.map((java.nio.file.Path p) -> p.toFile())
.orElse(new File(dataFolder(), "iris-fabric.jar"));
}
@Override
public int irisVersionNumber() {
return 0;
}
@Override
public int minecraftVersionNumber() {
return 0;
}
@Override
public void callEvent(Object event) {
}
@Override
public void dispatchConsoleCommand(String command) {
}
@Override
public void log(LogLevel level, String message) {
switch (level) {
case DEBUG -> LOGGER.debug(message);
case INFO -> LOGGER.info(message);
case WARN -> LOGGER.warn(message);
case ERROR -> LOGGER.error(message);
}
}
@Override
public void msg(String message) {
LOGGER.info(message);
}
@Override
public void reportError(Throwable error) {
Consumer<Throwable> sink = ERROR_SINK;
if (sink != null && error != null) {
sink.accept(error);
return;
}
if (error != null) {
LOGGER.error("Iris reported error", error);
}
}
private static final class FabricCapabilities implements PlatformCapabilities {
@Override
public boolean customBiomes() {
return true;
}
@Override
public boolean dataPacks() {
return true;
}
@Override
public boolean structurePlacement() {
return true;
}
@Override
public boolean regionizedThreading() {
return false;
}
}
}
@@ -0,0 +1,154 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformEntityType;
import art.arcane.iris.spi.PlatformItem;
import art.arcane.iris.spi.PlatformRegistries;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.item.Item;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.state.BlockState;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.function.Supplier;
public final class FabricRegistries implements PlatformRegistries {
private final Supplier<MinecraftServer> server;
public FabricRegistries(Supplier<MinecraftServer> server) {
this.server = server;
}
@Override
public PlatformBlockState block(String key) {
return FabricBlockResolution.get(key);
}
@Override
public PlatformBlockState blockOrNull(String key) {
return FabricBlockResolution.getOrNull(key);
}
@Override
public PlatformBlockState blockOrNull(String key, boolean warn) {
return FabricBlockResolution.getOrNull(key, warn);
}
@Override
public PlatformBlockState air() {
return FabricBlockResolution.getAir();
}
@Override
public PlatformBlockState deepSlateOre(PlatformBlockState block, PlatformBlockState ore) {
BlockState result = FabricBlockResolution.toDeepSlateOre((BlockState) block.nativeHandle(), (BlockState) ore.nativeHandle());
return FabricBlockState.of(result, null);
}
@Override
public PlatformBiome biome(String key) {
Identifier identifier = Identifier.tryParse(key);
if (identifier == null) {
return null;
}
Registry<Biome> registry = biomeRegistry();
if (registry == null) {
return null;
}
Biome biome = registry.getValue(identifier);
return biome == null ? null : FabricBiome.of(biome, identifier.toString());
}
@Override
public PlatformItem item(String key) {
String normalized = key.trim().toLowerCase(Locale.ROOT).replace(' ', '_');
if (normalized.startsWith("minecraft:")) {
normalized = normalized.substring("minecraft:".length());
}
Identifier identifier = Identifier.tryParse("minecraft:" + normalized);
if (identifier == null || !BuiltInRegistries.ITEM.containsKey(identifier)) {
return null;
}
Item item = BuiltInRegistries.ITEM.getValue(identifier);
return FabricItem.of(item, identifier.toString());
}
@Override
public PlatformEntityType entity(String key) {
Identifier identifier = Identifier.tryParse(key);
if (identifier == null || !BuiltInRegistries.ENTITY_TYPE.containsKey(identifier)) {
return null;
}
EntityType<?> type = BuiltInRegistries.ENTITY_TYPE.getValue(identifier);
return FabricEntityType.of(type, identifier.toString());
}
@Override
public List<String> blockKeys() {
List<String> keys = new ArrayList<>();
for (Identifier identifier : BuiltInRegistries.BLOCK.keySet()) {
keys.add(identifier.toString());
}
return keys;
}
@Override
public List<String> biomeKeys() {
List<String> keys = new ArrayList<>();
Registry<Biome> registry = biomeRegistry();
if (registry == null) {
return keys;
}
for (Identifier identifier : registry.keySet()) {
keys.add(identifier.toString());
}
return keys;
}
@Override
public List<String> structureKeys() {
List<String> keys = new ArrayList<>();
MinecraftServer instance = server.get();
if (instance == null) {
return keys;
}
for (Identifier identifier : instance.registryAccess().lookupOrThrow(Registries.STRUCTURE).keySet()) {
keys.add(identifier.toString());
}
return keys;
}
private Registry<Biome> biomeRegistry() {
MinecraftServer instance = server.get();
if (instance == null) {
return null;
}
return instance.registryAccess().lookupOrThrow(Registries.BIOME);
}
}
@@ -0,0 +1,47 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.spi.PlatformScheduler;
import art.arcane.iris.spi.PlatformWorld;
public final class FabricScheduler implements PlatformScheduler {
@Override
public void global(Runnable task) {
task.run();
}
@Override
public void region(PlatformWorld world, int chunkX, int chunkZ, Runnable task) {
task.run();
}
@Override
public void async(Runnable task) {
task.run();
}
@Override
public void laterGlobal(Runnable task, int ticks) {
}
@Override
public void laterRegion(PlatformWorld world, int chunkX, int chunkZ, Runnable task, int ticks) {
}
}
@@ -0,0 +1,74 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.engine.object.BlockDataMergeSupport;
import art.arcane.iris.spi.PlatformBlockState;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.Property;
import java.util.Map;
public final class FabricStateMerger implements BlockDataMergeSupport.StateMerger {
@Override
public PlatformBlockState merge(PlatformBlockState base, PlatformBlockState update) {
FabricBlockState fabricBase = (FabricBlockState) base;
FabricBlockState fabricUpdate = (FabricBlockState) update;
try {
return FabricBlockState.of(mergeStates(fabricBase.handle(), fabricUpdate.handle(), fabricUpdate.parsedProperties()), null);
} catch (IllegalArgumentException e) {
FabricBlockResolution.Parsed normalizedBase = FabricBlockResolution.resolveGet(FabricBlockState.serialize(fabricBase.handle()));
FabricBlockResolution.Parsed normalizedUpdate = FabricBlockResolution.resolveGet(FabricBlockState.serialize(fabricUpdate.handle()));
if (normalizedBase != null && normalizedUpdate != null) {
try {
return FabricBlockState.of(mergeStates(normalizedBase.state(), normalizedUpdate.state(), normalizedUpdate.properties()), null);
} catch (IllegalArgumentException ignored) {
return FabricBlockState.of(normalizedUpdate.state(), normalizedUpdate.properties());
}
}
if (normalizedUpdate != null) {
return FabricBlockState.of(normalizedUpdate.state(), normalizedUpdate.properties());
}
return update;
}
}
private static BlockState mergeStates(BlockState base, BlockState update, Map<Property<?>, Comparable<?>> parsedProperties) {
if (parsedProperties == null) {
throw new IllegalArgumentException("Block data not created via string parsing");
}
if (base.getBlock() != update.getBlock()) {
throw new IllegalArgumentException("States have different types (got " + update.getBlock() + ", expected " + base.getBlock() + ")");
}
BlockState merged = base;
for (Map.Entry<Property<?>, Comparable<?>> entry : parsedProperties.entrySet()) {
merged = apply(merged, entry.getKey(), entry.getValue());
}
return merged;
}
@SuppressWarnings("unchecked")
private static <T extends Comparable<T>> BlockState apply(BlockState state, Property<?> property, Comparable<?> value) {
return state.setValue((Property<T>) property, (T) value);
}
}
@@ -0,0 +1,350 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.math.IrisBlockVector;
import net.minecraft.core.Direction;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BooleanProperty;
import net.minecraft.world.level.block.state.properties.EnumProperty;
import net.minecraft.world.level.block.state.properties.IntegerProperty;
import net.minecraft.world.level.block.state.properties.Property;
import net.minecraft.world.level.block.state.properties.RedstoneSide;
import net.minecraft.world.level.block.state.properties.RotationSegment;
import net.minecraft.world.level.block.state.properties.WallSide;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public final class FabricStateRotator implements IrisObjectRotation.StateRotator {
private static final String[] FACE_NAMES = {"north", "east", "south", "west", "up", "down"};
private static final String[] WALL_FACE_NAMES = {"north", "south", "east", "west"};
private static final int[][] ROTATION_CYCLE_MODS = {
{0, 1}, {-1, 2}, {-1, 1}, {-2, 1}, {-1, 0}, {-2, -1}, {-1, -1}, {-1, -2},
{0, -1}, {1, -2}, {1, -1}, {2, -1}, {1, 0}, {2, 1}, {1, 1}, {1, 2}
};
@Override
public PlatformBlockState rotate(IrisObjectRotation rotation, PlatformBlockState state, int spinxx, int spinyy, int spinzz) {
FabricBlockState fabric = (FabricBlockState) state;
BlockState d = fabric.handle();
boolean deleted = false;
try {
int spinx = (int) (90D * (Math.ceil(Math.abs((spinxx % 360D) / 90D))));
int spiny = (int) (90D * (Math.ceil(Math.abs((spinyy % 360D) / 90D))));
int spinz = (int) (90D * (Math.ceil(Math.abs((spinzz % 360D) / 90D))));
if (!rotation.canRotate()) {
return state;
}
Property<?> facing = findFacing(d);
Property<?> rotationProperty = findRotation(d);
Property<?> axis = findAxis(d);
Map<String, BooleanProperty> multipleFacing = findMultipleFacing(d);
Map<String, EnumProperty<WallSide>> wall = findWall(d);
Map<String, EnumProperty<RedstoneSide>> wire = findWire(d);
if (facing != null) {
Direction f = (Direction) d.getValue(facing);
IrisBlockVector bv = new IrisBlockVector(f.getStepX(), f.getStepY(), f.getStepZ());
bv = rotation.rotate(bv.clone(), spinx, spiny, spinz);
Direction t = faceFor(bv);
if (facing.getPossibleValues().contains(t)) {
d = apply(d, facing, t);
} else if (!FabricBlockResolution.isSolid(d.getBlock().defaultBlockState())) {
deleted = true;
}
} else if (rotationProperty != null) {
int segment = (Integer) d.getValue(rotationProperty);
int[] mods = ROTATION_CYCLE_MODS[segment];
IrisBlockVector bv = new IrisBlockVector(mods[0], 0, mods[1]);
bv = rotation.rotate(bv.clone(), spinx, spiny, spinz);
int[] hex = hexMods(bv);
if (hex[0] == 0 && hex[1] == 0) {
throw new IllegalArgumentException("Invalid face, only horizontal face are allowed for this property!");
}
double length = Math.sqrt((double) (hex[0] * hex[0] + hex[1] * hex[1]));
float angle = (float) -Math.toDegrees(Math.atan2(hex[0] / length, hex[1] / length));
d = apply(d, rotationProperty, RotationSegment.convertToSegment(angle));
} else if (axis != null) {
Direction.Axis current = (Direction.Axis) d.getValue(axis);
IrisBlockVector bv = axisFaceVector(current);
bv = rotation.rotate(bv.clone(), spinx, spiny, spinz);
Direction.Axis a = axisOf(bv);
if (!a.equals(current) && axis.getPossibleValues().contains(a)) {
d = apply(d, axis, a);
}
} else if (multipleFacing.size() >= 2) {
List<String> trueFaces = new ArrayList<>();
for (Map.Entry<String, BooleanProperty> entry : multipleFacing.entrySet()) {
if (d.getValue(entry.getValue())) {
trueFaces.add(entry.getKey());
}
}
List<String> faces = new ArrayList<>();
for (String name : trueFaces) {
IrisBlockVector bv = faceVector(name);
bv = rotation.rotate(bv.clone(), spinx, spiny, spinz);
String r = faceName(faceFor(bv));
if (multipleFacing.containsKey(r)) {
faces.add(r);
}
}
for (String name : trueFaces) {
d = apply(d, multipleFacing.get(name), Boolean.FALSE);
}
for (String name : faces) {
d = apply(d, multipleFacing.get(name), Boolean.TRUE);
}
} else if (wall.size() == 4) {
Map<String, WallSide> heights = new LinkedHashMap<>();
for (String name : WALL_FACE_NAMES) {
WallSide h = (WallSide) d.getValue(wall.get(name));
IrisBlockVector bv = faceVector(name);
bv = rotation.rotate(bv.clone(), spinx, spiny, spinz);
String r = faceName(faceFor(bv));
if (wall.containsKey(r)) {
heights.put(r, h);
}
}
for (String name : WALL_FACE_NAMES) {
d = apply(d, wall.get(name), heights.getOrDefault(name, WallSide.NONE));
}
} else if (wire.size() == 4) {
Map<String, RedstoneSide> connections = new LinkedHashMap<>();
for (String name : WALL_FACE_NAMES) {
RedstoneSide connection = (RedstoneSide) d.getValue(wire.get(name));
IrisBlockVector bv = faceVector(name);
bv = rotation.rotate(bv.clone(), spinx, spiny, spinz);
String r = faceName(faceFor(bv));
if (wire.containsKey(r)) {
connections.put(r, connection);
}
}
for (String name : WALL_FACE_NAMES) {
d = apply(d, wire.get(name), connections.getOrDefault(name, RedstoneSide.NONE));
}
}
} catch (Throwable e) {
IrisLogging.reportError(e);
}
if (deleted) {
return null;
}
return d == fabric.handle() ? state : FabricBlockState.of(d, fabric.parsedProperties());
}
private static Property<?> findFacing(BlockState state) {
for (Property<?> property : state.getProperties()) {
if (property.getName().equals("facing") && property.getValueClass() == Direction.class) {
return property;
}
}
return null;
}
private static Property<?> findRotation(BlockState state) {
for (Property<?> property : state.getProperties()) {
if (property.getName().equals("rotation") && property instanceof IntegerProperty) {
return property;
}
}
return null;
}
private static Property<?> findAxis(BlockState state) {
for (Property<?> property : state.getProperties()) {
if (property.getName().equals("axis") && property.getValueClass() == Direction.Axis.class) {
return property;
}
}
return null;
}
private static Map<String, BooleanProperty> findMultipleFacing(BlockState state) {
Map<String, BooleanProperty> properties = new LinkedHashMap<>();
for (String name : FACE_NAMES) {
for (Property<?> property : state.getProperties()) {
if (property.getName().equals(name) && property instanceof BooleanProperty bool) {
properties.put(name, bool);
}
}
}
return properties;
}
@SuppressWarnings("unchecked")
private static Map<String, EnumProperty<WallSide>> findWall(BlockState state) {
Map<String, EnumProperty<WallSide>> properties = new LinkedHashMap<>();
for (String name : WALL_FACE_NAMES) {
for (Property<?> property : state.getProperties()) {
if (property.getName().equals(name) && property.getValueClass() == WallSide.class) {
properties.put(name, (EnumProperty<WallSide>) property);
}
}
}
return properties;
}
@SuppressWarnings("unchecked")
private static Map<String, EnumProperty<RedstoneSide>> findWire(BlockState state) {
Map<String, EnumProperty<RedstoneSide>> properties = new LinkedHashMap<>();
for (String name : WALL_FACE_NAMES) {
for (Property<?> property : state.getProperties()) {
if (property.getName().equals(name) && property.getValueClass() == RedstoneSide.class) {
properties.put(name, (EnumProperty<RedstoneSide>) property);
}
}
}
return properties;
}
private static Direction faceFor(IrisBlockVector v) {
int x = (int) Math.round(v.getX());
int y = (int) Math.round(v.getY());
int z = (int) Math.round(v.getZ());
if (x == 0 && z == -1) {
return Direction.NORTH;
}
if (x == 0 && z == 1) {
return Direction.SOUTH;
}
if (x == 1 && z == 0) {
return Direction.EAST;
}
if (x == -1 && z == 0) {
return Direction.WEST;
}
if (y > 0) {
return Direction.UP;
}
if (y < 0) {
return Direction.DOWN;
}
return Direction.SOUTH;
}
private static int[] hexMods(IrisBlockVector v) {
int x = v.getBlockX();
int y = v.getBlockY();
int z = v.getBlockZ();
if (x == 0 && z == -1) return new int[]{0, -1};
if (x == 1 && z == -2) return new int[]{1, -2};
if (x == 1 && z == -1) return new int[]{1, -1};
if (x == 2 && z == -1) return new int[]{2, -1};
if (x == 1 && z == 0) return new int[]{1, 0};
if (x == 2 && z == 1) return new int[]{2, 1};
if (x == 1 && z == 1) return new int[]{1, 1};
if (x == 1 && z == 2) return new int[]{1, 2};
if (x == 0 && z == 1) return new int[]{0, 1};
if (x == -1 && z == 2) return new int[]{-1, 2};
if (x == -1 && z == 1) return new int[]{-1, 1};
if (x == -2 && z == 1) return new int[]{-2, 1};
if (x == -1 && z == 0) return new int[]{-1, 0};
if (x == -2 && z == -1) return new int[]{-2, -1};
if (x == -1 && z == -1) return new int[]{-1, -1};
if (x == -1 && z == -2) return new int[]{-1, -2};
if (y > 0) {
return new int[]{0, 0};
}
if (y < 0) {
return new int[]{0, 0};
}
return new int[]{0, 1};
}
private static IrisBlockVector axisFaceVector(Direction.Axis axis) {
if (axis == Direction.Axis.X) {
return new IrisBlockVector(1, 0, 0);
}
if (axis == Direction.Axis.Y) {
return new IrisBlockVector(0, 1, 0);
}
return new IrisBlockVector(0, 0, 1);
}
private static Direction.Axis axisOf(IrisBlockVector v) {
if (Math.abs(v.getBlockX()) > Math.max(Math.abs(v.getBlockY()), Math.abs(v.getBlockZ()))) {
return Direction.Axis.X;
}
if (Math.abs(v.getBlockY()) > Math.max(Math.abs(v.getBlockX()), Math.abs(v.getBlockZ()))) {
return Direction.Axis.Y;
}
if (Math.abs(v.getBlockZ()) > Math.max(Math.abs(v.getBlockX()), Math.abs(v.getBlockY()))) {
return Direction.Axis.Z;
}
return Direction.Axis.Y;
}
private static IrisBlockVector faceVector(String name) {
return switch (name) {
case "north" -> new IrisBlockVector(0, 0, -1);
case "south" -> new IrisBlockVector(0, 0, 1);
case "east" -> new IrisBlockVector(1, 0, 0);
case "west" -> new IrisBlockVector(-1, 0, 0);
case "up" -> new IrisBlockVector(0, 1, 0);
case "down" -> new IrisBlockVector(0, -1, 0);
default -> new IrisBlockVector(0, 0, 0);
};
}
private static String faceName(Direction direction) {
return direction.getSerializedName();
}
@SuppressWarnings("unchecked")
private static <T extends Comparable<T>> BlockState apply(BlockState state, Property<?> property, Object value) {
return state.setValue((Property<T>) property, (T) value);
}
}
@@ -0,0 +1,116 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.spi.PlatformStructureHooks;
import art.arcane.iris.spi.PlatformWorld;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.levelgen.structure.Structure;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
public final class FabricStructureHooks implements PlatformStructureHooks {
private final Supplier<MinecraftServer> server;
public FabricStructureHooks(Supplier<MinecraftServer> server) {
this.server = server;
}
@Override
public List<String> structureKeys() {
return registryKeys(Registries.STRUCTURE);
}
@Override
public List<String> structureSetKeys() {
return registryKeys(Registries.STRUCTURE_SET);
}
@Override
public List<String> structureBiomeKeys(String structureKey) {
List<String> keys = new ArrayList<>();
MinecraftServer instance = server.get();
if (instance == null) {
return keys;
}
Identifier identifier = Identifier.tryParse(structureKey);
if (identifier == null) {
return keys;
}
Registry<Structure> registry = instance.registryAccess().lookupOrThrow(Registries.STRUCTURE);
Structure structure = registry.getValue(identifier);
if (structure == null) {
return keys;
}
for (Holder<Biome> holder : structure.biomes()) {
holder.unwrapKey().ifPresent((net.minecraft.resources.ResourceKey<Biome> key) -> keys.add(key.identifier().toString()));
}
return keys;
}
@Override
public List<String> objectFeatureKeys() {
return registryKeys(Registries.CONFIGURED_FEATURE);
}
@Override
public List<String> reachableStructureKeys(PlatformWorld world) {
return List.of();
}
@Override
public List<String> possibleBiomeKeys(PlatformWorld world) {
return List.of();
}
@Override
public boolean placeFeature(PlatformWorld world, int x, int y, int z, String featureKey, long seed) {
return false;
}
@Override
public int[] placeStructure(PlatformWorld world, int chunkX, int chunkZ, String structureKey, long seed, int maxSpan) {
return null;
}
@Override
public boolean supportsStructurePlacement() {
return false;
}
private <T> List<String> registryKeys(net.minecraft.resources.ResourceKey<net.minecraft.core.Registry<T>> registryKey) {
List<String> keys = new ArrayList<>();
MinecraftServer instance = server.get();
if (instance == null) {
return keys;
}
Registry<T> registry = instance.registryAccess().lookupOrThrow(registryKey);
for (Identifier identifier : registry.keySet()) {
keys.add(identifier.toString());
}
return keys;
}
}
@@ -0,0 +1,51 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.engine.object.TileData;
import art.arcane.volmlib.util.collection.KMap;
import java.io.DataOutputStream;
import java.io.IOException;
public final class FabricTileData extends TileData {
private final byte[] raw;
private final KMap<String, Object> tileProperties;
FabricTileData(byte[] raw, KMap<String, Object> tileProperties) {
super();
this.raw = raw;
this.tileProperties = tileProperties == null ? new KMap<>() : tileProperties;
}
@Override
public KMap<String, Object> getProperties() {
return tileProperties;
}
@Override
public void toBinary(DataOutputStream out) throws IOException {
out.write(raw);
}
@Override
public TileData clone() {
return this;
}
}
@@ -0,0 +1,250 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.engine.object.TileData;
import art.arcane.volmlib.util.collection.KMap;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.Strictness;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.block.entity.BannerPattern;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Locale;
import java.util.function.Supplier;
public final class FabricTileReader implements TileData.TileReader {
private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().setStrictness(Strictness.LENIENT).create();
private static final int DYE_COLOR_COUNT = 16;
private final Supplier<MinecraftServer> server;
public FabricTileReader(Supplier<MinecraftServer> server) {
this.server = server;
}
private static final class ReplayInputStream extends InputStream {
private final InputStream source;
private byte[] buffer = new byte[256];
private int size = 0;
private int position = 0;
private int marked = 0;
private ReplayInputStream(InputStream source) {
this.source = source;
}
@Override
public int read() throws IOException {
if (position < size) {
int value = buffer[position] & 0xFF;
position++;
return value;
}
int value = source.read();
if (value < 0) {
return value;
}
if (size == buffer.length) {
buffer = Arrays.copyOf(buffer, buffer.length * 2);
}
buffer[size] = (byte) value;
size++;
position++;
return value;
}
@Override
public boolean markSupported() {
return true;
}
@Override
public synchronized void mark(int readLimit) {
marked = position;
}
@Override
public synchronized void reset() {
position = marked;
}
private void rewind() {
position = 0;
}
private int position() {
return position;
}
private byte[] consumed() {
return Arrays.copyOf(buffer, position);
}
}
@Override
public TileData read(DataInputStream in) throws IOException {
if (!in.markSupported()) {
throw new IOException("Mark not supported");
}
in.mark(Integer.MAX_VALUE);
ReplayInputStream replay = new ReplayInputStream(in);
DataInputStream din = new DataInputStream(replay);
try {
return parse(din, replay);
} finally {
in.reset();
in.skipNBytes(replay.position());
in.mark(0);
}
}
private TileData parse(DataInputStream din, ReplayInputStream replay) throws IOException {
try {
String materialKey = din.readUTF();
boolean materialMatched = matchMaterial(materialKey);
String json = din.readUTF();
KMap<String, Object> properties = kmapFromJson(json);
if (!materialMatched) {
throw new NullPointerException("material is marked non-null but is null");
}
if (properties == null) {
throw new NullPointerException("properties is marked non-null but is null");
}
return new FabricTileData(replay.consumed(), properties);
} catch (Throwable e) {
replay.rewind();
return parseLegacy(din, replay);
}
}
@SuppressWarnings("unchecked")
private static KMap<String, Object> kmapFromJson(String json) {
return GSON.fromJson(json, KMap.class);
}
private TileData parseLegacy(DataInputStream din, ReplayInputStream replay) throws IOException {
int id = din.readShort();
switch (id) {
case 0 -> readSign(din);
case 1 -> readSpawner(din, replay);
case 2 -> readBanner(din, replay);
case 3 -> readLootable(din);
default -> throw new IOException("Unknown tile type: " + id);
}
return new FabricTileData(replay.consumed(), new KMap<>());
}
private static void readSign(DataInputStream din) throws IOException {
din.readUTF();
din.readUTF();
din.readUTF();
din.readUTF();
byte dye = din.readByte();
if (dye < 0 || dye >= DYE_COLOR_COUNT) {
throw new ArrayIndexOutOfBoundsException("Index " + dye + " out of bounds for length " + DYE_COLOR_COUNT);
}
}
private static void readSpawner(DataInputStream din, ReplayInputStream replay) throws IOException {
boolean resolved = false;
replay.mark(Integer.MAX_VALUE);
try {
String keyString = din.readUTF();
Identifier key = Identifier.tryParse(keyString);
resolved = key != null && BuiltInRegistries.ENTITY_TYPE.containsKey(key);
if (!resolved) {
replay.reset();
}
} catch (Throwable ignored) {
replay.reset();
}
if (!resolved) {
din.readShort();
}
}
private void readBanner(DataInputStream din, ReplayInputStream replay) throws IOException {
din.readUnsignedByte();
int listSize = din.readUnsignedByte();
replay.mark(Integer.MAX_VALUE);
boolean parsedKeyed = false;
try {
for (int i = 0; i < listSize; i++) {
din.readUnsignedByte();
Identifier patternKey = Identifier.tryParse(din.readUTF());
if (patternKey == null || !bannerPatternExists(patternKey)) {
throw new IOException("Unknown banner pattern key");
}
}
parsedKeyed = true;
} catch (Throwable ignored) {
replay.reset();
}
if (parsedKeyed) {
return;
}
for (int i = 0; i < listSize; i++) {
din.readUnsignedByte();
din.readUnsignedByte();
}
}
private boolean bannerPatternExists(Identifier key) {
MinecraftServer instance = server.get();
if (instance == null) {
return false;
}
Registry<BannerPattern> registry = instance.registryAccess().lookupOrThrow(Registries.BANNER_PATTERN);
return registry.containsKey(key);
}
private static void readLootable(DataInputStream din) throws IOException {
din.readUTF();
din.readUTF();
din.readLong();
}
private static boolean matchMaterial(String name) {
String filtered = name;
if (filtered.startsWith("minecraft:")) {
filtered = filtered.substring("minecraft:".length());
}
filtered = filtered.toUpperCase(Locale.ROOT);
filtered = filtered.replaceAll("\\s+", "_").replaceAll("\\W", "");
Identifier identifier = Identifier.tryParse("minecraft:" + filtered.toLowerCase(Locale.ROOT));
if (identifier == null) {
return false;
}
return BuiltInRegistries.ITEM.containsKey(identifier) || BuiltInRegistries.BLOCK.containsKey(identifier);
}
}
@@ -0,0 +1,164 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.levelgen.Heightmap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
public final class FabricWorldCheck {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private FabricWorldCheck() {
}
public static void schedule() {
Thread thread = new Thread(FabricWorldCheck::waitAndRun, "Iris World Check");
thread.setDaemon(true);
thread.start();
}
private static void waitAndRun() {
long start = System.currentTimeMillis();
MinecraftServer server = null;
while (System.currentTimeMillis() - start < 600000L) {
Object instance = FabricLoader.getInstance().getGameInstance();
if (instance instanceof MinecraftServer candidate && candidate.isReady()) {
server = candidate;
break;
}
try {
Thread.sleep(250L);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
if (server == null) {
LOGGER.error("[worldcheck] server did not become ready within 10 minutes");
return;
}
MinecraftServer serverRef = server;
AtomicBoolean pass = new AtomicBoolean(false);
try {
serverRef.submit(() -> pass.set(run(serverRef))).join();
} catch (Throwable e) {
LOGGER.error("[worldcheck] check failed", e);
}
LOGGER.info("[worldcheck] shutting down dev server (result={})", pass.get() ? "PASS" : "FAIL");
serverRef.halt(false);
}
private static boolean run(MinecraftServer server) {
ServerLevel overworld = server.overworld();
String generatorClass = overworld.getChunkSource().getGenerator().getClass().getName();
LOGGER.info("[worldcheck] overworld generator: {}", generatorClass);
boolean irisGenerator = overworld.getChunkSource().getGenerator() instanceof IrisFabricChunkGenerator;
if (!irisGenerator) {
LOGGER.error("[worldcheck] overworld is NOT using IrisFabricChunkGenerator");
}
BlockPos spawn = overworld.getRespawnData().pos();
LOGGER.info("[worldcheck] spawn: {} {} {} (minY={} height={})", spawn.getX(), spawn.getY(), spawn.getZ(), overworld.getMinY(), overworld.getHeight());
MessageDigest digest = sha256();
List<String> samples = new ArrayList<>();
Set<String> surfaceKeys = new LinkedHashSet<>();
for (int dx = 0; dx < 4; dx++) {
for (int dz = 0; dz < 4; dz++) {
int x = spawn.getX() + (dx - 2) * 16 + 8;
int z = spawn.getZ() + (dz - 2) * 16 + 8;
overworld.getChunk(x >> 4, z >> 4);
int y = overworld.getHeight(Heightmap.Types.WORLD_SURFACE, x, z);
BlockState surface = overworld.getBlockState(new BlockPos(x, y - 1, z));
String key = BuiltInRegistries.BLOCK.getKey(surface.getBlock()).toString();
String line = x + " " + (y - 1) + " " + z + " " + key;
samples.add(line);
surfaceKeys.add(key);
digest.update((line + "\n").getBytes(StandardCharsets.UTF_8));
}
}
for (int i = 0; i < Math.min(6, samples.size()); i++) {
LOGGER.info("[worldcheck] surface sample: {}", samples.get(i));
}
LOGGER.info("[worldcheck] surface digest: {} ({} columns, {} distinct surface blocks: {})",
HexFormat.of().formatHex(digest.digest()).substring(0, 12), samples.size(), surfaceKeys.size(), surfaceKeys);
ChunkAccess zeroChunk = overworld.getChunk(0, 0);
int nonEmptySections = 0;
for (LevelChunkSection section : zeroChunk.getSections()) {
if (!section.hasOnlyAir()) {
nonEmptySections++;
}
}
Set<String> columnKeys = new LinkedHashSet<>();
int surfaceY = overworld.getHeight(Heightmap.Types.WORLD_SURFACE, 8, 8);
for (int y = overworld.getMinY(); y < surfaceY; y += 16) {
BlockState state = zeroChunk.getBlockState(new BlockPos(8, y, 8));
if (!state.isAir()) {
columnKeys.add(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString());
}
}
LOGGER.info("[worldcheck] chunk 0,0: {} non-empty sections of {}; column blocks at (8,*,8): {}",
nonEmptySections, zeroChunk.getSections().length, columnKeys);
boolean sectionsOk = nonEmptySections >= 4;
boolean varietyOk = columnKeys.size() >= 2 || surfaceKeys.size() >= 2;
if (!sectionsOk) {
LOGGER.error("[worldcheck] chunk 0,0 looks empty/vanilla-flat ({} non-empty sections)", nonEmptySections);
}
if (!varietyOk) {
LOGGER.error("[worldcheck] generated terrain has no block variety (flat-world signature)");
}
boolean pass = irisGenerator && sectionsOk && varietyOk;
LOGGER.info("[worldcheck] {}", pass ? "PASS" : "FAIL");
return pass;
}
private static MessageDigest sha256() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
}
@@ -0,0 +1,137 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisWorld;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.storage.LevelResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class FabricWorldEngines {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final ConcurrentHashMap<ServerLevel, Engine> ENGINES = new ConcurrentHashMap<>();
private FabricWorldEngines() {
}
public static Engine get(ServerLevel level, String dimensionKey) {
Engine existing = ENGINES.get(level);
if (existing != null) {
return existing;
}
return ENGINES.computeIfAbsent(level, (ServerLevel l) -> create(l, dimensionKey));
}
private static Engine create(ServerLevel level, String dimensionKey) {
FabricEngineBootstrap.bind();
File pack = resolvePack(dimensionKey);
IrisData data = IrisData.get(pack);
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
if (dimension == null) {
LOGGER.error("Iris pack at {} does not contain dimension '{}' (expected dimensions/{}.json). Install a matching Iris pack and restart.",
pack.getAbsolutePath(), dimensionKey, dimensionKey);
throw new IllegalStateException("Iris dimension '" + dimensionKey + "' missing from pack " + pack.getAbsolutePath());
}
long seed = level.getSeed();
File worldFolder = DimensionType.getStorageFolder(level.dimension(), level.getServer().getWorldPath(LevelResource.ROOT)).toFile();
IrisWorld world = IrisWorld.builder()
.name(level.dimension().identifier().toString().replace(':', '_'))
.seed(seed)
.worldFolder(worldFolder)
.minHeight(dimension.getMinHeight())
.maxHeight(dimension.getMaxHeight())
.build();
Engine engine = new IrisEngine(new EngineTarget(world, dimension, data), false);
int levelMinY = level.getMinY();
int levelMaxY = level.getMinY() + level.getHeight();
if (dimension.getMinHeight() != levelMinY || dimension.getMaxHeight() != levelMaxY) {
LOGGER.error("Iris pack height mismatch for {}: pack generates {}..{} but the level is {}..{}. Terrain outside the level range will be clipped; ship a matching dimension_type.",
level.dimension().identifier(), dimension.getMinHeight(), dimension.getMaxHeight(), levelMinY, levelMaxY);
}
LOGGER.info("Iris engine up for {}: pack={} dim={} seed={} height={}..{}",
level.dimension().identifier(), pack.getAbsolutePath(), dimension.getLoadKey(), seed, dimension.getMinHeight(), dimension.getMaxHeight());
return engine;
}
private static File resolvePack(String dimensionKey) {
File pack = FabricLoader.getInstance().getConfigDir()
.resolve("irisworldgen")
.resolve("packs")
.resolve(dimensionKey)
.toFile();
if (pack.isDirectory()) {
return pack;
}
String parity = System.getProperty("iris.parity");
if (parity != null) {
String parityPath = parity;
int lastColon = parity.lastIndexOf(':');
if (lastColon > 0) {
try {
Integer.parseInt(parity.substring(lastColon + 1));
parityPath = parity.substring(0, lastColon);
} catch (NumberFormatException ignored) {
}
}
File parityPack = new File(parityPath);
if (parityPack.isDirectory()) {
LOGGER.warn("Iris pack missing at {}; falling back to parity pack {}", pack.getAbsolutePath(), parityPack.getAbsolutePath());
return parityPack;
}
}
LOGGER.error("===============================================================");
LOGGER.error("Iris pack for dimension '{}' is not installed.", dimensionKey);
LOGGER.error("Expected a pack folder at: {}", pack.getAbsolutePath());
LOGGER.error("Install an Iris pack there (the folder must contain dimensions/{}.json) and restart the server.", dimensionKey);
LOGGER.error("===============================================================");
throw new IllegalStateException("Iris pack not installed: " + pack.getAbsolutePath());
}
public static void shutdown() {
for (Map.Entry<ServerLevel, Engine> entry : ENGINES.entrySet()) {
Engine engine = entry.getValue();
try {
if (!engine.isClosed()) {
engine.close();
}
LOGGER.info("Iris engine closed for {}", entry.getKey().dimension().identifier());
} catch (Throwable e) {
LOGGER.error("Iris engine close failed for {}", entry.getKey().dimension().identifier(), e);
}
}
ENGINES.clear();
}
}
@@ -19,8 +19,13 @@
package art.arcane.iris.fabric;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -58,6 +63,22 @@ public final class IrisFabricBootstrap implements ModInitializer {
}
LOGGER.info("Iris core loaded ({} classes ok)", loadedClasses);
LOGGER.info("Iris worldgen is not wired on Fabric yet; this build verifies packaging and engine classloading");
FabricEngineBootstrap.bind();
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, Identifier.fromNamespaceAndPath("irisworldgen", "iris"), IrisFabricChunkGenerator.CODEC);
LOGGER.info("Iris chunk generator registered as irisworldgen:iris");
ServerLifecycleEvents.SERVER_STOPPING.register((MinecraftServer server) -> FabricWorldEngines.shutdown());
String parity = System.getProperty("iris.parity");
if (parity != null) {
LOGGER.info("Iris parity probe armed: {}", parity);
FabricParityProbe.schedule(parity);
}
String worldCheck = System.getProperty("iris.worldcheck");
if (worldCheck != null) {
LOGGER.info("Iris world check armed");
FabricWorldCheck.schedule();
}
}
}
@@ -0,0 +1,340 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.hunk.Hunk;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.QuartPos;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.biome.BiomeResolver;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.biome.Climate;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.blending.Blender;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
public final class IrisFabricChunkGenerator extends ChunkGenerator {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
public static final MapCodec<IrisFabricChunkGenerator> CODEC = RecordCodecBuilder.mapCodec((RecordCodecBuilder.Instance<IrisFabricChunkGenerator> instance) -> instance.group(
BiomeSource.CODEC.fieldOf("biome_source").forGetter((IrisFabricChunkGenerator generator) -> generator.biomeSource),
Codec.STRING.optionalFieldOf("dimension", "overworld").forGetter((IrisFabricChunkGenerator generator) -> generator.dimensionKey)
).apply(instance, IrisFabricChunkGenerator::new));
private final String dimensionKey;
private final ConcurrentHashMap<String, Holder<Biome>> biomeHolders = new ConcurrentHashMap<>();
private final AtomicBoolean announced = new AtomicBoolean(false);
private volatile Engine engine;
public IrisFabricChunkGenerator(BiomeSource biomeSource, String dimensionKey) {
super(biomeSource);
this.dimensionKey = dimensionKey;
}
@Override
protected MapCodec<? extends ChunkGenerator> codec() {
return CODEC;
}
private ServerLevel boundLevel() {
MinecraftServer server = FabricEngineBootstrap.currentServer();
if (server == null) {
return null;
}
for (ServerLevel level : server.getAllLevels()) {
if (level.getChunkSource().getGenerator() == this) {
return level;
}
}
return null;
}
private Engine engine() {
Engine cached = engine;
if (cached != null) {
return cached;
}
synchronized (this) {
if (engine != null) {
return engine;
}
ServerLevel level = boundLevel();
if (level == null) {
throw new IllegalStateException("Iris generator '" + dimensionKey + "' has no bound ServerLevel yet");
}
Engine created = FabricWorldEngines.get(level, dimensionKey);
engine = created;
return created;
}
}
private Engine engineOrNull() {
Engine cached = engine;
if (cached != null) {
return cached;
}
try {
return engine();
} catch (Throwable ignored) {
return null;
}
}
@Override
public CompletableFuture<ChunkAccess> fillFromNoise(Blender blender, RandomState randomState, StructureManager structureManager, ChunkAccess chunk) {
Engine generationEngine = engine();
ChunkPos pos = chunk.getPos();
if (announced.compareAndSet(false, true)) {
LOGGER.info("Iris generating {} through IrisFabricChunkGenerator (dim={} first chunk {},{})",
dimensionKey, generationEngine.getDimension().getLoadKey(), pos.x(), pos.z());
}
LOGGER.debug("Iris generating chunk {},{}", pos.x(), pos.z());
int dimMinY = generationEngine.getMinHeight();
int dimMaxY = generationEngine.getMaxHeight();
int height = dimMaxY - dimMinY;
PlatformBlockState air = IrisPlatforms.get().registries().air();
FabricBlockBuffer blocks = new FabricBlockBuffer(height, air);
Hunk<PlatformBiome> biomes = Hunk.newArrayHunk(16, height, 16);
try {
generationEngine.generate(pos.getMinBlockX(), pos.getMinBlockZ(), blocks, biomes, false);
} catch (Throwable e) {
LOGGER.error("Iris failed to generate chunk {},{}", pos.x(), pos.z(), e);
throw new IllegalStateException("Iris generation failed for chunk " + pos.x() + "," + pos.z(), e);
}
writeBlocks(chunk, blocks, dimMinY, height);
Heightmap.primeHeightmaps(chunk, EnumSet.of(Heightmap.Types.WORLD_SURFACE_WG, Heightmap.Types.OCEAN_FLOOR_WG));
Registry<Biome> biomeRegistry = structureManager.registryAccess().lookupOrThrow(Registries.BIOME);
chunk.fillBiomesFromNoise(new HunkBiomeResolver(this, biomes, biomeRegistry, pos, dimMinY, height), randomState.sampler());
return CompletableFuture.completedFuture(chunk);
}
private Holder<Biome> fallbackBiome(Registry<Biome> registry) {
Holder<Biome> existing = biomeHolders.get("minecraft:plains");
if (existing != null) {
return existing;
}
Holder<Biome> resolved = registry.get(Identifier.fromNamespaceAndPath("minecraft", "plains"))
.<Holder<Biome>>map((Holder.Reference<Biome> reference) -> reference)
.orElseThrow(() -> new IllegalStateException("minecraft:plains missing from biome registry"));
Holder<Biome> raced = biomeHolders.putIfAbsent("minecraft:plains", resolved);
return raced != null ? raced : resolved;
}
private Holder<Biome> holderFor(Registry<Biome> registry, PlatformBiome biome) {
if (biome == null) {
return fallbackBiome(registry);
}
String key = biome.key();
Holder<Biome> existing = biomeHolders.get(key);
if (existing != null) {
return existing;
}
Identifier identifier = Identifier.tryParse(key);
Optional<Holder.Reference<Biome>> reference = identifier == null ? Optional.empty() : registry.get(identifier);
Holder<Biome> resolved = reference.<Holder<Biome>>map((Holder.Reference<Biome> value) -> value).orElseGet(() -> fallbackBiome(registry));
Holder<Biome> raced = biomeHolders.putIfAbsent(key, resolved);
return raced != null ? raced : resolved;
}
private void writeBlocks(ChunkAccess chunk, FabricBlockBuffer blocks, int dimMinY, int height) {
int chunkMinY = chunk.getMinY();
int chunkMaxY = chunkMinY + chunk.getHeight();
int from = Math.max(dimMinY, chunkMinY);
int to = Math.min(dimMinY + height, chunkMaxY);
for (int y = from; y < to; ) {
int sectionIndex = chunk.getSectionIndex(y);
LevelChunkSection section = chunk.getSection(sectionIndex);
int sectionMinY = chunk.getSectionYFromSectionIndex(sectionIndex) << 4;
int sectionEnd = Math.min(sectionMinY + 16, to);
section.acquire();
try {
for (int blockY = y; blockY < sectionEnd; blockY++) {
int bufferY = blockY - dimMinY;
int localY = blockY & 15;
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
if (blocks.isAir(x, bufferY, z)) {
continue;
}
PlatformBlockState state = blocks.getRaw(x, bufferY, z);
section.setBlockState(x, localY, z, (BlockState) state.nativeHandle(), false);
}
}
}
} finally {
section.release();
}
y = sectionEnd;
}
}
private static final class HunkBiomeResolver implements BiomeResolver {
private final IrisFabricChunkGenerator generator;
private final Hunk<PlatformBiome> biomes;
private final Registry<Biome> registry;
private final ChunkPos pos;
private final int dimMinY;
private final int height;
private HunkBiomeResolver(IrisFabricChunkGenerator generator, Hunk<PlatformBiome> biomes, Registry<Biome> registry, ChunkPos pos, int dimMinY, int height) {
this.generator = generator;
this.biomes = biomes;
this.registry = registry;
this.pos = pos;
this.dimMinY = dimMinY;
this.height = height;
}
@Override
public Holder<Biome> getNoiseBiome(int quartX, int quartY, int quartZ, Climate.Sampler sampler) {
int localX = QuartPos.toBlock(quartX) - pos.getMinBlockX();
int localZ = QuartPos.toBlock(quartZ) - pos.getMinBlockZ();
int bufferY = Math.max(0, Math.min(height - 1, QuartPos.toBlock(quartY) - dimMinY));
PlatformBiome biome = biomes.get(localX, bufferY, localZ);
return generator.holderFor(registry, biome);
}
}
@Override
public void applyCarvers(WorldGenRegion region, long seed, RandomState randomState, BiomeManager biomeManager, StructureManager structureManager, ChunkAccess chunk) {
}
@Override
public void buildSurface(WorldGenRegion region, StructureManager structureManager, RandomState randomState, ChunkAccess chunk) {
}
@Override
public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk, StructureManager structureManager) {
}
@Override
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess chunk, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
}
@Override
public void createReferences(WorldGenLevel level, StructureManager structureManager, ChunkAccess chunk) {
}
@Override
public void spawnOriginalMobs(WorldGenRegion region) {
}
@Override
public int getGenDepth() {
Engine current = engineOrNull();
return current == null ? 384 : current.getMaxHeight() - current.getMinHeight();
}
@Override
public int getSeaLevel() {
Engine current = engineOrNull();
return current == null ? 63 : current.getDimension().getFluidHeight();
}
@Override
public int getMinY() {
Engine current = engineOrNull();
return current == null ? -64 : current.getMinHeight();
}
@Override
public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor heightAccessor, RandomState randomState) {
Engine current = engineOrNull();
if (current == null) {
return heightAccessor.getMinY() + Math.min(heightAccessor.getHeight(), 64);
}
boolean ignoreFluid = type == Heightmap.Types.OCEAN_FLOOR || type == Heightmap.Types.OCEAN_FLOOR_WG;
return current.getMinHeight() + current.getHeight(x, z, ignoreFluid) + 1;
}
@Override
public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor heightAccessor, RandomState randomState) {
int minY = heightAccessor.getMinY();
BlockState[] states = new BlockState[heightAccessor.getHeight()];
Engine current = engineOrNull();
BlockState airState = Blocks.AIR.defaultBlockState();
if (current == null) {
for (int i = 0; i < states.length; i++) {
states[i] = airState;
}
return new NoiseColumn(minY, states);
}
int surface = current.getMinHeight() + current.getHeight(x, z, true);
int fluid = current.getDimension().getFluidHeight();
BlockState stone = Blocks.STONE.defaultBlockState();
BlockState water = Blocks.WATER.defaultBlockState();
for (int i = 0; i < states.length; i++) {
int y = minY + i;
if (y <= surface) {
states[i] = stone;
} else if (y <= fluid) {
states[i] = water;
} else {
states[i] = airState;
}
}
return new NoiseColumn(minY, states);
}
@Override
public void addDebugScreenInfo(List<String> info, RandomState randomState, BlockPos pos) {
info.add("Iris dimension: " + dimensionKey);
}
}
@@ -0,0 +1,55 @@
{
"ambient_light": 0.0,
"attributes": {
"minecraft:audio/ambient_sounds": {
"mood": {
"block_search_extent": 8,
"offset": 2.0,
"sound": "minecraft:ambient.cave",
"tick_delay": 6000
}
},
"minecraft:audio/background_music": {
"creative": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.creative"
},
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.game"
}
},
"minecraft:gameplay/bed_rule": {
"can_set_spawn": "always",
"can_sleep": "when_dark",
"error_message": {
"translate": "block.minecraft.bed.no_sleep"
}
},
"minecraft:gameplay/nether_portal_spawns_piglin": true,
"minecraft:gameplay/respawn_anchor_works": false,
"minecraft:visual/ambient_light_color": "#0a0a0a",
"minecraft:visual/cloud_color": "#ccffffff",
"minecraft:visual/cloud_height": 192.33,
"minecraft:visual/fog_color": "#c0d8ff",
"minecraft:visual/sky_color": "#78a7ff"
},
"coordinate_scale": 1.0,
"default_clock": "minecraft:overworld",
"has_ceiling": false,
"has_ender_dragon_fight": false,
"has_skylight": true,
"height": 768,
"infiniburn": "#minecraft:infiniburn_overworld",
"logical_height": 512,
"min_y": -256,
"monster_spawn_block_light_limit": 0,
"monster_spawn_light_level": {
"type": "minecraft:uniform",
"max_inclusive": 7,
"min_inclusive": 0
},
"timelines": "#minecraft:in_overworld"
}
@@ -0,0 +1,11 @@
{
"type": "irisworldgen:overworld",
"generator": {
"type": "irisworldgen:iris",
"dimension": "overworld",
"biome_source": {
"type": "minecraft:fixed",
"biome": "minecraft:plains"
}
}
}
@@ -3,7 +3,7 @@
"id": "irisworldgen",
"version": "${version}",
"name": "Iris",
"description": "Iris World Generation Engine (Fabric adapter - packaging + core bundle; worldgen wiring lands next milestone)",
"description": "Iris World Generation Engine (Fabric adapter - native chunk generator, overworld override datapack, engine lifecycle)",
"authors": ["Arcane Arts (Volmit Software)"],
"contact": {
"sources": "https://github.com/VolmitSoftware/Iris"
@@ -15,6 +15,9 @@
},
"depends": {
"fabricloader": ">=0.19.0",
"fabric-registry-sync-v0": "*",
"fabric-resource-loader-v1": "*",
"fabric-lifecycle-events-v1": "*",
"minecraft": "~${minecraftVersion}",
"java": ">=25"
}
@@ -339,9 +339,13 @@ final class DecoratorCore {
static PlatformBlockState fixFacesForHunk(PlatformBlockState b, Hunk<PlatformBlockState> hunk, int rX, int rZ,
int x, int y, int z, EngineMantle mantle) {
if (!BUKKIT_PRESENT || !B.isVineBlock(b)) {
if (!B.isVineBlock(b)) {
return b;
}
if (!BUKKIT_PRESENT) {
DecoratorPlatformHooks.FaceFixer fixer = DecoratorPlatformHooks.faceFixer();
return fixer == null ? b : fixer.fixFaces(b, hunk, rX, rZ, x, y, z, mantle);
}
BlockData rawB = (BlockData) b.nativeHandle();
BlockData cloned = rawB.clone();
MultipleFacing data = (MultipleFacing) cloned;
@@ -392,7 +396,8 @@ final class DecoratorCore {
static boolean canGoOn(PlatformBlockState decorator, PlatformBlockState surface) {
if (!BUKKIT_PRESENT) {
return B.isSolid(surface);
DecoratorPlatformHooks.SurfaceSturdiness sturdiness = DecoratorPlatformHooks.surfaceSturdiness();
return sturdiness == null ? B.isSolid(surface) : sturdiness.canGoOn(surface);
}
return ((BlockData) surface.nativeHandle()).isFaceSturdy(BlockFace.UP, BlockSupport.FULL);
}
@@ -0,0 +1,52 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.decorator;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.hunk.Hunk;
public final class DecoratorPlatformHooks {
private static volatile FaceFixer FACE_FIXER = null;
private static volatile SurfaceSturdiness SURFACE_STURDINESS = null;
private DecoratorPlatformHooks() {
}
public interface FaceFixer {
PlatformBlockState fixFaces(PlatformBlockState state, Hunk<PlatformBlockState> hunk, int rX, int rZ, int x, int y, int z, EngineMantle mantle);
}
public interface SurfaceSturdiness {
boolean canGoOn(PlatformBlockState surface);
}
public static void bind(FaceFixer faceFixer, SurfaceSturdiness surfaceSturdiness) {
FACE_FIXER = faceFixer;
SURFACE_STURDINESS = surfaceSturdiness;
}
static FaceFixer faceFixer() {
return FACE_FIXER;
}
static SurfaceSturdiness surfaceSturdiness() {
return SURFACE_STURDINESS;
}
}
@@ -137,9 +137,9 @@ public final class StructureAssembler {
cb.getPosition().getY() - center.getY(),
cb.getPosition().getZ() - center.getZ());
IrisPosition rcr = rot.rotate(cr, 0, 0, 0);
int wcx = c.wx + c.facing.toVector().getBlockX();
int wcy = c.wy + c.facing.toVector().getBlockY();
int wcz = c.wz + c.facing.toVector().getBlockZ();
int wcx = c.wx + c.facing.x();
int wcy = c.wy + c.facing.y();
int wcz = c.wz + c.facing.z();
int px = wcx - rcr.getX();
int py = wcy - rcr.getY();
int pz = wcz - rcr.getZ();
@@ -7,12 +7,21 @@ import org.bukkit.block.data.BlockData;
import java.util.function.Function;
final class BlockDataMergeSupport {
public final class BlockDataMergeSupport {
private static final boolean BUKKIT_PRESENT = detectBukkit();
private static volatile StateMerger FALLBACK_MERGER = null;
private BlockDataMergeSupport() {
}
public interface StateMerger {
PlatformBlockState merge(PlatformBlockState base, PlatformBlockState update);
}
public static void bindFallbackMerger(StateMerger merger) {
FALLBACK_MERGER = merger;
}
private static boolean detectBukkit() {
try {
Class.forName("org.bukkit.Bukkit", false, BlockDataMergeSupport.class.getClassLoader());
@@ -24,7 +33,8 @@ final class BlockDataMergeSupport {
static PlatformBlockState merge(PlatformBlockState base, PlatformBlockState update) {
if (!BUKKIT_PRESENT) {
return mergeByKey(base, update);
StateMerger merger = FALLBACK_MERGER;
return merger == null ? mergeByKey(base, update) : merger.merge(base, update);
}
BlockData merged = merge((BlockData) base.nativeHandle(), (BlockData) update.nativeHandle(), BukkitBlockResolution::get);
return merged == null ? null : BukkitBlockState.of(merged);
@@ -26,7 +26,6 @@ import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.object.annotations.*;
import art.arcane.iris.platform.bukkit.BukkitBlockState;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.collection.KList;
@@ -39,8 +38,6 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.bukkit.Material;
import org.bukkit.block.data.BlockData;
import java.util.Map;
@@ -201,8 +198,11 @@ public class IrisBlockData extends IrisRegistrant {
public TileData tryGetTile(IrisData data) {
//TODO Do like a registry thing with the tile data registry. Also update the parsing of data to include **block** entities.
Material type = ((BlockData) getBlockData(data).nativeHandle()).getMaterial();
if (type == Material.SPAWNER && this.data.containsKey("entitySpawn")) {
PlatformBlockState state = getBlockData(data);
String stateKey = state.key();
int bracket = stateKey.indexOf('[');
String blockKey = bracket >= 0 ? stateKey.substring(0, bracket) : stateKey;
if (blockKey.equals("minecraft:spawner") && this.data.containsKey("entitySpawn")) {
String id = (String) this.data.get("entitySpawn");
if (tileData == null) tileData = new KMap<>();
KMap<String, Object> spawnData = (KMap<String, Object>) tileData.computeIfAbsent("SpawnData", k -> new KMap<>());
@@ -210,9 +210,9 @@ public class IrisBlockData extends IrisRegistrant {
entity.putIfAbsent("id", Identifier.fromString(id).toString());
}
if (!BukkitPlatform.hasTile(type) || tileData == null || tileData.isEmpty())
if (tileData == null || tileData.isEmpty() || !state.hasTileEntity())
return null;
return new TileData(type, this.tileData);
return TileData.of(state, this.tileData);
}
private String keyify(String dat) {
@@ -48,6 +48,15 @@ import java.util.Map;
@Data
public class IrisObjectRotation {
private static final boolean BUKKIT_PRESENT = detectBukkit();
private static volatile StateRotator FALLBACK_ROTATOR = null;
public interface StateRotator {
PlatformBlockState rotate(IrisObjectRotation rotation, PlatformBlockState state, int spinx, int spiny, int spinz);
}
public static void bindFallbackRotator(StateRotator rotator) {
FALLBACK_ROTATOR = rotator;
}
private static boolean detectBukkit() {
try {
@@ -278,7 +287,8 @@ public class IrisObjectRotation {
}
if (!BUKKIT_PRESENT) {
return state;
StateRotator rotator = FALLBACK_ROTATOR;
return rotator == null ? state : rotator.rotate(this, state, spinx, spiny, spinz);
}
BlockData raw = ((BlockData) state.nativeHandle()).clone();
@@ -19,6 +19,7 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.Strictness;
@@ -43,6 +44,25 @@ import java.io.IOException;
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class TileData implements Cloneable {
private static final Gson gson = new GsonBuilder().disableHtmlEscaping().setStrictness(Strictness.LENIENT).create();
private static final boolean BUKKIT_PRESENT = detectBukkit();
private static volatile TileReader FALLBACK_READER = null;
public interface TileReader {
TileData read(DataInputStream in) throws IOException;
}
public static void bindFallbackReader(TileReader reader) {
FALLBACK_READER = reader;
}
private static boolean detectBukkit() {
try {
Class.forName("org.bukkit.Bukkit", false, TileData.class.getClassLoader());
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
@NonNull
private Material material;
@@ -67,7 +87,24 @@ public class TileData implements Cloneable {
return new TileData().fromBukkit(block);
}
public static TileData of(PlatformBlockState state, KMap<String, Object> properties) {
if (!BUKKIT_PRESENT) {
return null;
}
Object handle = state.nativeHandle();
if (!(handle instanceof BlockData blockData)) {
return null;
}
return new TileData(blockData.getMaterial(), properties);
}
public static TileData read(DataInputStream in) throws IOException {
if (!BUKKIT_PRESENT) {
TileReader reader = FALLBACK_READER;
if (reader != null) {
return reader.read(in);
}
}
if (!in.markSupported())
throw new IOException("Mark not supported");
in.mark(Integer.MAX_VALUE);