mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 14:00:26 +00:00
Fabric Parity Worldgen
This commit is contained in:
1 parent
26d9115904
commit
2bac948edb
34 files changed
+3915
-34
No files matched your search
@@ -18,3 +18,4 @@ TreeGenStuff/
|
|||||||
dist/
|
dist/
|
||||||
core/plugins/
|
core/plugins/
|
||||||
adapters/bukkit/plugin/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://repo.codemc.org/repository/maven-public/') }
|
||||||
maven { url = uri('https://jitpack.io') }
|
maven { url = uri('https://jitpack.io') }
|
||||||
|
maven { url = uri('https://hub.spigotmc.org/nexus/content/repositories/snapshots/') }
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
@@ -56,6 +57,10 @@ configurations {
|
|||||||
canBeConsumed = false
|
canBeConsumed = false
|
||||||
canBeResolved = true
|
canBeResolved = true
|
||||||
}
|
}
|
||||||
|
devBundle {
|
||||||
|
canBeConsumed = false
|
||||||
|
canBeResolved = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations.named('bundle').configure {
|
configurations.named('bundle').configure {
|
||||||
@@ -71,28 +76,43 @@ configurations.named('bundle').configure {
|
|||||||
exclude(group: 'io.github.slimjar')
|
exclude(group: 'io.github.slimjar')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configurations.compileClasspath.extendsFrom(configurations.devBundle)
|
||||||
|
configurations.runtimeClasspath.extendsFrom(configurations.devBundle)
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
minecraft("com.mojang:minecraft:${minecraftVersion}")
|
minecraft("com.mojang:minecraft:${minecraftVersion}")
|
||||||
implementation("net.fabricmc:fabric-loader:${fabricLoaderVersion}")
|
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('org.slf4j:slf4j-api:2.0.17')
|
||||||
|
compileOnly(libs.spigot) {
|
||||||
|
transitive = false
|
||||||
|
}
|
||||||
|
|
||||||
bundle("art.arcane:core:${irisVersion}")
|
List<Object> shared = [
|
||||||
bundle("art.arcane:spi:${irisVersion}")
|
"art.arcane:core:${irisVersion}".toString(),
|
||||||
bundle('com.github.VolmitSoftware:VolmLib:master-SNAPSHOT')
|
"art.arcane:spi:${irisVersion}".toString(),
|
||||||
|
'com.github.VolmitSoftware:VolmLib:master-SNAPSHOT',
|
||||||
bundle(libs.paralithic)
|
libs.paralithic,
|
||||||
bundle(libs.lru)
|
libs.lru,
|
||||||
bundle(libs.kotlin.stdlib)
|
libs.kotlin.stdlib,
|
||||||
bundle(libs.kotlin.coroutines)
|
libs.kotlin.coroutines,
|
||||||
bundle(libs.commons.lang)
|
libs.commons.lang,
|
||||||
bundle(libs.commons.math3)
|
libs.commons.math3,
|
||||||
bundle(libs.caffeine)
|
libs.caffeine,
|
||||||
bundle(libs.lz4)
|
libs.lz4,
|
||||||
bundle(libs.zip)
|
libs.zip,
|
||||||
bundle(libs.sentry)
|
libs.sentry,
|
||||||
bundle(libs.oshi)
|
libs.oshi,
|
||||||
bundle(libs.byteBuddy.core)
|
libs.byteBuddy.core,
|
||||||
bundle(libs.byteBuddy.agent)
|
libs.byteBuddy.agent
|
||||||
|
]
|
||||||
|
shared.each { Object notation ->
|
||||||
|
add('bundle', notation)
|
||||||
|
add('devBundle', notation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named('compileJava', JavaCompile).configure {
|
tasks.named('compileJava', JavaCompile).configure {
|
||||||
@@ -100,6 +120,31 @@ tasks.named('compileJava', JavaCompile).configure {
|
|||||||
options.release.set(25)
|
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 {
|
processResources {
|
||||||
inputs.property('version', project.version)
|
inputs.property('version', project.version)
|
||||||
inputs.property('minecraftVersion', minecraftVersion)
|
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;
|
package art.arcane.iris.fabric;
|
||||||
|
|
||||||
import net.fabricmc.api.ModInitializer;
|
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.FabricLoader;
|
||||||
import net.fabricmc.loader.api.ModContainer;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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 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",
|
"id": "irisworldgen",
|
||||||
"version": "${version}",
|
"version": "${version}",
|
||||||
"name": "Iris",
|
"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)"],
|
"authors": ["Arcane Arts (Volmit Software)"],
|
||||||
"contact": {
|
"contact": {
|
||||||
"sources": "https://github.com/VolmitSoftware/Iris"
|
"sources": "https://github.com/VolmitSoftware/Iris"
|
||||||
@@ -15,6 +15,9 @@
|
|||||||
},
|
},
|
||||||
"depends": {
|
"depends": {
|
||||||
"fabricloader": ">=0.19.0",
|
"fabricloader": ">=0.19.0",
|
||||||
|
"fabric-registry-sync-v0": "*",
|
||||||
|
"fabric-resource-loader-v1": "*",
|
||||||
|
"fabric-lifecycle-events-v1": "*",
|
||||||
"minecraft": "~${minecraftVersion}",
|
"minecraft": "~${minecraftVersion}",
|
||||||
"java": ">=25"
|
"java": ">=25"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -339,9 +339,13 @@ final class DecoratorCore {
|
|||||||
|
|
||||||
static PlatformBlockState fixFacesForHunk(PlatformBlockState b, Hunk<PlatformBlockState> hunk, int rX, int rZ,
|
static PlatformBlockState fixFacesForHunk(PlatformBlockState b, Hunk<PlatformBlockState> hunk, int rX, int rZ,
|
||||||
int x, int y, int z, EngineMantle mantle) {
|
int x, int y, int z, EngineMantle mantle) {
|
||||||
if (!BUKKIT_PRESENT || !B.isVineBlock(b)) {
|
if (!B.isVineBlock(b)) {
|
||||||
return 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 rawB = (BlockData) b.nativeHandle();
|
||||||
BlockData cloned = rawB.clone();
|
BlockData cloned = rawB.clone();
|
||||||
MultipleFacing data = (MultipleFacing) cloned;
|
MultipleFacing data = (MultipleFacing) cloned;
|
||||||
@@ -392,7 +396,8 @@ final class DecoratorCore {
|
|||||||
|
|
||||||
static boolean canGoOn(PlatformBlockState decorator, PlatformBlockState surface) {
|
static boolean canGoOn(PlatformBlockState decorator, PlatformBlockState surface) {
|
||||||
if (!BUKKIT_PRESENT) {
|
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);
|
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().getY() - center.getY(),
|
||||||
cb.getPosition().getZ() - center.getZ());
|
cb.getPosition().getZ() - center.getZ());
|
||||||
IrisPosition rcr = rot.rotate(cr, 0, 0, 0);
|
IrisPosition rcr = rot.rotate(cr, 0, 0, 0);
|
||||||
int wcx = c.wx + c.facing.toVector().getBlockX();
|
int wcx = c.wx + c.facing.x();
|
||||||
int wcy = c.wy + c.facing.toVector().getBlockY();
|
int wcy = c.wy + c.facing.y();
|
||||||
int wcz = c.wz + c.facing.toVector().getBlockZ();
|
int wcz = c.wz + c.facing.z();
|
||||||
int px = wcx - rcr.getX();
|
int px = wcx - rcr.getX();
|
||||||
int py = wcy - rcr.getY();
|
int py = wcy - rcr.getY();
|
||||||
int pz = wcz - rcr.getZ();
|
int pz = wcz - rcr.getZ();
|
||||||
|
|||||||
@@ -7,12 +7,21 @@ import org.bukkit.block.data.BlockData;
|
|||||||
|
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
final class BlockDataMergeSupport {
|
public final class BlockDataMergeSupport {
|
||||||
private static final boolean BUKKIT_PRESENT = detectBukkit();
|
private static final boolean BUKKIT_PRESENT = detectBukkit();
|
||||||
|
private static volatile StateMerger FALLBACK_MERGER = null;
|
||||||
|
|
||||||
private BlockDataMergeSupport() {
|
private BlockDataMergeSupport() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface StateMerger {
|
||||||
|
PlatformBlockState merge(PlatformBlockState base, PlatformBlockState update);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void bindFallbackMerger(StateMerger merger) {
|
||||||
|
FALLBACK_MERGER = merger;
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean detectBukkit() {
|
private static boolean detectBukkit() {
|
||||||
try {
|
try {
|
||||||
Class.forName("org.bukkit.Bukkit", false, BlockDataMergeSupport.class.getClassLoader());
|
Class.forName("org.bukkit.Bukkit", false, BlockDataMergeSupport.class.getClassLoader());
|
||||||
@@ -24,7 +33,8 @@ final class BlockDataMergeSupport {
|
|||||||
|
|
||||||
static PlatformBlockState merge(PlatformBlockState base, PlatformBlockState update) {
|
static PlatformBlockState merge(PlatformBlockState base, PlatformBlockState update) {
|
||||||
if (!BUKKIT_PRESENT) {
|
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);
|
BlockData merged = merge((BlockData) base.nativeHandle(), (BlockData) update.nativeHandle(), BukkitBlockResolution::get);
|
||||||
return merged == null ? null : BukkitBlockState.of(merged);
|
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.data.cache.AtomicCache;
|
||||||
import art.arcane.iris.engine.object.annotations.*;
|
import art.arcane.iris.engine.object.annotations.*;
|
||||||
import art.arcane.iris.platform.bukkit.BukkitBlockState;
|
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.IrisLogging;
|
||||||
import art.arcane.iris.spi.PlatformBlockState;
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
@@ -39,8 +38,6 @@ import lombok.Data;
|
|||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
import org.bukkit.Material;
|
|
||||||
import org.bukkit.block.data.BlockData;
|
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -201,8 +198,11 @@ public class IrisBlockData extends IrisRegistrant {
|
|||||||
|
|
||||||
public TileData tryGetTile(IrisData data) {
|
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.
|
//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();
|
PlatformBlockState state = getBlockData(data);
|
||||||
if (type == Material.SPAWNER && this.data.containsKey("entitySpawn")) {
|
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");
|
String id = (String) this.data.get("entitySpawn");
|
||||||
if (tileData == null) tileData = new KMap<>();
|
if (tileData == null) tileData = new KMap<>();
|
||||||
KMap<String, Object> spawnData = (KMap<String, Object>) tileData.computeIfAbsent("SpawnData", k -> 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());
|
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 null;
|
||||||
return new TileData(type, this.tileData);
|
return TileData.of(state, this.tileData);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String keyify(String dat) {
|
private String keyify(String dat) {
|
||||||
|
|||||||
@@ -48,6 +48,15 @@ import java.util.Map;
|
|||||||
@Data
|
@Data
|
||||||
public class IrisObjectRotation {
|
public class IrisObjectRotation {
|
||||||
private static final boolean BUKKIT_PRESENT = detectBukkit();
|
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() {
|
private static boolean detectBukkit() {
|
||||||
try {
|
try {
|
||||||
@@ -278,7 +287,8 @@ public class IrisObjectRotation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!BUKKIT_PRESENT) {
|
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();
|
BlockData raw = ((BlockData) state.nativeHandle()).clone();
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
package art.arcane.iris.engine.object;
|
package art.arcane.iris.engine.object;
|
||||||
|
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.GsonBuilder;
|
import com.google.gson.GsonBuilder;
|
||||||
import com.google.gson.Strictness;
|
import com.google.gson.Strictness;
|
||||||
@@ -43,6 +44,25 @@ import java.io.IOException;
|
|||||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
public class TileData implements Cloneable {
|
public class TileData implements Cloneable {
|
||||||
private static final Gson gson = new GsonBuilder().disableHtmlEscaping().setStrictness(Strictness.LENIENT).create();
|
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
|
@NonNull
|
||||||
private Material material;
|
private Material material;
|
||||||
@@ -67,7 +87,24 @@ public class TileData implements Cloneable {
|
|||||||
return new TileData().fromBukkit(block);
|
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 {
|
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())
|
if (!in.markSupported())
|
||||||
throw new IOException("Mark not supported");
|
throw new IOException("Mark not supported");
|
||||||
in.mark(Integer.MAX_VALUE);
|
in.mark(Integer.MAX_VALUE);
|
||||||
|
|||||||
Reference in new issue
Block a user