mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-07-13 02:15:46 +00:00
Fabric Parity Worldgen
This commit is contained in:
@@ -18,3 +18,4 @@ TreeGenStuff/
|
||||
dist/
|
||||
core/plugins/
|
||||
adapters/bukkit/plugin/plugins/
|
||||
adapters/fabric/run/
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user