diff --git a/build.gradle b/build.gradle index e3ccd99e3..1e28c8274 100644 --- a/build.gradle +++ b/build.gradle @@ -68,6 +68,7 @@ registerCustomOutputTaskUnix('CrazyDev22LT', '/home/julian/Desktop/server/plugin def nmsBindings = [ v1_21_R7: '1.21.11-R0.1-SNAPSHOT', + v26_1_R1: '26.1.2.build.66-stable', ] Class nmsTypeClass = Class.forName('NMSBinding$Type') nmsBindings.each { key, value -> diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index cdbf689d8..45099a76e 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -21,5 +21,5 @@ repositories { dependencies { implementation('org.ow2.asm:asm:9.8') implementation('com.github.VolmitSoftware:NMSTools:c88961416f') - implementation('io.papermc.paperweight:paperweight-userdev:2.0.0-beta.18') + implementation('io.papermc.paperweight:paperweight-userdev:2.0.0-beta.21') } diff --git a/buildSrc/src/main/java/NMSBinding.java b/buildSrc/src/main/java/NMSBinding.java index 19ca21a39..588b759c4 100644 --- a/buildSrc/src/main/java/NMSBinding.java +++ b/buildSrc/src/main/java/NMSBinding.java @@ -139,7 +139,10 @@ public class NMSBinding implements Plugin { } String[] parts = trimmed.split("\\."); - return new int[]{Integer.parseInt(parts[1]), Integer.parseInt(parts[2])}; + if ("1".equals(parts[0])) { + return new int[]{Integer.parseInt(parts[1]), Integer.parseInt(parts[2])}; + } + return new int[]{Integer.parseInt(parts[0]), Integer.parseInt(parts[1])}; } private static T named(ObjectFactory objects, Class type, String name) { diff --git a/core/build.gradle b/core/build.gradle index 4c71a5619..038122f80 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -39,7 +39,7 @@ plugins { alias(libs.plugins.kotlin.lombok) } -def apiVersion = '1.21' +def apiVersion = '26.1' def mainClass = 'art.arcane.iris.Iris' def lib = 'art.arcane.iris.util' String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate') diff --git a/core/plugins/Iris/cache/instance b/core/plugins/Iris/cache/instance index f9dff3d09..b9b283d16 100644 --- a/core/plugins/Iris/cache/instance +++ b/core/plugins/Iris/cache/instance @@ -1 +1 @@ -1435163759 \ No newline at end of file +446774718 \ No newline at end of file diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandIris.java b/core/src/main/java/art/arcane/iris/core/commands/CommandIris.java index 5b604486c..45e579227 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandIris.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandIris.java @@ -97,6 +97,7 @@ public class CommandIris implements DirectorExecutor { private CommandPregen pregen; private CommandSettings settings; private CommandObject object; + private CommandStructure structure; private CommandWhat what; private CommandEdit edit; private CommandDeveloper developer; @@ -592,11 +593,8 @@ public class CommandIris implements DirectorExecutor { } @Director(description = "Toggle debug") - public void debug( - @Param(name = "on", description = "Whether or not debug should be on", defaultValue = "other") - Boolean on - ) { - boolean to = on == null ? !IrisSettings.get().getGeneral().isDebug() : on; + public void debug() { + boolean to = !IrisSettings.get().getGeneral().isDebug(); IrisSettings.get().getGeneral().setDebug(to); IrisSettings.get().forceSave(); sender().sendMessage(C.GREEN + "Set debug to: " + to); diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandStructure.java b/core/src/main/java/art/arcane/iris/core/commands/CommandStructure.java new file mode 100644 index 000000000..42acea60f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandStructure.java @@ -0,0 +1,273 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 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 . + */ + +package art.arcane.iris.core.commands; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.structure.StructureImporter; +import art.arcane.iris.core.structure.StructureIndexService; +import art.arcane.iris.core.structure.VillageImporter; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.PlacedStructurePiece; +import art.arcane.iris.engine.framework.StructureAssembler; +import art.arcane.iris.engine.framework.StructurePlacementGrid; +import art.arcane.iris.engine.object.IObjectPlacer; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.object.IrisObjectPlacement; +import art.arcane.iris.engine.object.IrisStructure; +import art.arcane.iris.engine.object.IrisStructurePlacement; +import art.arcane.iris.engine.object.ObjectPlaceMode; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.iris.util.common.director.DirectorExecutor; +import art.arcane.iris.util.common.director.DirectorHelp; +import art.arcane.iris.util.common.format.C; +import art.arcane.iris.engine.data.cache.Cache; +import art.arcane.volmlib.util.director.DirectorOrigin; +import art.arcane.volmlib.util.director.annotations.Director; +import art.arcane.volmlib.util.director.annotations.Param; +import art.arcane.volmlib.util.math.RNG; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +@Director(name = "structure", aliases = {"struct", "str"}, studio = true, description = "Iris structure tools (index, import, info)") +public class CommandStructure implements DirectorExecutor { + @Director(description = "Show help tree for this command group", aliases = {"?"}) + public void help() { + DirectorHelp.print(sender(), getClass()); + } + + @Director(description = "Regenerate structure-index.json listing all vanilla, datapack & iris structures", aliases = {"ls", "index"}, origin = DirectorOrigin.BOTH) + public void list( + @Param(description = "The dimension whose pack to index", aliases = "dim") + IrisDimension dimension + ) { + IrisData data = dimension.getLoader(); + if (data == null) { + sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey()); + return; + } + File file = StructureIndexService.write(data); + sender().sendMessage(C.GREEN + "Wrote structure index: " + C.WHITE + file.getPath()); + } + + @Director(description = "Import a vanilla/datapack structure NBT into a pack as editable Iris resources", aliases = {"imp"}, origin = DirectorOrigin.BOTH) + public void importStructure( + @Param(description = "The dimension whose pack to import into", aliases = "dim") + IrisDimension dimension, + @Param(description = "Structure key to import, e.g. minecraft:igloo/top or nova_structures:desert_temple") + String key, + @Param(description = "Name for the imported structure (defaults from the key)", defaultValue = "") + String name, + @Param(description = "overwrite | add-only | merge", defaultValue = "overwrite") + String mode + ) { + IrisData data = dimension.getLoader(); + if (data == null) { + sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey()); + return; + } + NamespacedKey nk = NamespacedKey.fromString(key.toLowerCase()); + if (nk == null) { + sender().sendMessage(C.RED + "Invalid structure key: " + key); + return; + } + String n = name == null || name.isEmpty() ? StructureImporter.deriveName(nk) : name; + StructureImporter.Result result = StructureImporter.importStructure(data, nk, n, StructureImporter.parseMode(mode)); + sender().sendMessage((result.success() ? C.GREEN : C.RED) + result.message()); + if (result.success()) { + sender().sendMessage(C.GRAY + "Reference it from a biome/region/dimension structures list as '" + n + "'."); + } + } + + @Director(description = "Import a full vanilla/datapack JIGSAW structure (e.g. a village) by reconstructing its template-pool graph into editable Iris pools, pieces & connectors", aliases = {"iv", "village"}, origin = DirectorOrigin.BOTH) + public void importVillage( + @Param(description = "The dimension whose pack to import into", aliases = "dim") + IrisDimension dimension, + @Param(description = "Jigsaw structure key to import, e.g. minecraft:village_plains") + String key, + @Param(description = "Name for the imported structure (defaults from the key)", defaultValue = "") + String name, + @Param(description = "overwrite | add-only | merge", defaultValue = "overwrite") + String mode + ) { + IrisData data = dimension.getLoader(); + if (data == null) { + sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey()); + return; + } + NamespacedKey nk = NamespacedKey.fromString(key.toLowerCase()); + if (nk == null) { + sender().sendMessage(C.RED + "Invalid structure key: " + key); + return; + } + String n = name == null || name.isEmpty() ? StructureImporter.deriveName(nk) : name; + VillageImporter.Result result = VillageImporter.importVillage(data, nk, n, StructureImporter.parseMode(mode)); + sender().sendMessage((result.success() ? C.GREEN : C.RED) + result.message()); + if (result.success()) { + sender().sendMessage(C.GRAY + "Reference it from a biome/region/dimension structures list as '" + n + "'."); + sender().sendMessage(C.GRAY + "Inspect the rebuilt jigsaw graph with: /iris structure info " + dimension.getLoadKey() + " " + n); + } + } + + @Director(description = "Resolve an iris structure's jigsaw graph and report piece count & bounds", origin = DirectorOrigin.BOTH) + public void info( + @Param(description = "The dimension whose pack holds the structure", aliases = "dim") + IrisDimension dimension, + @Param(description = "The iris structure key to inspect") + String structure + ) { + IrisData data = dimension.getLoader(); + if (data == null) { + sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey()); + return; + } + IrisStructure s = IrisData.loadAnyStructure(structure, data); + if (s == null) { + sender().sendMessage(C.RED + "No iris structure '" + structure + "' in this pack"); + return; + } + StructureAssembler assembler = new StructureAssembler(data, s, 0, 64, 0); + KList pieces = assembler.assemble(new RNG(1234)); + if (pieces == null || pieces.isEmpty()) { + sender().sendMessage(C.RED + "Structure '" + structure + "' assembled 0 pieces (check startPool '" + s.getStartPool() + "')"); + return; + } + int minX = Integer.MAX_VALUE; + int minZ = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxZ = Integer.MIN_VALUE; + for (PlacedStructurePiece p : pieces) { + minX = Math.min(minX, p.getMinX()); + minZ = Math.min(minZ, p.getMinZ()); + maxX = Math.max(maxX, p.getMaxX()); + maxZ = Math.max(maxZ, p.getMaxZ()); + } + sender().sendMessage(C.GREEN + "Structure '" + structure + "': " + C.WHITE + pieces.size() + C.GREEN + " pieces, footprint " + C.WHITE + (maxX - minX + 1) + "x" + (maxZ - minZ + 1) + C.GREEN + " blocks (sample seed 1234)"); + } + + @Director(description = "Assemble and place an iris structure at your location (studio testing)", aliases = {"p"}, origin = DirectorOrigin.PLAYER, sync = true) + public void place( + @Param(description = "The dimension whose pack holds the structure", aliases = "dim") + IrisDimension dimension, + @Param(description = "The iris structure key to place") + String structure + ) { + IrisData data = dimension.getLoader(); + if (data == null) { + sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey()); + return; + } + IrisStructure s = IrisData.loadAnyStructure(structure, data); + if (s == null) { + sender().sendMessage(C.RED + "No iris structure '" + structure + "' in this pack"); + return; + } + Location loc = player().getLocation(); + StructureAssembler assembler = new StructureAssembler(data, s, loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()); + RNG rng = new RNG((long) loc.getBlockX() * 341873128712L + loc.getBlockZ()); + KList pieces = assembler.assemble(rng); + if (pieces == null || pieces.isEmpty()) { + sender().sendMessage(C.RED + "Structure '" + structure + "' assembled 0 pieces"); + return; + } + Map future = new HashMap<>(); + IObjectPlacer placer = CommandObject.createPlacer(player().getWorld(), future); + for (PlacedStructurePiece p : pieces) { + IrisObjectPlacement config = new IrisObjectPlacement(); + config.setMode(ObjectPlaceMode.STRUCTURE_PIECE); + config.setRotation(p.getRotation()); + config.getPlace().add(p.getObject().getLoadKey()); + if (!s.getEdit().isEmpty()) { + config.setEdit(s.getEdit()); + } + p.getObject().place(p.getX(), p.getY(), p.getZ(), placer, config, rng, null, null, data); + } + sender().sendMessage(C.GREEN + "Placed '" + structure + "' (" + pieces.size() + " pieces) at your location."); + } + + @Director(description = "Find the nearest placement of an iris structure around you", aliases = {"l"}, origin = DirectorOrigin.PLAYER) + public void locate( + @Param(description = "The iris structure key to find") + String structure, + @Param(description = "Search radius in chunks", defaultValue = "96") + int radius + ) { + World world = player().getWorld(); + if (!IrisToolbelt.isIrisWorld(world)) { + sender().sendMessage(C.RED + "You must be in an Iris world"); + return; + } + Engine engine = IrisToolbelt.access(world).getEngine(); + IrisData data = engine.getData(); + long seed = engine.getSeedManager().getMantle(); + int pcx = player().getLocation().getBlockX() >> 4; + int pcz = player().getLocation().getBlockZ() >> 4; + int max = Math.max(1, Math.min(radius, 512)); + for (int r = 0; r <= max; r++) { + for (int dx = -r; dx <= r; dx++) { + for (int dz = -r; dz <= r; dz++) { + if (Math.max(Math.abs(dx), Math.abs(dz)) != r) { + continue; + } + int cx = pcx + dx; + int cz = pcz + dz; + if (chunkStarts(engine, seed, structure, cx, cz)) { + sender().sendMessage(C.GREEN + "Nearest '" + structure + "' starts near chunk " + cx + ", " + cz + " (block " + (cx << 4) + ", " + (cz << 4) + ")"); + return; + } + } + } + } + sender().sendMessage(C.YELLOW + "No '" + structure + "' placement found within " + max + " chunks."); + } + + private boolean chunkStarts(Engine engine, long seed, String structure, int cx, int cz) { + int bx = 8 + (cx << 4); + int bz = 8 + (cz << 4); + IrisBiome biome = engine.getComplex().getTrueBiomeStream().get(bx, bz); + IrisRegion region = engine.getComplex().getRegionStream().get(bx, bz); + KList placements = new KList<>(); + if (biome != null) { + placements.addAll(biome.getStructures()); + } + if (region != null) { + placements.addAll(region.getStructures()); + } + placements.addAll(engine.getDimension().getStructures()); + RNG rng = new RNG(Cache.key(cx, cz) + seed); + for (IrisStructurePlacement placement : placements) { + if (!placement.getStructures().contains(structure)) { + continue; + } + if (StructurePlacementGrid.startsInChunk(placement, cx, cz, seed, rng)) { + return true; + } + } + return false; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandStudio.java b/core/src/main/java/art/arcane/iris/core/commands/CommandStudio.java index 41f284ba9..14af8b6fa 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandStudio.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandStudio.java @@ -294,12 +294,6 @@ public class CommandStudio implements DirectorExecutor { }); } - @Director(description = "Get all structures in a radius of chunks", aliases = "dist", origin = DirectorOrigin.PLAYER) - public void distances(@Param(description = "The radius in chunks") int radius) { - sender().sendMessage(C.YELLOW + "Structure distance sampling for legacy structure data has been removed."); - } - - @Director(description = "Render a world map (External GUI)", aliases = "render") public void map( @Param(name = "world", description = "The world to open the generator for", contextual = true) @@ -616,7 +610,7 @@ public class CommandStudio implements DirectorExecutor { } } - @Director(aliases = "find-objects", description = "Get information about nearby structures") + @Director(aliases = "find-objects", description = "Capture an IGenData chunk report for nearby chunks") public void objects() { if (!IrisToolbelt.isIrisWorld(player().getWorld())) { sender().sendMessage(C.RED + "You must be in an Iris world"); diff --git a/core/src/main/java/art/arcane/iris/core/edit/DustRevealer.java b/core/src/main/java/art/arcane/iris/core/edit/DustRevealer.java index 1f2e19c6a..868489cb1 100644 --- a/core/src/main/java/art/arcane/iris/core/edit/DustRevealer.java +++ b/core/src/main/java/art/arcane/iris/core/edit/DustRevealer.java @@ -19,9 +19,13 @@ package art.arcane.iris.core.edit; import art.arcane.iris.Iris; +import art.arcane.iris.core.nms.INMS; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.iris.util.common.format.C; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.math.BlockPosition; import art.arcane.volmlib.util.math.M; @@ -32,7 +36,15 @@ import lombok.Data; import org.bukkit.Location; import org.bukkit.Sound; import org.bukkit.World; +import org.bukkit.block.Biome; import org.bukkit.block.Block; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Set; +import java.util.function.Supplier; @SuppressWarnings("ALL") @Data @@ -111,6 +123,8 @@ public class DustRevealer { Engine access = generator.getEngine(); if (access != null) { + describe(access, world, block, sender); + String a = access.getObjectPlacementKey(block.getX(), block.getY() - block.getWorld().getMinHeight(), block.getZ()); if (a != null) { world.playSound(block.getLocation(), Sound.ITEM_LODESTONE_COMPASS_LOCK, 1f, 0.1f); @@ -123,6 +137,144 @@ public class DustRevealer { } } + private static void describe(Engine engine, World world, Block block, VolmitSender sender) { + int x = block.getX(); + int y = block.getY(); + int z = block.getZ(); + int minHeight = world.getMinHeight(); + int relativeY = y - minHeight; + int surfaceRelative = engine.getHeight(x, z, true); + int surfaceY = surfaceRelative + minHeight; + int offset = y - surfaceY; + + String objectKey = safe(() -> engine.getObjectPlacementKey(x, relativeY, z)); + IrisBiome surfaceBiome = safe(() -> engine.getSurfaceBiome(x, z)); + IrisBiome biomeHere = safe(() -> engine.getBiome(x, relativeY, z)); + IrisBiome caveBiome = safe(() -> engine.getCaveOrMantleBiome(x, relativeY, z)); + IrisRegion region = safe(() -> engine.getRegion(x, z)); + + KList lines = new KList<>(); + lines.add("Iris Dust @ " + x + ", " + y + ", " + z); + lines.add("Block: " + block.getType().name()); + if (offset > 0) { + lines.add("Position: +" + offset + " ABOVE surface (surface Y=" + surfaceY + ")"); + } else if (offset < 0) { + lines.add("Position: " + (-offset) + " below surface (surface Y=" + surfaceY + ")"); + } else { + lines.add("Position: at surface (Y=" + surfaceY + ")"); + } + + String placedBy; + if (offset > 0) { + placedBy = objectKey != null ? "object/stilt '" + objectKey + "' (above surface)" : "decoration/object/stilt (above surface)"; + } else if (objectKey != null) { + placedBy = "buried object '" + objectKey + "'"; + } else { + placedBy = "terrain layer (depth " + Math.max(0, surfaceRelative - relativeY) + " below surface)"; + } + lines.add("Placed by: " + placedBy); + lines.add("Object @block: " + (objectKey == null ? "none" : objectKey)); + if (objectKey == null) { + String columnObject = findColumnObject(engine, x, relativeY, z, minHeight); + lines.add("Column object: " + (columnObject == null + ? "none within 64 (decorator or terrain, NOT an object stilt)" + : columnObject + " -> this block is likely that object's stilt")); + } + + if (surfaceBiome != null) { + lines.add("Surface biome: " + surfaceBiome.getLoadKey() + " (" + biomeKey(surfaceBiome.getDerivative()) + ")"); + } + if (biomeHere != null && (surfaceBiome == null || !biomeHere.getLoadKey().equals(surfaceBiome.getLoadKey()))) { + lines.add("Biome @Y: " + biomeHere.getLoadKey()); + } + if (caveBiome != null && (surfaceBiome == null || !caveBiome.getLoadKey().equals(surfaceBiome.getLoadKey()))) { + lines.add("Cave/Mantle biome: " + caveBiome.getLoadKey()); + } + + try { + lines.add("Server biome: " + INMS.get().getTrueBiomeBaseKey(block.getLocation()) + + " (ID: " + INMS.get().getTrueBiomeBaseId(INMS.get().getTrueBiomeBase(block.getLocation())) + ")"); + } catch (Throwable e) { + Iris.reportError(e); + } + + if (region != null) { + lines.add("Region: " + region.getLoadKey() + " (" + region.getName() + ")"); + } + + Set objects = safe(() -> engine.getObjectsAt(x >> 4, z >> 4)); + if (objects != null && !objects.isEmpty()) { + lines.add("Objects in chunk: " + objects); + } + + sender.sendMessage(C.IRIS + "--- " + lines.get(0) + " ---"); + for (int i = 1; i < lines.size(); i++) { + String line = lines.get(i); + String color = (line.startsWith("Position:") || line.startsWith("Placed by:")) ? C.YELLOW.toString() : C.WHITE.toString(); + sender.sendMessage(color + line); + } + sendCopyButton(sender, String.join("\n", lines)); + } + + private static String findColumnObject(Engine engine, int x, int relativeY, int z, int minHeight) { + for (int dy = 1; dy <= 64; dy++) { + if (relativeY + dy >= 0) { + try { + String up = engine.getObjectPlacementKey(x, relativeY + dy, z); + if (up != null) { + return up + " @Y=" + (relativeY + dy + minHeight) + " (above)"; + } + } catch (Throwable ignored) { + } + } + if (relativeY - dy >= 0) { + try { + String down = engine.getObjectPlacementKey(x, relativeY - dy, z); + if (down != null) { + return down + " @Y=" + (relativeY - dy + minHeight) + " (below)"; + } + } catch (Throwable ignored) { + } + } + } + return null; + } + + private static String biomeKey(Biome biome) { + if (biome == null) { + return "none"; + } + try { + return biome.getKey().getKey(); + } catch (Throwable ignored) { + return biome.toString(); + } + } + + private static T safe(Supplier supplier) { + try { + return supplier.get(); + } catch (Throwable e) { + Iris.reportError(e); + return null; + } + } + + private static void sendCopyButton(VolmitSender sender, String payload) { + if (!sender.isPlayer()) { + return; + } + try { + Component button = Component.text("[Click to copy these stats]") + .color(NamedTextColor.GREEN) + .clickEvent(ClickEvent.copyToClipboard(payload)) + .hoverEvent(HoverEvent.showText(Component.text("Copy block stats to clipboard"))); + Iris.audiences.player(sender.player()).sendMessage(button); + } catch (Throwable e) { + Iris.reportError(e); + } + } + private boolean is(BlockPosition a) { if (a.getY() < world.getMinHeight() || a.getY() >= world.getMaxHeight()) { return false; diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleStaging.java b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleStaging.java index 68c3f86da..36999fe32 100644 --- a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleStaging.java +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleStaging.java @@ -7,11 +7,13 @@ import org.jetbrains.annotations.Nullable; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; public final class WorldLifecycleStaging { private static final Map stagedGenerators = new ConcurrentHashMap<>(); private static final Map stagedBiomeProviders = new ConcurrentHashMap<>(); private static final Map stagedStemGenerators = new ConcurrentHashMap<>(); + private static final AtomicReference pendingStemGenerator = new AtomicReference<>(); private WorldLifecycleStaging() { } @@ -27,6 +29,7 @@ public final class WorldLifecycleStaging { public static void stageStemGenerator(@NotNull String worldName, @NotNull ChunkGenerator generator) { stagedStemGenerators.put(worldName, generator); + pendingStemGenerator.set(generator); } @Nullable @@ -41,7 +44,16 @@ public final class WorldLifecycleStaging { @Nullable public static ChunkGenerator consumeStemGenerator(@NotNull String worldName) { - return stagedStemGenerators.remove(worldName); + ChunkGenerator generator = stagedStemGenerators.remove(worldName); + if (generator != null) { + pendingStemGenerator.compareAndSet(generator, null); + return generator; + } + ChunkGenerator pending = pendingStemGenerator.getAndSet(null); + if (pending != null) { + stagedStemGenerators.values().remove(pending); + } + return pending; } public static void clearGenerator(@NotNull String worldName) { @@ -50,7 +62,10 @@ public final class WorldLifecycleStaging { } public static void clearStem(@NotNull String worldName) { - stagedStemGenerators.remove(worldName); + ChunkGenerator generator = stagedStemGenerators.remove(worldName); + if (generator != null) { + pendingStemGenerator.compareAndSet(generator, null); + } } public static void clearAll(@NotNull String worldName) { diff --git a/core/src/main/java/art/arcane/iris/core/loader/IrisData.java b/core/src/main/java/art/arcane/iris/core/loader/IrisData.java index d777d9429..d02f0998b 100644 --- a/core/src/main/java/art/arcane/iris/core/loader/IrisData.java +++ b/core/src/main/java/art/arcane/iris/core/loader/IrisData.java @@ -70,6 +70,9 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory { private ResourceLoader matterLoader; private ResourceLoader imageLoader; private ResourceLoader matterObjectLoader; + private ResourceLoader structureLoader; + private ResourceLoader jigsawPoolLoader; + private ResourceLoader jigsawPieceLoader; private KMap> possibleSnippets; private Gson gson; private Gson snippetLoader; @@ -167,6 +170,18 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory { return loadAny(IrisGenerator.class, key, nearest); } + public static IrisStructure loadAnyStructure(String key, @Nullable IrisData nearest) { + return loadAny(IrisStructure.class, key, nearest); + } + + public static IrisJigsawPool loadAnyJigsawPool(String key, @Nullable IrisData nearest) { + return loadAny(IrisJigsawPool.class, key, nearest); + } + + public static IrisJigsawPiece loadAnyJigsawPiece(String key, @Nullable IrisData nearest) { + return loadAny(IrisJigsawPiece.class, key, nearest); + } + public static T loadAny(Class type, String key, @Nullable IrisData nearest) { try { if (nearest != null) { @@ -297,6 +312,9 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory { this.objectLoader = registerLoader(IrisObject.class); this.imageLoader = registerLoader(IrisImage.class); this.matterObjectLoader = registerLoader(IrisMatterObject.class); + this.structureLoader = registerLoader(IrisStructure.class); + this.jigsawPoolLoader = registerLoader(IrisJigsawPool.class); + this.jigsawPieceLoader = registerLoader(IrisJigsawPiece.class); builder.registerTypeAdapterFactory(KeyedType::createTypeAdapter); gson = builder.create(); diff --git a/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java b/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java index a173c01e7..3ed4e2ee6 100644 --- a/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java +++ b/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java @@ -114,7 +114,7 @@ public class ResourceLoader implements MeteredCache { o.put("url", "./.iris/schema/" + getFolderName() + "-schema.json"); File a = new File(getManager().getDataFolder(), ".iris/schema/" + getFolderName() + "-schema.json"); String schemaPath = a.getAbsolutePath(); - if (!a.exists() && schemaBuildQueue.add(schemaPath)) { + if (schemaBuildQueue.add(schemaPath)) { schemaBuildExecutor.execute(() -> { try { IO.writeAll(a, new SchemaBuilder(objectClass, manager).construct().toString(4)); diff --git a/core/src/main/java/art/arcane/iris/core/nms/INMS.java b/core/src/main/java/art/arcane/iris/core/nms/INMS.java index 23248903f..eb4c10eb6 100644 --- a/core/src/main/java/art/arcane/iris/core/nms/INMS.java +++ b/core/src/main/java/art/arcane/iris/core/nms/INMS.java @@ -30,9 +30,10 @@ import java.util.Set; public class INMS { private static final Version CURRENT = Boolean.getBoolean("iris.no-version-limit") ? new Version(Integer.MAX_VALUE, Integer.MAX_VALUE, null) : - new Version(21, 11, null); + new Version(26, 1, null); private static final List REVISION = List.of( + new Version(26, 1, "v26_1_R1"), new Version(21, 11, "v1_21_R7") ); diff --git a/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java b/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java index 2289017c1..7b30c2c19 100644 --- a/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java +++ b/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java @@ -89,6 +89,14 @@ public interface INMSBinding { KList getBiomes(); + default KList getStructureKeys() { + return new KList<>(); + } + + default KList getStructureSetKeys() { + return new KList<>(); + } + boolean isBukkit(); int getBiomeId(Biome biome); diff --git a/core/src/main/java/art/arcane/iris/core/nms/MinecraftVersion.java b/core/src/main/java/art/arcane/iris/core/nms/MinecraftVersion.java index c6c326118..57c343ca5 100644 --- a/core/src/main/java/art/arcane/iris/core/nms/MinecraftVersion.java +++ b/core/src/main/java/art/arcane/iris/core/nms/MinecraftVersion.java @@ -79,13 +79,24 @@ final class MinecraftVersion { } String[] parts = input.split("\\."); - if (parts.length < 2 || !"1".equals(parts[0])) { + if (parts.length < 2 || parts.length > 3) { return null; } + for (String part : parts) { + if (part.isEmpty() || !part.chars().allMatch(Character::isDigit)) { + return null; + } + } try { - int major = Integer.parseInt(parts[1]); - int minor = parts.length > 2 ? Integer.parseInt(parts[2]) : 0; + if ("1".equals(parts[0])) { + int major = Integer.parseInt(parts[1]); + int minor = parts.length > 2 ? Integer.parseInt(parts[2]) : 0; + return new MinecraftVersion(input, major, minor); + } + + int major = Integer.parseInt(parts[0]); + int minor = Integer.parseInt(parts[1]); return new MinecraftVersion(input, major, minor); } catch (NumberFormatException ignored) { return null; diff --git a/core/src/main/java/art/arcane/iris/core/nms/datapack/DataVersion.java b/core/src/main/java/art/arcane/iris/core/nms/datapack/DataVersion.java index 8ad75c0f5..690b7d3ce 100644 --- a/core/src/main/java/art/arcane/iris/core/nms/datapack/DataVersion.java +++ b/core/src/main/java/art/arcane/iris/core/nms/datapack/DataVersion.java @@ -19,7 +19,8 @@ public enum DataVersion { V1_19_2("1.19.2", 10, DataFixerV1192::new), V1_20_5("1.20.6", 41, DataFixerV1206::new), V1_21_3("1.21.3", 57, DataFixerV1213::new), - V1_21_11("1.21.11", 75, DataFixerV1217::new); + V1_21_11("1.21.11", 75, DataFixerV1217::new), + V26_1_2("26.1.2", 101, DataFixerV1217::new); private static final KMap cache = new KMap<>(); @Getter(AccessLevel.NONE) private final Supplier constructor; diff --git a/core/src/main/java/art/arcane/iris/core/project/SchemaBuilder.java b/core/src/main/java/art/arcane/iris/core/project/SchemaBuilder.java index 61c1967da..e4122e909 100644 --- a/core/src/main/java/art/arcane/iris/core/project/SchemaBuilder.java +++ b/core/src/main/java/art/arcane/iris/core/project/SchemaBuilder.java @@ -26,6 +26,7 @@ import art.arcane.iris.core.loader.IrisRegistrant; import art.arcane.iris.core.loader.ResourceLoader; import art.arcane.iris.core.service.ExternalDataSVC; import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.core.nms.INMS; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; import art.arcane.iris.util.common.data.B; @@ -267,6 +268,44 @@ public class SchemaBuilder { prop.put("$ref", "#/definitions/" + key); description.add(SYMBOL_TYPE__N + " Must be a valid Block Type (use ctrl+space for auto complete!)"); + } else if (k.isAnnotationPresent(RegistryListVanillaStructure.class)) { + String key = "enum-vanilla-structure"; + + if (!definitions.containsKey(key)) { + JSONObject j = new JSONObject(); + JSONArray ja = new JSONArray(); + + for (String i : INMS.get().getStructureKeys()) { + ja.put(i); + } + + j.put("enum", ja); + definitions.put(key, j); + } + + fancyType = "Vanilla Structure"; + prop.put("$ref", "#/definitions/" + key); + description.add(SYMBOL_TYPE__N + " Must be a valid vanilla/datapack structure key (use ctrl+space for auto complete!)"); + + } else if (k.isAnnotationPresent(RegistryListVanillaStructureSet.class)) { + String key = "enum-vanilla-structure-set"; + + if (!definitions.containsKey(key)) { + JSONObject j = new JSONObject(); + JSONArray ja = new JSONArray(); + + for (String i : INMS.get().getStructureSetKeys()) { + ja.put(i); + } + + j.put("enum", ja); + definitions.put(key, j); + } + + fancyType = "Vanilla Structure Set"; + prop.put("$ref", "#/definitions/" + key); + description.add(SYMBOL_TYPE__N + " Must be a valid vanilla/datapack structure SET key (use ctrl+space for auto complete!)"); + } else if (k.isAnnotationPresent(RegistryListItemType.class)) { String key = "enum-item-type"; diff --git a/core/src/main/java/art/arcane/iris/core/structure/StructureImporter.java b/core/src/main/java/art/arcane/iris/core/structure/StructureImporter.java new file mode 100644 index 000000000..de8595767 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/StructureImporter.java @@ -0,0 +1,171 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 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 . + */ + +package art.arcane.iris.core.structure; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.LegacyTileData; +import com.google.gson.GsonBuilder; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.block.BlockState; +import org.bukkit.structure.Palette; +import org.bukkit.structure.Structure; +import org.bukkit.util.BlockVector; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public final class StructureImporter { + public enum Mode { + OVERWRITE, + ADD_ONLY, + MERGE + } + + public record Result(boolean success, String message, int blocks) { + } + + private StructureImporter() { + } + + public static Mode parseMode(String s) { + if (s == null) { + return Mode.OVERWRITE; + } + return switch (s.toLowerCase().replace('-', '_')) { + case "add_only", "addonly", "add" -> Mode.ADD_ONLY; + case "merge" -> Mode.MERGE; + default -> Mode.OVERWRITE; + }; + } + + public static String deriveName(NamespacedKey key) { + return (key.getNamespace() + "_" + key.getKey()).replace('/', '_').replace(':', '_'); + } + + public static Result importStructure(IrisData data, NamespacedKey key, String name, Mode mode) { + Structure structure; + try { + structure = Bukkit.getStructureManager().loadStructure(key); + } catch (Throwable e) { + return new Result(false, "Failed to load structure " + key + ": " + e.getMessage(), 0); + } + if (structure == null || structure.getPalettes().isEmpty()) { + return new Result(false, "No loadable structure NBT for key " + key + " (jigsaw structures must be imported by their piece keys)", 0); + } + + BlockVector size = structure.getSize(); + int w = Math.max(1, size.getBlockX()); + int h = Math.max(1, size.getBlockY()); + int d = Math.max(1, size.getBlockZ()); + + File objectFile = new File(data.getDataFolder(), "objects/" + name + ".iob"); + File pieceFile = new File(data.getDataFolder(), "jigsaw-pieces/" + name + ".json"); + File poolFile = new File(data.getDataFolder(), "jigsaw-pools/" + name + ".json"); + File structureFile = new File(data.getDataFolder(), "structures/" + name + ".json"); + + boolean exists = objectFile.exists() || structureFile.exists(); + if (mode == Mode.ADD_ONLY && exists) { + return new Result(false, "Skipped (add-only): '" + name + "' already exists", 0); + } + + IrisObject object = new IrisObject(w, h, d); + int count = 0; + int tiles = 0; + Palette palette = structure.getPalettes().get(0); + for (BlockState block : palette.getBlocks()) { + Location loc = block.getLocation(); + int x = loc.getBlockX(); + int y = loc.getBlockY(); + int z = loc.getBlockZ(); + if (x < 0 || y < 0 || z < 0 || x >= w || y >= h || z >= d) { + continue; + } + object.setUnsigned(x, y, z, block.getBlockData()); + count++; + LegacyTileData tile = captureTile(block); + if (tile != null) { + object.setUnsignedTile(x, y, z, tile); + tiles++; + } + } + + try { + objectFile.getParentFile().mkdirs(); + object.write(objectFile); + writeJson(pieceFile, pieceJson(name)); + writeJson(poolFile, poolJson(name)); + writeJson(structureFile, structureJson(name, key.toString(), Math.max(w, d))); + } catch (Throwable e) { + return new Result(false, "Failed writing import for '" + name + "': " + e.getMessage(), count); + } + + return new Result(true, "Imported " + key + " as '" + name + "' (" + count + " blocks, " + tiles + " tiles, " + w + "x" + h + "x" + d + ")", count); + } + + private static LegacyTileData captureTile(BlockState block) { + try { + return LegacyTileData.fromBukkit(block); + } catch (Throwable e) { + return null; + } + } + + private static Map pieceJson(String name) { + Map piece = new LinkedHashMap<>(); + piece.put("object", name); + piece.put("connectors", new ArrayList<>()); + piece.put("rotatable", true); + return piece; + } + + private static Map poolJson(String name) { + Map entry = new LinkedHashMap<>(); + entry.put("piece", name); + entry.put("weight", 1); + List pieces = new ArrayList<>(); + pieces.add(entry); + Map pool = new LinkedHashMap<>(); + pool.put("pieces", pieces); + return pool; + } + + private static Map structureJson(String name, String source, int maxSpan) { + Map root = new LinkedHashMap<>(); + root.put("startPool", name); + root.put("maxDepth", 1); + root.put("maxSizeChunks", Math.max(1, (maxSpan / 16) + 1)); + root.put("placeMode", "CENTER_HEIGHT"); + root.put("vanillaSource", source); + return root; + } + + private static void writeJson(File file, Map content) throws Exception { + file.getParentFile().mkdirs(); + String json = new GsonBuilder().setPrettyPrinting().create().toJson(content); + Files.writeString(file.toPath(), json, StandardCharsets.UTF_8); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/StructureIndexService.java b/core/src/main/java/art/arcane/iris/core/structure/StructureIndexService.java new file mode 100644 index 000000000..d0b8699bc --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/StructureIndexService.java @@ -0,0 +1,130 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 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 . + */ + +package art.arcane.iris.core.structure; + +import art.arcane.iris.Iris; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.nms.INMS; +import art.arcane.volmlib.util.collection.KList; +import com.google.gson.GsonBuilder; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public final class StructureIndexService { + private static final Set GENERATED = ConcurrentHashMap.newKeySet(); + + private StructureIndexService() { + } + + public static File writeOnce(IrisData data) { + if (data == null) { + return null; + } + + File dataFolder = data.getDataFolder(); + String key = dataFolder == null ? null : dataFolder.getAbsolutePath(); + if (key == null || !GENERATED.add(key)) { + return null; + } + + try { + return write(data); + } catch (Throwable e) { + GENERATED.remove(key); + Iris.reportError(e); + return null; + } + } + + public static File write(IrisData data) { + KList structures = INMS.get().getStructureKeys(); + KList sets = INMS.get().getStructureSetKeys(); + + List vanilla = new ArrayList<>(); + List datapack = new ArrayList<>(); + for (String k : structures) { + if (k.startsWith("minecraft:")) { + vanilla.add(k); + } else { + datapack.add(k); + } + } + + List vanillaSets = new ArrayList<>(); + List datapackSets = new ArrayList<>(); + for (String k : sets) { + if (k.startsWith("minecraft:")) { + vanillaSets.add(k); + } else { + datapackSets.add(k); + } + } + + List iris = new ArrayList<>(); + if (data.getStructureLoader() != null) { + Collections.addAll(iris, data.getStructureLoader().getPossibleKeys()); + } + + Collections.sort(vanilla); + Collections.sort(datapack); + Collections.sort(vanillaSets); + Collections.sort(datapackSets); + Collections.sort(iris); + + Map structuresNode = new LinkedHashMap<>(); + structuresNode.put("vanilla", vanilla); + structuresNode.put("datapack", datapack); + + Map setsNode = new LinkedHashMap<>(); + setsNode.put("vanilla", vanillaSets); + setsNode.put("datapack", datapackSets); + + Map counts = new LinkedHashMap<>(); + counts.put("vanillaStructures", vanilla.size()); + counts.put("datapackStructures", datapack.size()); + counts.put("structureSets", sets.size()); + counts.put("irisStructures", iris.size()); + + Map root = new LinkedHashMap<>(); + root.put("note", "Generated index of every structure available at runtime. Toggle vanilla/datapack generation via the dimension vanillaStructures block (by structure SET key). Place structures via biome/region/dimension structures lists."); + root.put("counts", counts); + root.put("structures", structuresNode); + root.put("structureSets", setsNode); + root.put("iris", iris); + + File file = new File(data.getDataFolder(), "structures/structure-index.json"); + try { + file.getParentFile().mkdirs(); + String json = new GsonBuilder().setPrettyPrinting().create().toJson(root); + Files.writeString(file.toPath(), json, StandardCharsets.UTF_8); + } catch (Throwable e) { + Iris.reportError(e); + } + return file; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/VillageImporter.java b/core/src/main/java/art/arcane/iris/core/structure/VillageImporter.java new file mode 100644 index 000000000..6dad17e74 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/VillageImporter.java @@ -0,0 +1,596 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 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 . + */ + +package art.arcane.iris.core.structure; + +import art.arcane.iris.core.loader.IrisData; +import com.google.gson.GsonBuilder; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public final class VillageImporter { + public record Result(boolean success, String message, int pools, int pieces) { + } + + private VillageImporter() { + } + + public static Result importVillage(IrisData data, NamespacedKey structureKey, String name, StructureImporter.Mode mode) { + Object server; + Object registryAccess; + Object structureManager; + try { + Object craftServer = Bukkit.getServer(); + Object dedicated = invoke(craftServer, "getHandle"); + server = invoke(dedicated, "getServer"); + registryAccess = resolveRegistryAccess(server); + structureManager = invoke(server, "getStructureManager"); + } catch (Throwable e) { + return new Result(false, "Failed to access server registries via reflection: " + e, 0, 0); + } + if (registryAccess == null) { + return new Result(false, "Could not resolve RegistryAccess from the server", 0, 0); + } + + Object startPool; + int maxDepth; + try { + Object structureRegistry = lookupRegistry(registryAccess, "STRUCTURE"); + Object structure = registryGet(structureRegistry, structureKey); + if (structure == null) { + return new Result(false, "No structure registered for key " + structureKey, 0, 0); + } + if (!structure.getClass().getName().endsWith("JigsawStructure")) { + return new Result(false, "Structure " + structureKey + " is not a jigsaw structure (" + structure.getClass().getSimpleName() + "); use 'import' for single-template structures", 0, 0); + } + Object startPoolHolder = invoke(structure, "getStartPool"); + startPool = unwrapHolder(startPoolHolder); + maxDepth = readIntField(structure, "maxDepth", 7); + } catch (Throwable e) { + return new Result(false, "Failed to read jigsaw structure graph: " + e, 0, 0); + } + + Object templatePoolRegistry; + java.util.Random random; + try { + templatePoolRegistry = lookupRegistry(registryAccess, "TEMPLATE_POOL"); + random = new java.util.Random(structureKey.hashCode()); + } catch (Throwable e) { + return new Result(false, "Failed to access TEMPLATE_POOL registry: " + e, 0, 0); + } + + String startPoolKey; + try { + startPoolKey = registryKeyOf(templatePoolRegistry, startPool); + } catch (Throwable e) { + startPoolKey = null; + } + if (startPoolKey == null) { + return new Result(false, "Could not resolve the start pool key for " + structureKey, 0, 0); + } + + Set visitedPools = new HashSet<>(); + Set importedPieces = new HashSet<>(); + Deque poolQueue = new ArrayDeque<>(); + poolQueue.add(startPoolKey); + + Map> emittedPools = new LinkedHashMap<>(); + Map> emittedPieces = new LinkedHashMap<>(); + List errors = new ArrayList<>(); + int pieceBlocks = 0; + + while (!poolQueue.isEmpty()) { + String poolKey = poolQueue.poll(); + if (!visitedPools.add(poolKey)) { + continue; + } + Object pool; + try { + pool = registryGetByKey(templatePoolRegistry, poolKey); + } catch (Throwable e) { + errors.add("pool " + poolKey + ": " + e.getMessage()); + continue; + } + if (pool == null) { + continue; + } + + String irisPoolName = poolName(name, poolKey); + List pieceEntries = new ArrayList<>(); + + String fallbackKey = null; + try { + Object fallbackHolder = invoke(pool, "getFallback"); + Object fallbackPool = unwrapHolder(fallbackHolder); + fallbackKey = registryKeyOf(templatePoolRegistry, fallbackPool); + } catch (Throwable ignored) { + } + if (fallbackKey != null && !fallbackKey.equals(poolKey)) { + poolQueue.add(fallbackKey); + } + + List templates; + try { + templates = (List) invoke(pool, "getTemplates"); + } catch (Throwable e) { + errors.add("templates " + poolKey + ": " + e.getMessage()); + templates = List.of(); + } + + for (Object pair : templates) { + Object element; + int weight; + try { + element = invoke(pair, "getFirst"); + Object second = invoke(pair, "getSecond"); + weight = second instanceof Number ? Math.max(1, ((Number) second).intValue()) : 1; + } catch (Throwable e) { + continue; + } + if (element == null) { + continue; + } + String templateLocation = templateLocationOf(element); + if (templateLocation == null) { + continue; + } + NamespacedKey pieceNbtKey = NamespacedKey.fromString(templateLocation.toLowerCase()); + if (pieceNbtKey == null) { + errors.add("bad piece key " + templateLocation); + continue; + } + String irisPieceName = pieceName(name, templateLocation); + + if (importedPieces.add(irisPieceName)) { + StructureImporter.Result imported = StructureImporter.importStructure(data, pieceNbtKey, irisPieceName, mode); + if (!imported.success()) { + errors.add(templateLocation + ": " + imported.message()); + continue; + } + pieceBlocks += imported.blocks(); + removeStrayPieceArtifacts(data, irisPieceName); + + Connectors result = readConnectors(element, structureManager, random, name); + emittedPieces.put(irisPieceName, pieceJson(irisPieceName, result.json())); + poolQueue.addAll(result.targetPoolKeys()); + } + + Map entry = new LinkedHashMap<>(); + entry.put("piece", irisPieceName); + entry.put("weight", weight); + pieceEntries.add(entry); + } + + Map poolJson = new LinkedHashMap<>(); + poolJson.put("pieces", pieceEntries); + if (fallbackKey != null && !fallbackKey.equals(poolKey)) { + poolJson.put("fallback", poolName(name, fallbackKey)); + } + emittedPools.put(irisPoolName, poolJson); + } + + if (emittedPieces.isEmpty()) { + return new Result(false, "Imported 0 pieces for " + structureKey + (errors.isEmpty() ? "" : " (" + errors.get(0) + ")"), 0, 0); + } + + try { + for (Map.Entry> e : emittedPieces.entrySet()) { + writeJson(new File(data.getDataFolder(), "jigsaw-pieces/" + e.getKey() + ".json"), e.getValue()); + } + for (Map.Entry> e : emittedPools.entrySet()) { + writeJson(new File(data.getDataFolder(), "jigsaw-pools/" + e.getKey() + ".json"), e.getValue()); + } + writeJson(new File(data.getDataFolder(), "structures/" + name + ".json"), structureJson(name, structureKey.toString(), poolName(name, startPoolKey), maxDepth)); + } catch (Throwable e) { + return new Result(false, "Failed writing jigsaw resources for '" + name + "': " + e, emittedPools.size(), emittedPieces.size()); + } + + String msg = "Imported village " + structureKey + " as '" + name + "': " + emittedPieces.size() + " pieces, " + emittedPools.size() + " pools, " + pieceBlocks + " blocks"; + if (!errors.isEmpty()) { + msg += " (" + errors.size() + " piece(s) skipped; first: " + errors.get(0) + ")"; + } + return new Result(true, msg, emittedPools.size(), emittedPieces.size()); + } + + private record Connectors(List> json, Set targetPoolKeys) { + } + + private static Connectors readConnectors(Object element, Object structureManager, java.util.Random random, String baseName) { + List> connectors = new ArrayList<>(); + Set targets = new HashSet<>(); + try { + Object zero = staticField("net.minecraft.core.BlockPos", "ZERO"); + Object rotationNone = staticField("net.minecraft.world.level.block.Rotation", "NONE"); + Method m = findMethod4(element.getClass(), "getShuffledJigsawBlocks"); + if (m == null) { + return new Connectors(connectors, targets); + } + m.setAccessible(true); + Object random0 = freshRandomSource(random); + List blocks = (List) m.invoke(element, structureManager, zero, rotationNone, random0); + if (blocks == null) { + return new Connectors(connectors, targets); + } + for (Object jigsaw : blocks) { + String[] rawPoolKey = new String[1]; + Map connector = connectorFrom(jigsaw, baseName, rawPoolKey); + if (connector != null) { + connectors.add(connector); + if (rawPoolKey[0] != null && !rawPoolKey[0].isEmpty()) { + targets.add(rawPoolKey[0]); + } + } + } + } catch (Throwable ignored) { + } + return new Connectors(connectors, targets); + } + + private static Map connectorFrom(Object jigsaw, String baseName, String[] rawPoolKeyOut) { + try { + Object info = invoke(jigsaw, "info"); + Object pos = invoke(info, "pos"); + Object blockState = invoke(info, "state"); + int x = readInt(pos, "getX"); + int y = readInt(pos, "getY"); + int z = readInt(pos, "getZ"); + + Object poolKey = invoke(jigsaw, "pool"); + String poolId = identifierString(invoke(poolKey, "identifier")); + Object nameId = invoke(jigsaw, "name"); + Object targetId = invoke(jigsaw, "target"); + Object jointType = invoke(jigsaw, "jointType"); + + String front = frontFacing(blockState); + rawPoolKeyOut[0] = poolId; + + Map connector = new LinkedHashMap<>(); + Map position = new LinkedHashMap<>(); + position.put("x", x); + position.put("y", y); + position.put("z", z); + connector.put("position", position); + connector.put("direction", irisDirection(front)); + connector.put("pool", poolId == null ? "" : poolName(baseName, poolId)); + connector.put("name", identifierString(nameId)); + connector.put("targetName", identifierString(targetId)); + connector.put("joint", jointType != null && jointType.toString().toUpperCase().contains("ALIGN") ? "ALIGNED" : "ROLLABLE"); + return connector; + } catch (Throwable e) { + return null; + } + } + + private static String frontFacing(Object blockState) { + try { + Class jigsawBlock = Class.forName("net.minecraft.world.level.block.JigsawBlock"); + Method getFront = jigsawBlock.getMethod("getFrontFacing", Class.forName("net.minecraft.world.level.block.state.BlockState")); + Object direction = getFront.invoke(null, blockState); + if (direction == null) { + return "north"; + } + Method getName = direction.getClass().getMethod("getName"); + getName.setAccessible(true); + return String.valueOf(getName.invoke(direction)).toLowerCase(); + } catch (Throwable e) { + return "north"; + } + } + + private static String irisDirection(String front) { + return switch (front) { + case "up" -> "UP_POSITIVE_Y"; + case "down" -> "DOWN_NEGATIVE_Y"; + case "south" -> "SOUTH_POSITIVE_Z"; + case "east" -> "EAST_POSITIVE_X"; + case "west" -> "WEST_NEGATIVE_X"; + default -> "NORTH_NEGATIVE_Z"; + }; + } + + private static String templateLocationOf(Object element) { + try { + Method m = findMethod(element.getClass(), "getTemplateLocation"); + if (m == null) { + return null; + } + m.setAccessible(true); + Object id = m.invoke(element); + return identifierString(id); + } catch (Throwable e) { + return null; + } + } + + private static Object resolveRegistryAccess(Object server) { + try { + Class frozen = Class.forName("net.minecraft.core.RegistryAccess$Frozen"); + for (Method m : server.getClass().getMethods()) { + if (m.getParameterCount() == 0 && frozen.isAssignableFrom(m.getReturnType())) { + m.setAccessible(true); + Object o = m.invoke(server); + if (o != null) { + return o; + } + } + } + Class ra = Class.forName("net.minecraft.core.RegistryAccess"); + for (Method m : server.getClass().getMethods()) { + if (m.getParameterCount() == 0 && ra.isAssignableFrom(m.getReturnType())) { + m.setAccessible(true); + Object o = m.invoke(server); + if (o != null) { + return o; + } + } + } + } catch (Throwable ignored) { + } + return null; + } + + private static Object lookupRegistry(Object registryAccess, String registryName) throws Exception { + Class registries = Class.forName("net.minecraft.core.registries.Registries"); + Object resourceKey = registries.getField(registryName).get(null); + Class registryClass = Class.forName("net.minecraft.core.Registry"); + Method registryOverload = null; + for (Method m : registryAccess.getClass().getMethods()) { + if (m.getName().equals("lookupOrThrow") && m.getParameterCount() == 1 + && m.getParameterTypes()[0].getName().endsWith("ResourceKey") + && registryClass.isAssignableFrom(m.getReturnType())) { + registryOverload = m; + break; + } + } + if (registryOverload == null) { + for (Method m : registryAccess.getClass().getMethods()) { + if (m.getName().equals("lookupOrThrow") && m.getParameterCount() == 1 + && m.getParameterTypes()[0].getName().endsWith("ResourceKey")) { + registryOverload = m; + break; + } + } + } + if (registryOverload == null) { + throw new NoSuchMethodException("lookupOrThrow(ResourceKey) on " + registryAccess.getClass().getName()); + } + registryOverload.setAccessible(true); + return registryOverload.invoke(registryAccess, resourceKey); + } + + private static Object registryGet(Object registry, NamespacedKey key) throws Exception { + Object id = identifierOf(key); + for (Method m : registry.getClass().getMethods()) { + if (m.getName().equals("getValue") && m.getParameterCount() == 1 && m.getParameterTypes()[0].getName().endsWith("Identifier")) { + m.setAccessible(true); + return m.invoke(registry, id); + } + } + for (Method m : registry.getClass().getMethods()) { + if (m.getName().equals("getOptional") && m.getParameterCount() == 1 && m.getParameterTypes()[0].getName().endsWith("Identifier")) { + m.setAccessible(true); + return unwrapOptional(m.invoke(registry, id)); + } + } + return null; + } + + private static Object registryGetByKey(Object registry, String key) throws Exception { + NamespacedKey nk = NamespacedKey.fromString(key.toLowerCase()); + if (nk == null) { + return null; + } + return registryGet(registry, nk); + } + + private static String registryKeyOf(Object registry, Object value) throws Exception { + if (value == null) { + return null; + } + for (Method m : registry.getClass().getMethods()) { + if (m.getName().equals("getKey") && m.getParameterCount() == 1) { + m.setAccessible(true); + Object id = m.invoke(registry, value); + String s = identifierString(id); + if (s != null) { + return s; + } + } + } + return null; + } + + private static Object identifierOf(NamespacedKey key) throws Exception { + Class identifier = Class.forName("net.minecraft.resources.Identifier"); + try { + Method fromNamespaceAndPath = identifier.getMethod("fromNamespaceAndPath", String.class, String.class); + return fromNamespaceAndPath.invoke(null, key.getNamespace(), key.getKey()); + } catch (NoSuchMethodException e) { + Method withDefaultNamespace = identifier.getMethod("parse", String.class); + return withDefaultNamespace.invoke(null, key.toString()); + } + } + + private static String identifierString(Object id) { + if (id == null) { + return null; + } + try { + Method getNamespace = id.getClass().getMethod("getNamespace"); + Method getPath = id.getClass().getMethod("getPath"); + getNamespace.setAccessible(true); + getPath.setAccessible(true); + return getNamespace.invoke(id) + ":" + getPath.invoke(id); + } catch (Throwable e) { + return id.toString(); + } + } + + private static Object unwrapHolder(Object holder) { + if (holder == null) { + return null; + } + try { + Method value = findMethod(holder.getClass(), "value"); + if (value != null) { + value.setAccessible(true); + return value.invoke(holder); + } + } catch (Throwable ignored) { + } + return holder; + } + + private static Object unwrapOptional(Object opt) { + if (opt == null) { + return null; + } + if (opt instanceof java.util.Optional o) { + return o.orElse(null); + } + return opt; + } + + private static int readIntField(Object o, String fieldName, int fallback) { + try { + Field f = o.getClass().getDeclaredField(fieldName); + f.setAccessible(true); + return f.getInt(o); + } catch (Throwable e) { + return fallback; + } + } + + private static int readInt(Object o, String method) throws Exception { + Method m = o.getClass().getMethod(method); + m.setAccessible(true); + return ((Number) m.invoke(o)).intValue(); + } + + private static Object staticField(String className, String fieldName) throws Exception { + Class c = Class.forName(className); + Field f = c.getField(fieldName); + return f.get(null); + } + + private static Object invoke(Object target, String method) throws Exception { + Method m = findMethod(target.getClass(), method); + if (m == null) { + throw new NoSuchMethodException(method + " on " + target.getClass().getName()); + } + m.setAccessible(true); + return m.invoke(target); + } + + private static Method findMethod(Class type, String name) { + Class c = type; + while (c != null) { + for (Method m : c.getDeclaredMethods()) { + if (m.getName().equals(name) && m.getParameterCount() == 0) { + return m; + } + } + c = c.getSuperclass(); + } + for (Method m : type.getMethods()) { + if (m.getName().equals(name) && m.getParameterCount() == 0) { + return m; + } + } + return null; + } + + private static Method findMethod4(Class type, String name) { + Class c = type; + while (c != null) { + for (Method m : c.getDeclaredMethods()) { + if (m.getName().equals(name) && m.getParameterCount() == 4) { + return m; + } + } + c = c.getSuperclass(); + } + for (Method m : type.getMethods()) { + if (m.getName().equals(name) && m.getParameterCount() == 4) { + return m; + } + } + return null; + } + + private static Object freshRandomSource(java.util.Random random) throws Exception { + Class randomSource = Class.forName("net.minecraft.util.RandomSource"); + Method create = randomSource.getMethod("create", long.class); + return create.invoke(null, random.nextLong()); + } + + private static String poolName(String base, String poolKey) { + return base + "/pool/" + sanitize(poolKey); + } + + private static String pieceName(String base, String templateLocation) { + return base + "/piece/" + sanitize(templateLocation); + } + + private static String sanitize(String key) { + return key.replace(':', '_').replace('/', '_'); + } + + private static Map pieceJson(String pieceName, List> connectors) { + Map piece = new LinkedHashMap<>(); + piece.put("object", pieceName); + piece.put("connectors", connectors); + piece.put("rotatable", true); + return piece; + } + + private static Map structureJson(String name, String source, String startPool, int maxDepth) { + Map root = new LinkedHashMap<>(); + root.put("startPool", startPool); + root.put("maxDepth", Math.max(1, Math.min(30, maxDepth))); + root.put("maxSizeChunks", 8); + root.put("placeMode", "STRUCTURE_PIECE"); + root.put("vanillaSource", source); + return root; + } + + private static void removeStrayPieceArtifacts(IrisData data, String pieceName) { + new File(data.getDataFolder(), "jigsaw-pools/" + pieceName + ".json").delete(); + new File(data.getDataFolder(), "structures/" + pieceName + ".json").delete(); + } + + private static void writeJson(File file, Map content) throws Exception { + file.getParentFile().mkdirs(); + String json = new GsonBuilder().setPrettyPrinting().create().toJson(content); + Files.writeString(file.toPath(), json, StandardCharsets.UTF_8); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java index f441d8076..c52d276ec 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java @@ -31,6 +31,7 @@ import art.arcane.iris.core.nms.container.BlockPos; import art.arcane.iris.core.nms.container.Pair; import art.arcane.iris.core.project.IrisProject; import art.arcane.iris.core.service.PreservationSVC; +import art.arcane.iris.core.structure.StructureIndexService; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.data.cache.AtomicCache; import art.arcane.iris.engine.framework.*; @@ -147,6 +148,11 @@ public class IrisEngine implements Engine { _t0 = M.ms(); getData().loadPrefetch(this); Iris.info("[IrisEngine timing] loadPrefetch=" + (M.ms() - _t0) + "ms"); + try { + StructureIndexService.writeOnce(getData()); + } catch (Throwable e) { + Iris.reportError(e); + } Iris.info("Initializing Engine: " + target.getWorld().name() + "/" + target.getDimension().getLoadKey() + " (" + target.getDimension().getDimensionHeight() + " height) Seed: " + getSeedManager().getSeed()); failing = false; closed = false; diff --git a/core/src/main/java/art/arcane/iris/engine/IrisEngineMantle.java b/core/src/main/java/art/arcane/iris/engine/IrisEngineMantle.java index 03d66d994..0a06fa567 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisEngineMantle.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisEngineMantle.java @@ -31,6 +31,7 @@ import art.arcane.iris.engine.mantle.components.MantleCarvingComponent; import art.arcane.iris.engine.mantle.components.MantleFloatingObjectComponent; import art.arcane.iris.engine.mantle.components.MantleFluidBodyComponent; import art.arcane.iris.engine.mantle.components.MantleObjectComponent; +import art.arcane.iris.engine.mantle.components.IrisStructureComponent; import art.arcane.iris.util.project.matter.IrisMatterSupport; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; @@ -84,6 +85,7 @@ public class IrisEngineMantle implements EngineMantle { object = new MantleObjectComponent(this); registerComponent(object); registerComponent(new MantleFloatingObjectComponent(this)); + registerComponent(new IrisStructureComponent(this)); } @Override diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/DecoratorCore.java b/core/src/main/java/art/arcane/iris/engine/decorator/DecoratorCore.java index bce313c10..414fdb5ea 100644 --- a/core/src/main/java/art/arcane/iris/engine/decorator/DecoratorCore.java +++ b/core/src/main/java/art/arcane/iris/engine/decorator/DecoratorCore.java @@ -348,8 +348,10 @@ final class DecoratorCore { r = EngineMantle.AIR; } if (r.isFaceSturdy(f.getOppositeFace(), BlockSupport.FULL)) { - found = true; - data.setFace(f, true); + if (data.getAllowedFaces().contains(f)) { + found = true; + data.setFace(f, true); + } continue; } @@ -361,12 +363,17 @@ final class DecoratorCore { r = hunk.get(xx, yy, zz); if (r.isFaceSturdy(f.getOppositeFace(), BlockSupport.FULL)) { - found = true; - data.setFace(f, true); + if (data.getAllowedFaces().contains(f)) { + found = true; + data.setFace(f, true); + } } } if (!found) { - data.setFace(BlockFace.DOWN, true); + BlockFace fallback = data.getAllowedFaces().contains(BlockFace.DOWN) ? BlockFace.DOWN : BlockFace.UP; + if (data.getAllowedFaces().contains(fallback)) { + data.setFace(fallback, true); + } } return data; } diff --git a/core/src/main/java/art/arcane/iris/engine/framework/PlacedStructurePiece.java b/core/src/main/java/art/arcane/iris/engine/framework/PlacedStructurePiece.java new file mode 100644 index 000000000..783217033 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/PlacedStructurePiece.java @@ -0,0 +1,48 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 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 . + */ + +package art.arcane.iris.engine.framework; + +import art.arcane.iris.engine.object.IrisJigsawPiece; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisObjectRotation; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class PlacedStructurePiece { + private final IrisJigsawPiece piece; + private final IrisObject object; + private final int x; + private final int y; + private final int z; + private final IrisObjectRotation rotation; + private final int minX; + private final int minY; + private final int minZ; + private final int maxX; + private final int maxY; + private final int maxZ; + + public boolean intersects(PlacedStructurePiece o) { + return minX <= o.maxX && maxX >= o.minX + && minY <= o.maxY && maxY >= o.minY + && minZ <= o.maxZ && maxZ >= o.minZ; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/StructureAssembler.java b/core/src/main/java/art/arcane/iris/engine/framework/StructureAssembler.java new file mode 100644 index 000000000..ee5b37f67 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/StructureAssembler.java @@ -0,0 +1,278 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 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 . + */ + +package art.arcane.iris.engine.framework; + +import art.arcane.iris.Iris; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.object.IrisDirection; +import art.arcane.iris.engine.object.IrisJigsawConnector; +import art.arcane.iris.engine.object.IrisJigsawPiece; +import art.arcane.iris.engine.object.IrisJigsawPieceEntry; +import art.arcane.iris.engine.object.IrisJigsawPool; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisObjectRotation; +import art.arcane.iris.engine.object.IrisStructure; +import art.arcane.iris.engine.object.JigsawJoint; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import org.bukkit.util.BlockVector; + +import java.util.ArrayDeque; +import java.util.Deque; + +public final class StructureAssembler { + private static final int HARD_PIECE_CAP = 512; + private static final int[] Y_DEGREES = {0, 90, 180, 270}; + + private final IrisData data; + private final IrisStructure structure; + private final int originX; + private final int originY; + private final int originZ; + private final int radius; + + public StructureAssembler(IrisData data, IrisStructure structure, int originX, int originY, int originZ) { + this.data = data; + this.structure = structure; + this.originX = originX; + this.originY = originY; + this.originZ = originZ; + this.radius = Math.max(1, structure.getMaxSizeChunks()) * 16; + } + + public KList assemble(RNG rng) { + IrisJigsawPool startPool = IrisData.loadAnyJigsawPool(structure.getStartPool(), data); + if (startPool == null || startPool.getPieces().isEmpty()) { + Iris.warn("Structure " + structure.getLoadKey() + " has no resolvable start pool '" + structure.getStartPool() + "'"); + return null; + } + + KList placed = new KList<>(); + Deque open = new ArrayDeque<>(); + + IrisJigsawPiece startPiece = pickPiece(startPool, rng); + if (startPiece == null) { + return null; + } + IrisObject startObject = IrisData.loadAnyObject(startPiece.getObject(), data); + if (startObject == null) { + Iris.warn("Jigsaw piece references missing object '" + startPiece.getObject() + "'"); + return null; + } + + int startRotY = startPiece.isRotatable() ? Y_DEGREES[rng.i(0, 3)] : 0; + IrisObjectRotation startRot = IrisObjectRotation.of(0, startRotY, 0); + PlacedStructurePiece start = build(startPiece, startObject, originX, originY, originZ, startRot); + placed.add(start); + enqueueConnectors(open, start, startObject, 1, null); + + while (!open.isEmpty() && placed.size() < HARD_PIECE_CAP) { + OpenConnector c = open.poll(); + String poolName = c.pool; + int depth = c.depth; + + IrisJigsawPool pool = IrisData.loadAnyJigsawPool(poolName, data); + if (pool == null) { + continue; + } + if (depth > structure.getMaxDepth()) { + if (pool.getFallback() == null || pool.getFallback().isEmpty()) { + continue; + } + pool = IrisData.loadAnyJigsawPool(pool.getFallback(), data); + if (pool == null || pool.getPieces().isEmpty()) { + continue; + } + } + + attachOne(open, placed, pool, c, depth, rng); + } + + return placed; + } + + private void attachOne(Deque open, KList placed, IrisJigsawPool pool, OpenConnector c, int depth, RNG rng) { + for (String pieceName : weightedOrder(pool, rng)) { + IrisJigsawPiece piece = IrisData.loadAnyJigsawPiece(pieceName, data); + if (piece == null) { + continue; + } + IrisObject object = IrisData.loadAnyObject(piece.getObject(), data); + if (object == null) { + continue; + } + + for (IrisJigsawConnector cb : piece.getConnectors()) { + if (!cb.getName().equals(c.targetName)) { + continue; + } + + IrisDirection needed = c.facing.reverse(); + for (int yDeg : rotationCandidates(cb.getJoint(), rng)) { + IrisObjectRotation rot = IrisObjectRotation.of(0, yDeg, 0); + IrisDirection rotatedFace = rotateDirection(rot, cb.getDirection()); + if (rotatedFace != needed) { + continue; + } + + BlockVector centerB = centerOf(object); + BlockVector crB = new BlockVector( + cb.getPosition().getX() - centerB.getBlockX(), + cb.getPosition().getY() - centerB.getBlockY(), + cb.getPosition().getZ() - centerB.getBlockZ()); + BlockVector rcrB = rot.rotate(crB.clone(), 0, 0, 0); + int wcx = c.wx + c.facing.toVector().getBlockX(); + int wcy = c.wy + c.facing.toVector().getBlockY(); + int wcz = c.wz + c.facing.toVector().getBlockZ(); + int px = wcx - rcrB.getBlockX(); + int py = wcy - rcrB.getBlockY(); + int pz = wcz - rcrB.getBlockZ(); + + PlacedStructurePiece candidate = build(piece, object, px, py, pz, rot); + if (!withinRadius(candidate) || collides(placed, candidate)) { + continue; + } + + placed.add(candidate); + enqueueConnectors(open, candidate, object, depth + 1, cb); + return; + } + } + } + } + + private void enqueueConnectors(Deque open, PlacedStructurePiece p, IrisObject object, int depth, IrisJigsawConnector skip) { + BlockVector center = centerOf(object); + for (IrisJigsawConnector con : p.getPiece().getConnectors()) { + if (con == skip) { + continue; + } + BlockVector cr = new BlockVector( + con.getPosition().getX() - center.getBlockX(), + con.getPosition().getY() - center.getBlockY(), + con.getPosition().getZ() - center.getBlockZ()); + BlockVector rcr = p.getRotation().rotate(cr.clone(), 0, 0, 0); + IrisDirection facing = rotateDirection(p.getRotation(), con.getDirection()); + open.add(new OpenConnector( + p.getX() + rcr.getBlockX(), + p.getY() + rcr.getBlockY(), + p.getZ() + rcr.getBlockZ(), + facing, con.getPool(), con.getName(), con.getTargetName(), con.getJoint(), depth)); + } + } + + private PlacedStructurePiece build(IrisJigsawPiece piece, IrisObject object, int x, int y, int z, IrisObjectRotation rot) { + BlockVector rotated = rot.rotate(new BlockVector(object.getW(), object.getH(), object.getD()), 0, 0, 0); + int rw = Math.abs(rotated.getBlockX()); + int rh = Math.abs(rotated.getBlockY()); + int rd = Math.abs(rotated.getBlockZ()); + int hx = rw / 2; + int hy = rh / 2; + int hz = rd / 2; + return new PlacedStructurePiece(piece, object, x, y, z, rot, + x - hx, y - hy, z - hz, x + hx, y + hy, z + hz); + } + + private boolean withinRadius(PlacedStructurePiece p) { + return p.getMinX() >= originX - radius && p.getMaxX() <= originX + radius + && p.getMinZ() >= originZ - radius && p.getMaxZ() <= originZ + radius; + } + + private boolean collides(KList placed, PlacedStructurePiece candidate) { + for (PlacedStructurePiece p : placed) { + if (p.intersects(candidate)) { + return true; + } + } + return false; + } + + private int[] rotationCandidates(JigsawJoint joint, RNG rng) { + if (joint == JigsawJoint.ALIGNED) { + return Y_DEGREES; + } + int[] shuffled = {0, 90, 180, 270}; + for (int i = shuffled.length - 1; i > 0; i--) { + int j = rng.i(0, i); + int t = shuffled[i]; + shuffled[i] = shuffled[j]; + shuffled[j] = t; + } + return shuffled; + } + + private IrisDirection rotateDirection(IrisObjectRotation rot, IrisDirection direction) { + BlockVector v = direction.toVector().toBlockVector(); + BlockVector r = rot.rotate(v.clone(), 0, 0, 0); + return IrisDirection.getDirection(r); + } + + private BlockVector centerOf(IrisObject object) { + return new BlockVector(object.getW() / 2, object.getH() / 2, object.getD() / 2); + } + + private IrisJigsawPiece pickPiece(IrisJigsawPool pool, RNG rng) { + String name = weightedPick(pool, rng); + return name == null ? null : IrisData.loadAnyJigsawPiece(name, data); + } + + private String weightedPick(IrisJigsawPool pool, RNG rng) { + int total = 0; + for (IrisJigsawPieceEntry e : pool.getPieces()) { + total += Math.max(1, e.getWeight()); + } + if (total <= 0) { + return null; + } + int t = rng.i(0, total - 1); + for (IrisJigsawPieceEntry e : pool.getPieces()) { + t -= Math.max(1, e.getWeight()); + if (t < 0) { + return e.getPiece(); + } + } + return pool.getPieces().getFirst().getPiece(); + } + + private KList weightedOrder(IrisJigsawPool pool, RNG rng) { + KList remaining = new KList<>(pool.getPieces()); + KList order = new KList<>(); + while (!remaining.isEmpty()) { + int total = 0; + for (IrisJigsawPieceEntry e : remaining) { + total += Math.max(1, e.getWeight()); + } + int t = rng.i(0, total - 1); + int idx = 0; + for (int i = 0; i < remaining.size(); i++) { + t -= Math.max(1, remaining.get(i).getWeight()); + if (t < 0) { + idx = i; + break; + } + } + order.add(remaining.remove(idx).getPiece()); + } + return order; + } + + private record OpenConnector(int wx, int wy, int wz, IrisDirection facing, String pool, String name, + String targetName, JigsawJoint joint, int depth) { + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/StructurePlacementGrid.java b/core/src/main/java/art/arcane/iris/engine/framework/StructurePlacementGrid.java new file mode 100644 index 000000000..b78520637 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/StructurePlacementGrid.java @@ -0,0 +1,85 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 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 . + */ + +package art.arcane.iris.engine.framework; + +import art.arcane.iris.engine.object.IrisStructurePlacement; +import art.arcane.volmlib.util.math.RNG; + +public final class StructurePlacementGrid { + private StructurePlacementGrid() { + } + + public static boolean startsInChunk(IrisStructurePlacement placement, int cx, int cz, long seed, RNG chunkRng) { + return switch (placement.getDistribution()) { + case RANDOM_SPREAD -> randomSpreadStart(cx, cz, placement.getSpacing(), placement.getSeparation(), placement.getSalt(), seed); + case DENSITY -> chunkRng.chance(placement.getDensity()); + case CONCENTRIC_RINGS -> concentricRingsStart(cx, cz, placement, seed); + }; + } + + public static boolean randomSpreadStart(int cx, int cz, int spacing, int separation, int salt, long seed) { + int sp = Math.max(1, spacing); + int sep = Math.max(0, Math.min(separation, sp - 1)); + int cellX = Math.floorDiv(cx, sp); + int cellZ = Math.floorDiv(cz, sp); + RNG r = new RNG(mix(seed, cellX, cellZ, salt)); + int range = Math.max(1, sp - sep); + int startCx = cellX * sp + r.i(0, range - 1); + int startCz = cellZ * sp + r.i(0, range - 1); + return startCx == cx && startCz == cz; + } + + private static boolean concentricRingsStart(int cx, int cz, IrisStructurePlacement placement, long seed) { + if (cx == 0 && cz == 0) { + return false; + } + int distance = Math.max(1, placement.getRingDistance()); + int count = Math.max(1, placement.getRingCount()); + int spread = Math.max(1, placement.getRingSpread()); + double dist = Math.sqrt((double) cx * cx + (double) cz * cz); + int ring = (int) Math.round(dist / distance); + if (ring <= 0) { + return false; + } + int ringRadius = ring * distance; + int slots = Math.min(spread * ring, count); + if (slots <= 0) { + return false; + } + double slotAngle = (2 * Math.PI) / slots; + double offset = (mix(seed, ring, 0, 0) & 0xFFFFL) / 65536.0 * slotAngle; + double angle = Math.atan2(cz, cx); + int slotIndex = (int) Math.round((angle - offset) / slotAngle); + double idealAngle = offset + slotIndex * slotAngle; + int idealCx = (int) Math.round(Math.cos(idealAngle) * ringRadius); + int idealCz = (int) Math.round(Math.sin(idealAngle) * ringRadius); + return idealCx == cx && idealCz == cz; + } + + public static long mix(long seed, int a, int b, int salt) { + long h = seed; + h = h * 6364136223846793005L + (a * 341873128712L); + h = h * 6364136223846793005L + (b * 132897987541L); + h = h * 6364136223846793005L + salt; + h ^= (h >>> 33); + h *= 0xff51afd7ed558ccdL; + h ^= (h >>> 33); + return h; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java new file mode 100644 index 000000000..067f273cc --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java @@ -0,0 +1,168 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 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 . + */ + +package art.arcane.iris.engine.mantle.components; + +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.data.cache.Cache; +import art.arcane.iris.engine.framework.PlacedStructurePiece; +import art.arcane.iris.engine.framework.StructureAssembler; +import art.arcane.iris.engine.framework.StructurePlacementGrid; +import art.arcane.iris.engine.mantle.ComponentFlag; +import art.arcane.iris.engine.mantle.EngineMantle; +import art.arcane.iris.engine.mantle.IrisMantleComponent; +import art.arcane.iris.engine.mantle.MantleWriter; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisObjectPlacement; +import art.arcane.iris.engine.object.ObjectPlaceMode; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.object.IrisStructure; +import art.arcane.iris.engine.object.IrisStructurePlacement; +import art.arcane.iris.engine.object.StructurePlacementRoute; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.volmlib.util.documentation.ChunkCoordinates; +import art.arcane.volmlib.util.mantle.flag.ReservedFlag; +import art.arcane.volmlib.util.math.RNG; + +@ComponentFlag(ReservedFlag.JIGSAW) +public class IrisStructureComponent extends IrisMantleComponent { + public IrisStructureComponent(EngineMantle engineMantle) { + super(engineMantle, ReservedFlag.JIGSAW, 1); + } + + @Override + @ChunkCoordinates + public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) { + IrisComplex complex = context.getComplex(); + int xxx = 8 + (x << 4); + int zzz = 8 + (z << 4); + IrisRegion region = complex.getRegionStream().get(xxx, zzz); + IrisBiome biome = complex.getTrueBiomeStream().get(xxx, zzz); + RNG rng = new RNG(Cache.key(x, z) + seed()); + + KList placements = new KList<>(); + if (biome != null) { + placements.addAll(biome.getStructures()); + } + if (region != null) { + placements.addAll(region.getStructures()); + } + placements.addAll(getDimension().getStructures()); + + for (IrisStructurePlacement placement : placements) { + placeFromPlacement(writer, placement, x, z, rng); + } + } + + @ChunkCoordinates + private void placeFromPlacement(MantleWriter writer, IrisStructurePlacement placement, int cx, int cz, RNG rng) { + if (placement.getRoute() == StructurePlacementRoute.NATIVE_AT_POINT) { + return; + } + if (placement.getStructures().isEmpty()) { + return; + } + if (!StructurePlacementGrid.startsInChunk(placement, cx, cz, seed(), rng)) { + return; + } + + int sx = (cx << 4) + rng.i(0, 15); + int sz = (cz << 4) + rng.i(0, 15); + int surfaceY = getEngineMantle().getEngine().getHeight(sx, sz, true) + getEngineMantle().getEngine().getMinHeight(); + if (surfaceY < placement.getMinHeight() || surfaceY > placement.getMaxHeight()) { + return; + } + + String key = placement.getStructures().get(rng.i(0, placement.getStructures().size() - 1)); + IrisStructure structure = art.arcane.iris.core.loader.IrisData.loadAnyStructure(key, getData()); + if (structure == null) { + return; + } + + StructureAssembler assembler = new StructureAssembler(getData(), structure, sx, surfaceY, sz); + KList pieces = assembler.assemble(rng); + if (pieces == null || pieces.isEmpty()) { + return; + } + + ObjectPlaceMode mode = structure.getPlaceMode(); + if (mode == ObjectPlaceMode.STRUCTURE_PIECE || mode == ObjectPlaceMode.FLOATING) { + for (PlacedStructurePiece p : pieces) { + placeObject(writer, structure, p, ObjectPlaceMode.STRUCTURE_PIECE, p.getY(), rng); + } + } else if (pieces.size() == 1) { + placeObject(writer, structure, pieces.getFirst(), mode, -1, rng); + } else { + int lowest = Integer.MAX_VALUE; + for (PlacedStructurePiece p : pieces) { + lowest = Math.min(lowest, p.getMinY()); + } + int shift = surfaceY - lowest; + for (PlacedStructurePiece p : pieces) { + placeObject(writer, structure, p, ObjectPlaceMode.STRUCTURE_PIECE, p.getY() + shift, rng); + } + } + } + + private void placeObject(MantleWriter writer, IrisStructure structure, PlacedStructurePiece p, ObjectPlaceMode mode, int y, RNG rng) { + IrisObject object = p.getObject(); + IrisObjectPlacement config = new IrisObjectPlacement(); + config.setMode(mode); + config.setRotation(p.getRotation()); + config.getPlace().add(object.getLoadKey()); + if (!structure.getEdit().isEmpty()) { + config.setEdit(structure.getEdit()); + } + object.place(p.getX(), y, p.getZ(), writer, config, rng, null, null, getData()); + } + + @Override + protected int computeRadius() { + IrisDimension dimension = getDimension(); + int maxChunks = 0; + + for (IrisRegion region : dimension.getAllRegions(this::getData)) { + maxChunks = Math.max(maxChunks, maxFrom(region.getStructures())); + } + for (IrisBiome biome : dimension.getAllBiomes(this::getData)) { + maxChunks = Math.max(maxChunks, maxFrom(biome.getStructures())); + } + maxChunks = Math.max(maxChunks, maxFrom(dimension.getStructures())); + + return maxChunks * 16; + } + + private int maxFrom(KList placements) { + int max = 0; + for (IrisStructurePlacement placement : placements) { + if (placement.getRoute() == StructurePlacementRoute.NATIVE_AT_POINT) { + continue; + } + for (String key : placement.getStructures()) { + IrisStructure structure = art.arcane.iris.core.loader.IrisData.loadAnyStructure(key, getData()); + if (structure != null) { + max = Math.max(max, structure.getMaxSizeChunks()); + } + } + } + return max; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java index cd0d22c63..1337cdfdd 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java @@ -27,6 +27,8 @@ import art.arcane.iris.engine.mantle.ComponentFlag; import art.arcane.iris.engine.mantle.EngineMantle; import art.arcane.iris.engine.mantle.IrisMantleComponent; import art.arcane.iris.engine.mantle.MantleWriter; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.*; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; @@ -46,6 +48,7 @@ import art.arcane.iris.util.project.noise.NoiseType; import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import org.bukkit.util.BlockVector; +import org.bukkit.block.data.BlockData; import java.io.IOException; import java.util.Locale; @@ -111,7 +114,7 @@ public class MantleObjectComponent extends IrisMantleComponent { + " carvedAtCenter=" + carvedBlocks + " biomeCarvingObjects=" + caveBiome.getCarvingObjects().size() + " regionCarvingObjects=" + region.getCarvingObjects().size() - + " sameBiome=" + (caveBiome == surfaceBiome || caveBiome.getLoadKey().equals(surfaceBiome.getLoadKey()))); + + " sameBiome=" + (caveBiome == surfaceBiome || java.util.Objects.equals(caveBiome.getLoadKey(), surfaceBiome.getLoadKey()))); } if (traceRegen) { Iris.info("Regen object layer start: chunk=" + x + "," + z @@ -590,7 +593,9 @@ public class MantleObjectComponent extends IrisMantleComponent { AtomicBoolean wrotePlacementData = new AtomicBoolean(false); try { - int result = object.place(x, y, z, writer, effectivePlacement, rng, (b, data) -> { + int caveCeiling = findCaveCeiling(writer, x, y, z); + IObjectPlacer clampedPlacer = new CeilingClampedPlacer(writer, caveCeiling); + int result = object.place(x, y, z, clampedPlacer, effectivePlacement, rng, (b, data) -> { wrotePlacementData.set(true); String marker = placementMarker(object, id, "cave"); if (marker != null) { @@ -901,6 +906,104 @@ public class MantleObjectComponent extends IrisMantleComponent { return anchors.get(anchors.size() - 1); } + private int findCaveCeiling(MantleWriter writer, int x, int anchorY, int z) { + Engine engine = getEngineMantle().getEngine(); + int surfaceY = engine.getHeight(x, z); + int maxScan = Math.min(engine.getHeight() - 1, Math.max(0, surfaceY)); + for (int sy = anchorY + 1; sy <= maxScan; sy++) { + if (!writer.isCarved(x, sy, z)) { + return sy; + } + } + return maxScan; + } + + private static final class CeilingClampedPlacer implements IObjectPlacer { + private final IObjectPlacer delegate; + private final int maxY; + + private CeilingClampedPlacer(IObjectPlacer delegate, int maxY) { + this.delegate = delegate; + this.maxY = maxY; + } + + @Override + public int getHighest(int x, int z, IrisData data) { + return delegate.getHighest(x, z, data); + } + + @Override + public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) { + return delegate.getHighest(x, z, data, ignoreFluid); + } + + @Override + public void set(int x, int y, int z, BlockData d) { + if (y >= maxY) { + return; + } + delegate.set(x, y, z, d); + } + + @Override + public BlockData get(int x, int y, int z) { + return delegate.get(x, y, z); + } + + @Override + public boolean isPreventingDecay() { + return delegate.isPreventingDecay(); + } + + @Override + public boolean isCarved(int x, int y, int z) { + return delegate.isCarved(x, y, z); + } + + @Override + public boolean isSolid(int x, int y, int z) { + return delegate.isSolid(x, y, z); + } + + @Override + public boolean isUnderwater(int x, int z) { + return delegate.isUnderwater(x, z); + } + + @Override + public int getFluidHeight() { + return delegate.getFluidHeight(); + } + + @Override + public boolean isDebugSmartBore() { + return delegate.isDebugSmartBore(); + } + + @Override + public void setData(int xx, int yy, int zz, T data) { + delegate.setData(xx, yy, zz, data); + } + + @Override + public T getData(int xx, int yy, int zz, Class t) { + return delegate.getData(xx, yy, zz, t); + } + + @Override + public void setTile(int xx, int yy, int zz, TileData tile) { + if (yy >= maxY) { + return; + } + delegate.setTile(xx, yy, zz, tile); + } + + @Override + public Engine getEngine() { + return delegate.getEngine(); + } + } + private int findCaveAnchorY(MantleWriter writer, RNG rng, int x, int z, IrisCaveAnchorMode anchorMode, int anchorScanStep, int objectMinDepthBelowSurface, KMap> anchorCache) { long key = Cache.key(x, z); KList anchors = anchorCache.computeIfAbsent(key, (k) -> scanCaveAnchorColumn(writer, anchorMode, anchorScanStep, objectMinDepthBelowSurface, x, z)); diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java index c102d7cdd..6c6e25973 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java @@ -166,6 +166,9 @@ public class IrisBiome extends IrisRegistrant implements IRare { @ArrayType(min = 1, type = IrisObjectPlacement.class) @Desc("Objects define what schematics (iob files) iris will place in this biome") private KList objects = new KList<>(); + @ArrayType(min = 1, type = IrisStructurePlacement.class) + @Desc("Structures define jigsaw or vanilla/datapack structures iris will place in this biome") + private KList structures = new KList<>(); @ArrayType(min = 1, type = IrisFloatingChildBiomes.class) @Desc("Floating child biomes that procedurally generate above this biome's terrain. Each entry references a target biome whose layers, decorators, and objects drive the floating island's visual design, while the config here drives size, shape, altitude, rarity, and water level. Multiple entries are supported and selected by rarity per column unless mergeFloatingChildBiomes is enabled.") private KList floatingChildBiomes = new KList<>(); diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java index 08b2d9eb3..229b858f2 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java @@ -255,6 +255,11 @@ public class IrisDimension extends IrisRegistrant { @Desc("Collection of ores to be generated") @ArrayType(type = IrisOreGenerator.class, min = 1) private KList ores = new KList<>(); + @Desc("Dimension-wide structure placements (jigsaw or vanilla/datapack), independent of biome/region placements") + @ArrayType(type = IrisStructurePlacement.class, min = 1) + private KList structures = new KList<>(); + @Desc("Controls native vanilla & datapack structure generation for this dimension. Defaults to ALL_ON.") + private IrisVanillaStructureControl vanillaStructures = new IrisVanillaStructureControl(); @MinNumber(0) @MaxNumber(318) @Desc("The Subterrain Fluid Layer Height") @@ -585,7 +590,9 @@ public class IrisDimension extends IrisRegistrant { { "pack": { "description": "Iris Data Pack. This pack contains all installed Iris Packs' resources.", - "pack_format": {} + "pack_format": {}, + "min_format": {}, + "max_format": {} } } """.replace("{}", INMS.get().getDataVersion().getPackFormat() + ""); diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawConnector.java b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawConnector.java new file mode 100644 index 000000000..7eb65fb37 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawConnector.java @@ -0,0 +1,56 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 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 . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("A jigsaw connection point on a piece. When two connectors face each other and their names match, the assembler may join the pieces.") +@Data +public class IrisJigsawConnector { + @Required + @Desc("The position of this connector relative to the piece object's origin (0,0,0 is the lowest-corner of the object).") + private IrisPosition position = new IrisPosition(); + + @Required + @Desc("The direction this connector faces. The connecting piece is placed on this side.") + private IrisDirection direction = IrisDirection.NORTH_NEGATIVE_Z; + + @Required + @RegistryListResource(IrisJigsawPool.class) + @Desc("The jigsaw pool to draw the connecting piece from.") + private String pool = ""; + + @Desc("The name of this connector. A connector only joins to a connector on another piece whose targetName equals this name (and whose name equals this connector's targetName).") + private String name = ""; + + @Desc("The name this connector wants to connect to on the other piece.") + private String targetName = ""; + + @Desc("How the connecting piece may be rotated relative to this connector.") + private JigsawJoint joint = JigsawJoint.ROLLABLE; +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawPiece.java b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawPiece.java new file mode 100644 index 000000000..557d6cac5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawPiece.java @@ -0,0 +1,68 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 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 . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.json.JSONObject; +import art.arcane.iris.util.common.plugin.VolmitSender; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("A jigsaw piece. A piece is a single Iris object plus the connection points (connectors) that let the assembler attach other pieces to it.") +@Data +@EqualsAndHashCode(callSuper = false) +public class IrisJigsawPiece extends IrisRegistrant { + @Required + @RegistryListResource(IrisObject.class) + @Desc("The object (schematic) that makes up this piece.") + private String object = ""; + + @ArrayType(type = IrisJigsawConnector.class, min = 1) + @Desc("The jigsaw connection points on this piece.") + private KList connectors = new KList<>(); + + @Desc("If true, this piece may be rotated around the Y axis when placed so the assembler can match its connectors. Disable for pieces that must keep a fixed orientation.") + private boolean rotatable = true; + + @Override + public String getFolderName() { + return "jigsaw-pieces"; + } + + @Override + public String getTypeName() { + return "Jigsaw Piece"; + } + + @Override + public void scanForErrors(JSONObject p, VolmitSender sender) { + + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawPieceEntry.java b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawPieceEntry.java new file mode 100644 index 000000000..d7d821907 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawPieceEntry.java @@ -0,0 +1,44 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 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 . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("A weighted reference to a jigsaw piece inside a jigsaw pool.") +@Data +public class IrisJigsawPieceEntry { + @Required + @RegistryListResource(IrisJigsawPiece.class) + @Desc("The jigsaw piece this entry refers to.") + private String piece = ""; + + @MinNumber(1) + @Desc("The relative weight of this piece within its pool. Higher weights are chosen more often.") + private int weight = 1; +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawPool.java b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawPool.java new file mode 100644 index 000000000..577cf5cb6 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawPool.java @@ -0,0 +1,63 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 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 . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.json.JSONObject; +import art.arcane.iris.util.common.plugin.VolmitSender; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("A jigsaw pool. A pool is a weighted set of pieces the assembler chooses from when a connector targets this pool.") +@Data +@EqualsAndHashCode(callSuper = false) +public class IrisJigsawPool extends IrisRegistrant { + @ArrayType(type = IrisJigsawPieceEntry.class, min = 1) + @Desc("The weighted pieces in this pool.") + private KList pieces = new KList<>(); + + @RegistryListResource(IrisJigsawPool.class) + @Desc("The pool to fall back to when the structure's max depth is reached. Use this for terminal pieces (caps, dead ends). Leave empty to simply stop expanding.") + private String fallback = ""; + + @Override + public String getFolderName() { + return "jigsaw-pools"; + } + + @Override + public String getTypeName() { + return "Jigsaw Pool"; + } + + @Override + public void scanForErrors(JSONObject p, VolmitSender sender) { + + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java index 59afb8da4..640e81caf 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java @@ -595,6 +595,16 @@ public class IrisObject extends IrisRegistrant { } } + public void setUnsignedTile(int x, int y, int z, TileData tile) { + Vector3i v = getSigned(x, y, z); + + if (tile == null) { + states.remove(v); + } else { + states.put(v, tile); + } + } + public void setUnsigned(int x, int y, int z, Block block, boolean legacy) { Vector3i v = getSigned(x, y, z); diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java index 6d51cc6b5..1170853c3 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java @@ -92,6 +92,9 @@ public class IrisRegion extends IrisRegistrant implements IRare { @ArrayType(min = 1, type = IrisObjectPlacement.class) @Desc("Objects define what schematics (iob files) iris will place in this region") private KList objects = new KList<>(); + @ArrayType(min = 1, type = IrisStructurePlacement.class) + @Desc("Structures define jigsaw or vanilla/datapack structures iris will place in this region") + private KList structures = new KList<>(); @MinNumber(0) @Desc("The min shore height") private double shoreHeightMin = 1.2; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisStructure.java b/core/src/main/java/art/arcane/iris/engine/object/IrisStructure.java new file mode 100644 index 000000000..5a27f67bb --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisStructure.java @@ -0,0 +1,88 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 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 . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.json.JSONObject; +import art.arcane.iris.util.common.plugin.VolmitSender; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("A jigsaw structure. Starting from a pool, the assembler places a start piece and recursively attaches more pieces through matching connectors until it reaches the max depth or size. Pieces are Iris objects, so they carry the full object placement and block-control feature set.") +@Data +@EqualsAndHashCode(callSuper = false) +public class IrisStructure extends IrisRegistrant { + @Required + @RegistryListResource(IrisJigsawPool.class) + @Desc("The pool the assembler draws the start piece from.") + private String startPool = ""; + + @MinNumber(1) + @MaxNumber(30) + @Desc("The maximum jigsaw recursion depth. Larger values allow larger structures (e.g. villages) at the cost of generation time.") + private int maxDepth = 7; + + @MinNumber(1) + @MaxNumber(32) + @Desc("The maximum radius of the structure in chunks from its start piece. Acts as a hard bound on assembly size and collision checks.") + private int maxSizeChunks = 8; + + @Desc("The place mode used when stamping each piece object into the world.") + private ObjectPlaceMode placeMode = ObjectPlaceMode.STRUCTURE_PIECE; + + @ArrayType(min = 1, type = IrisObjectReplace.class) + @Desc("Find-and-replace block edits applied to every piece of this structure, using the exact same syntax as an object's 'edit'. For example, replace stone bricks with obsidian.") + private KList edit = new KList<>(); + + @ArrayType(type = String.class, min = 1) + @RegistryListResource(IrisLootTable.class) + @Desc("Loot tables applied to containers placed by this structure's pieces.") + private KList loot = new KList<>(); + + @Desc("If this structure was generated by importing a vanilla or datapack structure, this is that structure's key (provenance). Empty for hand-authored structures.") + private String vanillaSource = ""; + + @Override + public String getFolderName() { + return "structures"; + } + + @Override + public String getTypeName() { + return "Structure"; + } + + @Override + public void scanForErrors(JSONObject p, VolmitSender sender) { + + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisStructurePlacement.java b/core/src/main/java/art/arcane/iris/engine/object/IrisStructurePlacement.java new file mode 100644 index 000000000..e694d077f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisStructurePlacement.java @@ -0,0 +1,101 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 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 . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.RegistryListVanillaStructure; +import art.arcane.volmlib.util.collection.KList; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Attaches structures to a biome, region or dimension and controls where and how often they generate. This is independent of a structure's own native generation, so you can add custom placements in tandem with native generation or instead of it.") +@Data +public class IrisStructurePlacement { + @ArrayType(type = String.class, min = 1) + @RegistryListResource(IrisStructure.class) + @Desc("Iris structures to place here (route IRIS_PLACED). Use this for imported or hand-authored structures with full block control.") + private KList structures = new KList<>(); + + @ArrayType(type = String.class, min = 1) + @RegistryListVanillaStructure + @Desc("Vanilla or datapack structure keys to place here faithfully (route NATIVE_AT_POINT), e.g. minecraft:igloo. Real loot/mobs/variety, no block editing.") + private KList vanilla = new KList<>(); + + @Desc("How the placement is realized. Defaults sensibly by which list is populated, but you may set it explicitly.") + private StructurePlacementRoute route = StructurePlacementRoute.IRIS_PLACED; + + @Desc("How start positions are scattered.") + private StructureDistribution distribution = StructureDistribution.RANDOM_SPREAD; + + @MinNumber(1) + @MaxNumber(4096) + @Desc("RANDOM_SPREAD only: the grid cell size in chunks. One placement attempt per cell. Larger = rarer.") + private int spacing = 32; + + @MinNumber(0) + @Desc("RANDOM_SPREAD only: the minimum chunk separation between placements within the grid. Must be smaller than spacing.") + private int separation = 8; + + @Desc("RANDOM_SPREAD only: a salt mixed into the placement RNG so different structures using the same spacing do not stack.") + private int salt = 165745296; + + @MinNumber(0) + @MaxNumber(1) + @Desc("DENSITY only: the per-chunk probability (0-1) of a placement starting in a chunk.") + private double density = 0.02; + + @MinNumber(1) + @Desc("CONCENTRIC_RINGS only: the number of placements distributed across the rings.") + private int ringCount = 128; + + @MinNumber(1) + @Desc("CONCENTRIC_RINGS only: the ring spacing in chunks.") + private int ringDistance = 32; + + @MinNumber(1) + @Desc("CONCENTRIC_RINGS only: how many placements share each ring before moving outward.") + private int ringSpread = 3; + + @Desc("IRIS_PLACED only: rotation applied to the placed structure.") + private IrisObjectRotation rotation = new IrisObjectRotation(); + + @Desc("IRIS_PLACED only: translation applied to the placed structure.") + private IrisObjectTranslate translate = new IrisObjectTranslate(); + + @Desc("IRIS_PLACED only: scale applied to the placed structure.") + private IrisObjectScale scale = new IrisObjectScale(); + + @Desc("The minimum world Y this structure may start at.") + private int minHeight = -2032; + + @Desc("The maximum world Y this structure may start at.") + private int maxHeight = 2032; + + @Desc("If false, this placement is skipped underwater.") + private boolean underwater = false; +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisVanillaStructureControl.java b/core/src/main/java/art/arcane/iris/engine/object/IrisVanillaStructureControl.java new file mode 100644 index 000000000..ca5582108 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisVanillaStructureControl.java @@ -0,0 +1,74 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 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 . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.RegistryListVanillaStructure; +import art.arcane.volmlib.util.collection.KList; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Controls native vanilla & datapack structure generation for this dimension. The default mode is ALL_ON (everything generates).") +@Data +public class IrisVanillaStructureControl { + @Desc("Master toggle. ALL_ON generates every vanilla & datapack structure except those in 'disabled'. ALL_OFF (or CUSTOM) generates nothing except those in 'enabled'.") + private VanillaStructureMode mode = VanillaStructureMode.ALL_ON; + + @ArrayType(type = String.class, min = 1) + @RegistryListVanillaStructure + @Desc("Structure keys to turn OFF while mode is ALL_ON, e.g. 'minecraft:stronghold'. A namespace:path prefix also matches, so 'minecraft:village' disables every village.") + private KList disabled = new KList<>(); + + @ArrayType(type = String.class, min = 1) + @RegistryListVanillaStructure + @Desc("Structure keys to turn ON while mode is ALL_OFF (or CUSTOM), e.g. 'minecraft:village_plains'. A namespace:path prefix also matches.") + private KList enabled = new KList<>(); + + public boolean active() { + return mode == VanillaStructureMode.ALL_ON || !enabled.isEmpty(); + } + + public boolean shouldGenerate(String key) { + if (mode == VanillaStructureMode.ALL_ON) { + return !matches(disabled, key); + } + return matches(enabled, key); + } + + private boolean matches(KList list, String key) { + if (key == null) { + return false; + } + for (String entry : list) { + if (entry == null || entry.isEmpty()) { + continue; + } + if (key.equals(entry) || key.startsWith(entry)) { + return true; + } + } + return false; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/JigsawJoint.java b/core/src/main/java/art/arcane/iris/engine/object/JigsawJoint.java new file mode 100644 index 000000000..9d83a5369 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/JigsawJoint.java @@ -0,0 +1,30 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 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 . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("How a jigsaw connection may be rotated when matching two pieces together.") +public enum JigsawJoint { + @Desc("The connecting piece may rotate freely around the connection axis. Use this for vertical connectors (floors, ceilings) and any joint where roll does not matter.") + ROLLABLE, + + @Desc("The connecting piece's rotation is locked to face the connector. Use this for horizontal connectors where the attached piece must align to a specific facing (doorways, street ends).") + ALIGNED +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/LegacyTileData.java b/core/src/main/java/art/arcane/iris/engine/object/LegacyTileData.java index e8fd3a94c..a9ab67af6 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/LegacyTileData.java +++ b/core/src/main/java/art/arcane/iris/engine/object/LegacyTileData.java @@ -19,6 +19,8 @@ import org.bukkit.block.sign.Side; import org.bukkit.block.sign.SignSide; import org.bukkit.block.data.BlockData; import org.bukkit.entity.EntityType; +import org.bukkit.loot.LootTable; +import org.bukkit.loot.Lootable; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -37,7 +39,8 @@ public class LegacyTileData extends TileData { private static final Map>> legacy = Map.of( 0, new Pair<>(SignHandler::fromBukkit, SignHandler::new), 1, new Pair<>(SpawnerHandler::fromBukkit, SpawnerHandler::new), - 2, new Pair<>(BannerHandler::fromBukkit, BannerHandler::new)); + 2, new Pair<>(BannerHandler::fromBukkit, BannerHandler::new), + 3, new Pair<>(LootableHandler::fromBukkit, LootableHandler::new)); private static final AtomicCache> SIGNS = new AtomicCache<>(); private final int id; private final Handler handler; @@ -338,6 +341,75 @@ public class LegacyTileData extends TileData { } } + @ToString + @EqualsAndHashCode + @AllArgsConstructor + private static class LootableHandler implements Handler { + private final Material material; + private final NamespacedKey lootTable; + private final long seed; + + private LootableHandler(DataInputStream in) throws IOException { + String materialKey = in.readUTF(); + NamespacedKey resolvedMaterial = NamespacedKey.fromString(materialKey); + Material resolved = resolvedMaterial == null ? null : Registry.MATERIAL.get(resolvedMaterial); + if (resolved == null) { + resolved = Material.matchMaterial(materialKey); + } + material = resolved == null ? Material.CHEST : resolved; + String tableKey = in.readUTF(); + lootTable = tableKey.isEmpty() ? null : NamespacedKey.fromString(tableKey); + seed = in.readLong(); + } + + private static LootableHandler fromBukkit(BlockState blockState, Material type) { + if (!(blockState instanceof Lootable lootable)) { + return null; + } + LootTable table = lootable.getLootTable(); + if (table == null) { + return null; + } + NamespacedKey key = table.getKey(); + if (key == null) { + return null; + } + return new LootableHandler(type, key, lootable.getSeed()); + } + + @Override + public Material getMaterial() { + return material; + } + + @Override + public boolean isApplicable(BlockData data) { + return data.getMaterial() == material; + } + + @Override + public void toBinary(DataOutputStream out) throws IOException { + NamespacedKey key = KeyedType.getKey(material); + out.writeUTF(key == null ? material.name() : key.toString()); + out.writeUTF(lootTable == null ? "" : lootTable.toString()); + out.writeLong(seed); + } + + @Override + public void toBukkit(Block block) { + if (lootTable == null || !(block.getState() instanceof Lootable lootable)) { + return; + } + LootTable table = Bukkit.getLootTable(lootTable); + if (table == null) { + return; + } + lootable.setLootTable(table); + lootable.setSeed(seed); + ((BlockState) lootable).update(true, false); + } + } + private static Tag signsTag() { return SIGNS.aquire(() -> { var signs = Bukkit.getTag("blocks", NamespacedKey.minecraft("all_signs"), Material.class); diff --git a/core/src/main/java/art/arcane/iris/engine/object/StructureDistribution.java b/core/src/main/java/art/arcane/iris/engine/object/StructureDistribution.java new file mode 100644 index 000000000..ff396886c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/StructureDistribution.java @@ -0,0 +1,33 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 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 . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Controls how structure start positions are scattered across the world.") +public enum StructureDistribution { + @Desc("Vanilla-style spacing/separation grid. One placement attempt per grid cell, randomly offset within it by the separation. Use spacing, separation and salt.") + RANDOM_SPREAD, + + @Desc("A per-chunk probability roll, like object density. Use density (0-1).") + DENSITY, + + @Desc("Concentric rings around the world origin, like vanilla strongholds. Use rings.") + CONCENTRIC_RINGS +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/StructurePlacementRoute.java b/core/src/main/java/art/arcane/iris/engine/object/StructurePlacementRoute.java new file mode 100644 index 000000000..a3a80bd44 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/StructurePlacementRoute.java @@ -0,0 +1,30 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 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 . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("How a custom structure placement is realized in the world.") +public enum StructurePlacementRoute { + @Desc("Place an imported or hand-authored Iris structure through the object pipeline. Full block & content control. References structures (IrisStructure keys).") + IRIS_PLACED, + + @Desc("Invoke the real vanilla/datapack structure generation at the chosen coordinates (faithful loot, mobs, jigsaw variety). No Iris block editing. References vanilla (structure keys).") + NATIVE_AT_POINT +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/VanillaStructureMode.java b/core/src/main/java/art/arcane/iris/engine/object/VanillaStructureMode.java new file mode 100644 index 000000000..8659df8a6 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/VanillaStructureMode.java @@ -0,0 +1,33 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 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 . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Master toggle for vanilla & datapack structure generation in a dimension.") +public enum VanillaStructureMode { + @Desc("All vanilla & datapack structures generate normally. This is the default. Per-set overrides may still disable individual sets.") + ALL_ON, + + @Desc("All vanilla & datapack structures are suppressed. Per-set overrides may still enable individual sets.") + ALL_OFF, + + @Desc("Nothing generates unless explicitly enabled via a per-set override with enabled=true.") + CUSTOM +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListVanillaStructure.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListVanillaStructure.java new file mode 100644 index 000000000..8d0a2e8f4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListVanillaStructure.java @@ -0,0 +1,31 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 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 . + */ + +package art.arcane.iris.engine.object.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({PARAMETER, TYPE, FIELD}) +public @interface RegistryListVanillaStructure { + +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListVanillaStructureSet.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListVanillaStructureSet.java new file mode 100644 index 000000000..fb46765df --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListVanillaStructureSet.java @@ -0,0 +1,31 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 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 . + */ + +package art.arcane.iris.engine.object.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({PARAMETER, TYPE, FIELD}) +public @interface RegistryListVanillaStructureSet { + +} diff --git a/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java b/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java index c1c15b4ae..e73b8e3f3 100644 --- a/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java @@ -33,6 +33,7 @@ import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.EngineTarget; import art.arcane.iris.engine.framework.GenerationSessionException; import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.VanillaStructureMode; import art.arcane.iris.engine.object.IrisWorld; import art.arcane.iris.engine.object.StudioMode; import art.arcane.iris.engine.platform.studio.StudioGenerator; @@ -816,7 +817,15 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun @Override public boolean shouldGenerateStructures() { - return false; + try { + Engine e = this.engine; + if (e == null) { + return true; + } + return e.getDimension().getVanillaStructures().getMode() != VanillaStructureMode.ALL_OFF; + } catch (Throwable t) { + return true; + } } @Override diff --git a/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleStagingTest.java b/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleStagingTest.java index ba69f8983..c6ba83a0a 100644 --- a/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleStagingTest.java +++ b/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleStagingTest.java @@ -30,6 +30,17 @@ public class WorldLifecycleStagingTest { assertSame(generator, WorldLifecycleStaging.consumeStemGenerator("world")); } + @Test + public void stagedStemGeneratorIsConsumedWhenLevelIdDiffersFromWorldName() { + ChunkGenerator generator = mock(ChunkGenerator.class); + + WorldLifecycleStaging.stageStemGenerator("iris_world", generator); + + assertSame(generator, WorldLifecycleStaging.consumeStemGenerator("world")); + assertNull(WorldLifecycleStaging.consumeStemGenerator("iris_world")); + assertNull(WorldLifecycleStaging.consumeStemGenerator("world")); + } + @Test public void clearAllRemovesGeneratorBiomeAndStemState() { ChunkGenerator generator = mock(ChunkGenerator.class); diff --git a/core/src/test/java/art/arcane/iris/engine/framework/StructurePlacementGridTest.java b/core/src/test/java/art/arcane/iris/engine/framework/StructurePlacementGridTest.java new file mode 100644 index 000000000..74c339882 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/framework/StructurePlacementGridTest.java @@ -0,0 +1,104 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 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 . + */ + +package art.arcane.iris.engine.framework; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +public class StructurePlacementGridTest { + @Test + public void randomSpreadStartIsDeterministic() { + boolean first = StructurePlacementGrid.randomSpreadStart(5, 7, 32, 8, 165745296, 12345L); + boolean second = StructurePlacementGrid.randomSpreadStart(5, 7, 32, 8, 165745296, 12345L); + assertEquals(first, second); + } + + @Test + public void randomSpreadStartYieldsExactlyOneStartPerGridCell() { + int spacing = 32; + int separation = 8; + int salt = 165745296; + long seed = 99L; + int count = 0; + for (int cx = 0; cx < spacing; cx++) { + for (int cz = 0; cz < spacing; cz++) { + if (StructurePlacementGrid.randomSpreadStart(cx, cz, spacing, separation, salt, seed)) { + count++; + } + } + } + assertEquals(1, count); + } + + @Test + public void randomSpreadStartVariesAcrossCells() { + int spacing = 32; + int separation = 8; + int salt = 165745296; + long seed = 99L; + int starts = 0; + for (int cellX = 0; cellX < 4; cellX++) { + for (int cellZ = 0; cellZ < 4; cellZ++) { + for (int x = 0; x < spacing; x++) { + for (int z = 0; z < spacing; z++) { + int cx = cellX * spacing + x; + int cz = cellZ * spacing + z; + if (StructurePlacementGrid.randomSpreadStart(cx, cz, spacing, separation, salt, seed)) { + starts++; + } + } + } + } + } + assertEquals(16, starts); + } + + @Test + public void mixIsDeterministicAndSensitiveToInputs() { + assertEquals(StructurePlacementGrid.mix(1L, 2, 3, 4), StructurePlacementGrid.mix(1L, 2, 3, 4)); + assertNotEquals(StructurePlacementGrid.mix(1L, 2, 3, 4), StructurePlacementGrid.mix(1L, 2, 3, 5)); + assertNotEquals(StructurePlacementGrid.mix(1L, 2, 3, 4), StructurePlacementGrid.mix(2L, 2, 3, 4)); + } + + @Test + public void differentSaltsShiftStartPositions() { + int spacing = 32; + int separation = 8; + long seed = 7L; + int matches = 0; + int total = 0; + for (int cx = 0; cx < spacing; cx++) { + for (int cz = 0; cz < spacing; cz++) { + boolean a = StructurePlacementGrid.randomSpreadStart(cx, cz, spacing, separation, 10387312, seed); + boolean b = StructurePlacementGrid.randomSpreadStart(cx, cz, spacing, separation, 165745296, seed); + if (a) { + total++; + } + if (a && b) { + matches++; + } + } + } + assertEquals(1, total); + assertTrue(matches <= total); + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c7de7aa3b..60718ad68 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ grgit = "5.3.2" # https://github.com/ajoberstar/grgit # Core Libraries lombok = "1.18.44" -spigot = "1.21.11-R0.1-SNAPSHOT" # https://hub.spigotmc.org/nexus/repository/snapshots/org/spigotmc/spigot-api/maven-metadata.xml +spigot = "26.1.2-R0.1-SNAPSHOT" # https://hub.spigotmc.org/nexus/repository/snapshots/org/spigotmc/spigot-api/maven-metadata.xml log4j = "2.19.0" # https://central.sonatype.com/artifact/org.apache.logging.log4j/log4j-api adventure-api = "4.24.0" # https://github.com/KyoriPowered/adventure adventure-platform = "4.4.1" # https://github.com/KyoriPowered/adventure-platform diff --git a/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/IrisChunkGenerator.java b/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/IrisChunkGenerator.java index 6cd1e5655..2ede3548a 100644 --- a/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/IrisChunkGenerator.java +++ b/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/IrisChunkGenerator.java @@ -2,10 +2,13 @@ package art.arcane.iris.core.nms.v1_21_R7; import com.mojang.datafixers.util.Pair; import com.mojang.serialization.MapCodec; +import art.arcane.iris.Iris; import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisVanillaStructureControl; import art.arcane.iris.util.common.reflect.WrappedField; import art.arcane.iris.util.common.reflect.WrappedReturningMethod; import net.minecraft.core.*; +import net.minecraft.resources.Identifier; import net.minecraft.resources.ResourceKey; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.WorldGenRegion; @@ -23,7 +26,19 @@ import net.minecraft.world.level.levelgen.*; import net.minecraft.world.level.levelgen.blending.Blender; import net.minecraft.world.level.levelgen.structure.Structure; import net.minecraft.world.level.levelgen.structure.StructureSet; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; +import net.minecraft.core.registries.Registries; +import java.util.stream.Collectors; +import art.arcane.iris.engine.framework.StructurePlacementGrid; +import art.arcane.iris.engine.object.IrisStructurePlacement; +import art.arcane.iris.engine.object.StructurePlacementRoute; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.data.cache.Cache; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; import org.bukkit.World; import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.craftbukkit.generator.CustomChunkGenerator; @@ -49,7 +64,10 @@ public class IrisChunkGenerator extends CustomChunkGenerator { @Override public @Nullable Pair> findNearestMapStructure(ServerLevel level, HolderSet holders, BlockPos pos, int radius, boolean findUnexplored) { - return null; + if (!vanillaControl().active()) { + return null; + } + return delegate.findNearestMapStructure(level, holders, pos, radius, findUnexplored); } @Override @@ -76,6 +94,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator { @Override public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess access, StructureTemplateManager templateManager, ResourceKey levelKey) { + if (!vanillaControl().active()) { + return; + } + delegate.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey); + } + + private IrisVanillaStructureControl vanillaControl() { + return engine.getDimension().getVanillaStructures(); } @Override @@ -126,9 +152,121 @@ public class IrisChunkGenerator extends CustomChunkGenerator { @Override public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla) { addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager); + if (vanillaControl().active()) { + placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager); + } + placeIrisNativeStructures(generatoraccessseed, ichunkaccess); delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false); } + private void placeVanillaStructures(WorldGenLevel world, ChunkAccess chunk, StructureManager structureManager) { + if (!structureManager.shouldGenerateStructures()) { + return; + } + ChunkPos chunkPos = chunk.getPos(); + SectionPos sectionPos = SectionPos.of(chunkPos, world.getMinSectionY()); + BlockPos origin = sectionPos.origin(); + Registry registry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE); + Map> byStep = registry.stream().collect(Collectors.groupingBy(s -> s.step().ordinal())); + WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed())); + long decoSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ()); + BoundingBox area = writableArea(chunk); + int steps = GenerationStep.Decoration.values().length; + IrisVanillaStructureControl control = vanillaControl(); + for (int step = 0; step < steps; step++) { + int index = 0; + for (Structure structure : byStep.getOrDefault(step, List.of())) { + Object id = registry.getKey(structure); + if (control.shouldGenerate(id == null ? null : id.toString())) { + random.setFeatureSeed(decoSeed, index, step); + try { + structureManager.startsForStructure(sectionPos, structure) + .forEach(start -> start.placeInChunk(world, structureManager, this, random, area, chunkPos)); + } catch (Throwable e) { + Iris.reportError(e); + } + } + index++; + } + } + } + + private BoundingBox writableArea(ChunkAccess chunk) { + ChunkPos cp = chunk.getPos(); + int i = cp.getMinBlockX(); + int j = cp.getMinBlockZ(); + int minY = getMinY() + 1; + int maxY = getMinY() + engine.getHeight() - 1; + return new BoundingBox(i, minY, j, i + 15, maxY, j + 15); + } + + private void placeIrisNativeStructures(WorldGenLevel world, ChunkAccess chunk) { + if (!(world instanceof WorldGenRegion region)) { + return; + } + ChunkPos chunkPos = chunk.getPos(); + int cx = chunkPos.getMinBlockX() >> 4; + int cz = chunkPos.getMinBlockZ() >> 4; + int bx = 8 + (cx << 4); + int bz = 8 + (cz << 4); + IrisBiome biome = engine.getComplex().getTrueBiomeStream().get(bx, bz); + IrisRegion ireg = engine.getComplex().getRegionStream().get(bx, bz); + KList placements = new KList<>(); + if (biome != null) { + placements.addAll(biome.getStructures()); + } + if (ireg != null) { + placements.addAll(ireg.getStructures()); + } + placements.addAll(engine.getDimension().getStructures()); + if (placements.isEmpty()) { + return; + } + + long seed = world.getSeed(); + RNG rng = new RNG(Cache.key(cx, cz) + seed); + ServerLevel level = region.getLevel(); + Registry registry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE); + RandomState randomState = level.getChunkSource().randomState(); + StructureTemplateManager templateManager = level.getStructureManager(); + StructureManager structureManager = level.structureManager(); + BiomeSource biomeSource = getBiomeSource(); + ResourceKey levelKey = level.dimension(); + int minY = getMinY() + 1; + int maxY = getMinY() + engine.getHeight() - 1; + + for (IrisStructurePlacement placement : placements) { + if (placement.getRoute() != StructurePlacementRoute.NATIVE_AT_POINT || placement.getVanilla().isEmpty()) { + continue; + } + if (!StructurePlacementGrid.startsInChunk(placement, cx, cz, seed, rng)) { + continue; + } + String key = placement.getVanilla().get(rng.i(0, placement.getVanilla().size() - 1)).toLowerCase(); + try { + String[] parts = key.split(":", 2); + Identifier id = parts.length > 1 ? Identifier.fromNamespaceAndPath(parts[0], parts[1]) : Identifier.fromNamespaceAndPath("minecraft", parts[0]); + Holder holder = registry.getOrThrow(ResourceKey.create(Registries.STRUCTURE, id)); + Structure structure = holder.value(); + StructureStart start = structure.generate(holder, levelKey, world.registryAccess(), this, biomeSource, randomState, templateManager, seed, chunkPos, 0, level, b -> true); + if (start == null || !start.isValid()) { + continue; + } + BoundingBox box = start.getBoundingBox(); + WorldgenRandom placeRandom = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed())); + for (int scx = box.minX() >> 4; scx <= box.maxX() >> 4; scx++) { + for (int scz = box.minZ() >> 4; scz <= box.maxZ() >> 4; scz++) { + ChunkPos target = new ChunkPos(scx, scz); + BoundingBox chunkBox = new BoundingBox(target.getMinBlockX(), minY, target.getMinBlockZ(), target.getMaxBlockX(), maxY, target.getMaxBlockZ()); + start.placeInChunk(world, structureManager, this, placeRandom, chunkBox, target); + } + } + } catch (Throwable e) { + Iris.reportError(e); + } + } + } + @Override public void addVanillaDecorations(WorldGenLevel level, ChunkAccess chunkAccess, StructureManager structureManager) { SectionPos sectionPos = SectionPos.of(chunkAccess.getPos(), level.getMinSectionY()); diff --git a/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/NMSBinding.java b/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/NMSBinding.java index 5ab3a2d49..b5b835a6e 100644 --- a/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/NMSBinding.java +++ b/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/NMSBinding.java @@ -61,7 +61,6 @@ import net.minecraft.world.level.levelgen.FlatLevelSource; import net.minecraft.world.level.levelgen.flat.FlatLayerInfo; import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings; import net.minecraft.world.level.storage.LevelStorageSource; -import net.minecraft.world.level.storage.ServerLevelData; import org.bukkit.*; import org.bukkit.block.Biome; import org.bukkit.block.data.BlockData; @@ -391,6 +390,28 @@ public class NMSBinding implements INMSBinding { return biomes; } + @Override + public KList getStructureKeys() { + KList keys = new KList<>(); + try { + registry().lookupOrThrow(Registries.STRUCTURE).keySet().forEach(k -> keys.add(k.toString())); + } catch (Throwable e) { + Iris.reportError(e); + } + return keys; + } + + @Override + public KList getStructureSetKeys() { + KList keys = new KList<>(); + try { + registry().lookupOrThrow(Registries.STRUCTURE_SET).keySet().forEach(k -> keys.add(k.toString())); + } catch (Throwable e) { + Iris.reportError(e); + } + return keys; + } + @Override public boolean isBukkit() { return true; @@ -731,9 +752,9 @@ public class NMSBinding implements INMSBinding { Iris.info("Injecting Bukkit"); var buddy = new ByteBuddy(); buddy.redefine(ServerLevel.class) - .visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor().and(ElementMatchers.takesArguments( - MinecraftServer.class, Executor.class, LevelStorageSource.LevelStorageAccess.class, ServerLevelData.class, - ResourceKey.class, LevelStem.class, boolean.class, long.class, List.class, boolean.class)))) + .visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor() + .and(ElementMatchers.takesArgument(0, MinecraftServer.class)) + .and(ElementMatchers.takesArgument(5, LevelStem.class)))) .make() .load(ServerLevel.class.getClassLoader(), Agent.installed()); for (Class clazz : List.of(ChunkAccess.class, ProtoChunk.class)) { @@ -807,8 +828,7 @@ public class NMSBinding implements INMSBinding { static void enter( @Advice.Argument(0) MinecraftServer server, @Advice.Argument(2) LevelStorageSource.LevelStorageAccess levelStorageAccess, - @Advice.Argument(value = 5, readOnly = false) LevelStem levelStem, - @Advice.Argument(3) ServerLevelData levelData + @Advice.Argument(value = 5, readOnly = false) LevelStem levelStem ) { if (levelStorageAccess == null) return; @@ -833,11 +853,21 @@ public class NMSBinding implements INMSBinding { .getClassLoader()) .getDeclaredMethod("get") .invoke(null); - if (!(bindings instanceof INMSBinding binding)) { + if (bindings == null) { throw new IllegalStateException("Iris failed to resolve an INMSBinding instance."); } - Object resolvedStem = binding.createRuntimeLevelStem(server.registryAccess(), gen); + java.lang.reflect.Method stemMethod = null; + for (java.lang.reflect.Method candidate : bindings.getClass().getMethods()) { + if (candidate.getName().equals("createRuntimeLevelStem") && candidate.getParameterCount() == 2) { + stemMethod = candidate; + break; + } + } + if (stemMethod == null) { + throw new IllegalStateException("Iris binding is missing createRuntimeLevelStem."); + } + Object resolvedStem = stemMethod.invoke(bindings, server.registryAccess(), gen); if (!(resolvedStem instanceof LevelStem runtimeStem)) { throw new IllegalStateException("Iris runtime LevelStem binding returned " + (resolvedStem == null ? "null" : resolvedStem.getClass().getName()) + "."); } diff --git a/nms/v26_1_R1/src/main/java/art/arcane/iris/core/nms/v26_1_R1/CustomBiomeSource.java b/nms/v26_1_R1/src/main/java/art/arcane/iris/core/nms/v26_1_R1/CustomBiomeSource.java new file mode 100644 index 000000000..c49725dbf --- /dev/null +++ b/nms/v26_1_R1/src/main/java/art/arcane/iris/core/nms/v26_1_R1/CustomBiomeSource.java @@ -0,0 +1,331 @@ +package art.arcane.iris.core.nms.v26_1_R1; + +import com.mojang.serialization.MapCodec; +import art.arcane.iris.Iris; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisBiomeCustom; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.math.RNG; +import net.minecraft.core.Holder; +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.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.biome.Climate; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.craftbukkit.CraftServer; +import org.bukkit.craftbukkit.CraftWorld; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.concurrent.ConcurrentHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +public class CustomBiomeSource extends BiomeSource { + private static final int NOISE_BIOME_CACHE_MAX = 262144; + + private final long seed; + private final Engine engine; + private final Registry biomeCustomRegistry; + private final Registry biomeRegistry; + private final AtomicCache registryAccess = new AtomicCache<>(); + private final KMap> customBiomes; + private final Holder fallbackBiome; + private final ConcurrentHashMap> noiseBiomeCache = new ConcurrentHashMap<>(); + + public CustomBiomeSource(long seed, Engine engine, World world) { + this.engine = engine; + this.seed = seed; + this.biomeCustomRegistry = registry().lookup(Registries.BIOME).orElse(null); + this.biomeRegistry = ((RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())).lookup(Registries.BIOME).orElse(null); + this.fallbackBiome = resolveFallbackBiome(this.biomeRegistry, this.biomeCustomRegistry); + this.customBiomes = fillCustomBiomes(this.biomeCustomRegistry, engine, this.fallbackBiome); + } + + private static List> getAllBiomes(Registry customRegistry, Registry registry, Engine engine, Holder fallback) { + LinkedHashSet> biomes = new LinkedHashSet<>(); + if (fallback != null) { + biomes.add(fallback); + } + + for (IrisBiome i : engine.getAllBiomes()) { + if (i.isCustom()) { + for (IrisBiomeCustom j : i.getCustomDerivitives()) { + Holder customHolder = resolveCustomBiomeHolder(customRegistry, engine, j.getId()); + if (customHolder != null) { + biomes.add(customHolder); + } else if (fallback != null) { + biomes.add(fallback); + } + } + } else { + Holder vanillaHolder = NMSBinding.biomeToBiomeBase(registry, i.getVanillaDerivative()); + if (vanillaHolder != null) { + biomes.add(vanillaHolder); + } else if (fallback != null) { + biomes.add(fallback); + } + } + } + + return new ArrayList<>(biomes); + } + + private static Object getFor(Class type, Object source) { + Object o = fieldFor(type, source); + + if (o != null) { + return o; + } + + return invokeFor(type, source); + } + + private static Object fieldFor(Class returns, Object in) { + return fieldForClass(returns, in.getClass(), in); + } + + private static Object invokeFor(Class returns, Object in) { + for (Method i : in.getClass().getMethods()) { + if (i.getReturnType().equals(returns)) { + i.setAccessible(true); + try { + Iris.debug("[NMS] Found " + returns.getSimpleName() + " in " + in.getClass().getSimpleName() + "." + i.getName() + "()"); + return i.invoke(in); + } catch (Throwable e) { + e.printStackTrace(); + } + } + } + + return null; + } + + @SuppressWarnings("unchecked") + private static T fieldForClass(Class returnType, Class sourceType, Object in) { + for (Field i : sourceType.getDeclaredFields()) { + if (i.getType().equals(returnType)) { + i.setAccessible(true); + try { + Iris.debug("[NMS] Found " + returnType.getSimpleName() + " in " + sourceType.getSimpleName() + "." + i.getName()); + return (T) i.get(in); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + } + return null; + } + + @Override + protected Stream> collectPossibleBiomes() { + return getAllBiomes( + ((RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())) + .lookup(Registries.BIOME).orElse(null), + ((CraftWorld) engine.getWorld().realWorld()).getHandle().registryAccess().lookup(Registries.BIOME).orElse(null), + engine, + fallbackBiome).stream(); + } + + private KMap> fillCustomBiomes(Registry customRegistry, Engine engine, Holder fallback) { + KMap> m = new KMap<>(); + if (customRegistry == null) { + return m; + } + + for (IrisBiome i : engine.getAllBiomes()) { + if (i.isCustom()) { + for (IrisBiomeCustom j : i.getCustomDerivitives()) { + Holder holder = resolveCustomBiomeHolder(customRegistry, engine, j.getId()); + if (holder == null) { + if (fallback != null) { + m.put(j.getId(), fallback); + } + Iris.error("Cannot find biome for IrisBiomeCustom " + j.getId() + " from engine " + engine.getName()); + continue; + } + m.put(j.getId(), holder); + } + } + } + + return m; + } + + private RegistryAccess registry() { + return registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())); + } + + @Override + protected MapCodec codec() { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Holder getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) { + long cacheKey = packNoiseKey(x, y, z); + Holder cachedHolder = noiseBiomeCache.get(cacheKey); + if (cachedHolder != null) { + return cachedHolder; + } + + Holder resolvedHolder = resolveNoiseBiomeHolder(x, y, z); + Holder existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder); + if (existingHolder != null) { + return existingHolder; + } + + if (noiseBiomeCache.size() > NOISE_BIOME_CACHE_MAX) { + noiseBiomeCache.clear(); + } + + return resolvedHolder; + } + + private Holder resolveNoiseBiomeHolder(int x, int y, int z) { + if (engine == null || engine.isClosed()) { + return getFallbackBiome(); + } + + if (engine.getComplex() == null) { + return getFallbackBiome(); + } + + int blockX = x << 2; + int blockZ = z << 2; + int blockY = y << 2; + int worldMinHeight = engine.getWorld().minHeight(); + int internalY = blockY - worldMinHeight; + int surfaceInternalY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue(); + int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40); + boolean deepUnderground = internalY <= caveSwitchInternalY; + boolean belowSurface = internalY <= surfaceInternalY - 8; + boolean underground = deepUnderground && belowSurface; + IrisBiome irisBiome = underground + ? engine.getCaveBiome(blockX, internalY, blockZ) + : engine.getComplex().getTrueBiomeStream().get(blockX, blockZ); + if (irisBiome == null && underground) { + irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ); + } + if (irisBiome == null) { + return getFallbackBiome(); + } + + RNG noiseRng = new RNG(seed + ^ (((long) blockX) * 341873128712L) + ^ (((long) blockY) * 132897987541L) + ^ (((long) blockZ) * 42317861L)); + + if (irisBiome.isCustom()) { + IrisBiomeCustom customBiome = irisBiome.getCustomBiome(noiseRng, blockX, blockY, blockZ); + if (customBiome != null) { + Holder holder = customBiomes.get(customBiome.getId()); + if (holder != null) { + return holder; + } + } + + return getFallbackBiome(); + } + + org.bukkit.block.Biome vanillaBiome = underground + ? irisBiome.getGroundBiome(noiseRng, blockX, blockY, blockZ) + : irisBiome.getSkyBiome(noiseRng, blockX, blockY, blockZ); + Holder holder = NMSBinding.biomeToBiomeBase(biomeRegistry, vanillaBiome); + if (holder != null) { + return holder; + } + + return getFallbackBiome(); + } + + private Holder getFallbackBiome() { + if (fallbackBiome != null) { + return fallbackBiome; + } + + Holder holder = resolveFallbackBiome(biomeRegistry, biomeCustomRegistry); + if (holder != null) { + return holder; + } + + throw new IllegalStateException("Unable to resolve any biome holder fallback for Iris biome source"); + } + + private static long packNoiseKey(int x, int y, int z) { + return (((long) x & 67108863L) << 38) + | (((long) z & 67108863L) << 12) + | ((long) y & 4095L); + } + + private static Holder resolveCustomBiomeHolder(Registry customRegistry, Engine engine, String customBiomeId) { + if (customRegistry == null || engine == null || customBiomeId == null || customBiomeId.isBlank()) { + return null; + } + + Identifier resourceLocation = Identifier.fromNamespaceAndPath( + engine.getDimension().getLoadKey().toLowerCase(java.util.Locale.ROOT), + customBiomeId.toLowerCase(java.util.Locale.ROOT) + ); + Biome biome = customRegistry.getValue(resourceLocation); + if (biome == null) { + return null; + } + + Optional> optionalBiomeKey = customRegistry.getResourceKey(biome); + if (optionalBiomeKey.isEmpty()) { + return null; + } + + Optional> optionalReferenceHolder = customRegistry.get(optionalBiomeKey.get()); + if (optionalReferenceHolder.isEmpty()) { + return null; + } + + return optionalReferenceHolder.get(); + } + + private static Holder resolveFallbackBiome(Registry registry, Registry customRegistry) { + Holder plains = NMSBinding.biomeToBiomeBase(registry, org.bukkit.block.Biome.PLAINS); + if (plains != null) { + return plains; + } + + Holder vanilla = firstHolder(registry); + if (vanilla != null) { + return vanilla; + } + + return firstHolder(customRegistry); + } + + private static Holder firstHolder(Registry registry) { + if (registry == null) { + return null; + } + + for (Biome biome : registry) { + Optional> optionalBiomeKey = registry.getResourceKey(biome); + if (optionalBiomeKey.isEmpty()) { + continue; + } + + Optional> optionalHolder = registry.get(optionalBiomeKey.get()); + if (optionalHolder.isPresent()) { + return optionalHolder.get(); + } + } + + return null; + } +} diff --git a/nms/v26_1_R1/src/main/java/art/arcane/iris/core/nms/v26_1_R1/IrisChunkGenerator.java b/nms/v26_1_R1/src/main/java/art/arcane/iris/core/nms/v26_1_R1/IrisChunkGenerator.java new file mode 100644 index 000000000..63cdde686 --- /dev/null +++ b/nms/v26_1_R1/src/main/java/art/arcane/iris/core/nms/v26_1_R1/IrisChunkGenerator.java @@ -0,0 +1,378 @@ +package art.arcane.iris.core.nms.v26_1_R1; + +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.MapCodec; +import art.arcane.iris.Iris; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisVanillaStructureControl; +import art.arcane.iris.util.common.reflect.WrappedField; +import art.arcane.iris.util.common.reflect.WrappedReturningMethod; +import net.minecraft.core.*; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.util.random.WeightedList; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.level.*; +import net.minecraft.world.level.biome.*; +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.status.ChunkStatus; +import net.minecraft.world.level.levelgen.*; +import net.minecraft.world.level.levelgen.blending.Blender; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureSet; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; +import net.minecraft.core.registries.Registries; +import java.util.stream.Collectors; +import art.arcane.iris.engine.framework.StructurePlacementGrid; +import art.arcane.iris.engine.object.IrisStructurePlacement; +import art.arcane.iris.engine.object.StructurePlacementRoute; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.data.cache.Cache; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import org.bukkit.World; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.craftbukkit.generator.CustomChunkGenerator; +import org.spigotmc.SpigotWorldConfig; + +import javax.annotation.Nullable; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.*; +import java.util.concurrent.CompletableFuture; + +public class IrisChunkGenerator extends CustomChunkGenerator { + private static final WrappedField BIOME_SOURCE; + private static final WrappedReturningMethod SET_HEIGHT; + private final ChunkGenerator delegate; + private final Engine engine; + + public IrisChunkGenerator(ChunkGenerator delegate, long seed, Engine engine, World world) { + super(((CraftWorld) world).getHandle(), edit(delegate, new CustomBiomeSource(seed, engine, world)), null); + this.delegate = delegate; + this.engine = engine; + } + + @Override + public @Nullable Pair> findNearestMapStructure(ServerLevel level, HolderSet holders, BlockPos pos, int radius, boolean findUnexplored) { + if (!vanillaControl().active()) { + return null; + } + return delegate.findNearestMapStructure(level, holders, pos, radius, findUnexplored); + } + + @Override + protected MapCodec codec() { + return MapCodec.unit(null); + } + + @Override + public ChunkGenerator getDelegate() { + if (delegate instanceof CustomChunkGenerator chunkGenerator) + return chunkGenerator.getDelegate(); + return delegate; + } + + @Override + public int getMinY() { + return delegate.getMinY(); + } + + @Override + public int getSeaLevel() { + return delegate.getSeaLevel(); + } + + @Override + public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess access, StructureTemplateManager templateManager, ResourceKey levelKey) { + if (!vanillaControl().active()) { + return; + } + delegate.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey); + } + + private IrisVanillaStructureControl vanillaControl() { + return engine.getDimension().getVanillaStructures(); + } + + @Override + public ChunkGeneratorStructureState createState(HolderLookup holderlookup, RandomState randomstate, long i, SpigotWorldConfig conf) { + return delegate.createState(holderlookup, randomstate, i, conf); + } + + @Override + public void createReferences(WorldGenLevel generatoraccessseed, StructureManager structuremanager, ChunkAccess ichunkaccess) { + delegate.createReferences(generatoraccessseed, structuremanager, ichunkaccess); + } + + @Override + public CompletableFuture createBiomes(RandomState randomstate, Blender blender, StructureManager structuremanager, ChunkAccess ichunkaccess) { + return delegate.createBiomes(randomstate, blender, structuremanager, ichunkaccess); + } + + @Override + public void buildSurface(WorldGenRegion regionlimitedworldaccess, StructureManager structuremanager, RandomState randomstate, ChunkAccess ichunkaccess) { + delegate.buildSurface(regionlimitedworldaccess, structuremanager, randomstate, ichunkaccess); + } + + @Override + public void applyCarvers(WorldGenRegion regionlimitedworldaccess, long seed, RandomState randomstate, BiomeManager biomemanager, StructureManager structuremanager, ChunkAccess ichunkaccess) { + delegate.applyCarvers(regionlimitedworldaccess, seed, randomstate, biomemanager, structuremanager, ichunkaccess); + } + + @Override + public CompletableFuture fillFromNoise(Blender blender, RandomState randomstate, StructureManager structuremanager, ChunkAccess ichunkaccess) { + return delegate.fillFromNoise(blender, randomstate, structuremanager, ichunkaccess); + } + + @Override + public WeightedList getMobsAt(Holder holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) { + return delegate.getMobsAt(holder, structuremanager, enumcreaturetype, blockposition); + } + + @Override + public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager) { + applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, true); + } + + @Override + public void addDebugScreenInfo(List list, RandomState randomstate, BlockPos blockposition) { + delegate.addDebugScreenInfo(list, randomstate, blockposition); + } + + @Override + public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla) { + addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager); + if (vanillaControl().active()) { + placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager); + } + placeIrisNativeStructures(generatoraccessseed, ichunkaccess); + delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false); + } + + private void placeVanillaStructures(WorldGenLevel world, ChunkAccess chunk, StructureManager structureManager) { + if (!structureManager.shouldGenerateStructures()) { + return; + } + ChunkPos chunkPos = chunk.getPos(); + SectionPos sectionPos = SectionPos.of(chunkPos, world.getMinSectionY()); + BlockPos origin = sectionPos.origin(); + Registry registry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE); + Map> byStep = registry.stream().collect(Collectors.groupingBy(s -> s.step().ordinal())); + WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed())); + long decoSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ()); + BoundingBox area = writableArea(chunk); + int steps = GenerationStep.Decoration.values().length; + IrisVanillaStructureControl control = vanillaControl(); + for (int step = 0; step < steps; step++) { + int index = 0; + for (Structure structure : byStep.getOrDefault(step, List.of())) { + Object id = registry.getKey(structure); + if (control.shouldGenerate(id == null ? null : id.toString())) { + random.setFeatureSeed(decoSeed, index, step); + try { + structureManager.startsForStructure(sectionPos, structure) + .forEach(start -> start.placeInChunk(world, structureManager, this, random, area, chunkPos)); + } catch (Throwable e) { + Iris.reportError(e); + } + } + index++; + } + } + } + + private BoundingBox writableArea(ChunkAccess chunk) { + ChunkPos cp = chunk.getPos(); + int i = cp.getMinBlockX(); + int j = cp.getMinBlockZ(); + int minY = getMinY() + 1; + int maxY = getMinY() + engine.getHeight() - 1; + return new BoundingBox(i, minY, j, i + 15, maxY, j + 15); + } + + private void placeIrisNativeStructures(WorldGenLevel world, ChunkAccess chunk) { + if (!(world instanceof WorldGenRegion region)) { + return; + } + ChunkPos chunkPos = chunk.getPos(); + int cx = chunkPos.getMinBlockX() >> 4; + int cz = chunkPos.getMinBlockZ() >> 4; + int bx = 8 + (cx << 4); + int bz = 8 + (cz << 4); + IrisBiome biome = engine.getComplex().getTrueBiomeStream().get(bx, bz); + IrisRegion ireg = engine.getComplex().getRegionStream().get(bx, bz); + KList placements = new KList<>(); + if (biome != null) { + placements.addAll(biome.getStructures()); + } + if (ireg != null) { + placements.addAll(ireg.getStructures()); + } + placements.addAll(engine.getDimension().getStructures()); + if (placements.isEmpty()) { + return; + } + + long seed = world.getSeed(); + RNG rng = new RNG(Cache.key(cx, cz) + seed); + ServerLevel level = region.getLevel(); + Registry registry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE); + RandomState randomState = level.getChunkSource().randomState(); + StructureTemplateManager templateManager = level.getStructureManager(); + StructureManager structureManager = level.structureManager(); + BiomeSource biomeSource = getBiomeSource(); + ResourceKey levelKey = level.dimension(); + int minY = getMinY() + 1; + int maxY = getMinY() + engine.getHeight() - 1; + + for (IrisStructurePlacement placement : placements) { + if (placement.getRoute() != StructurePlacementRoute.NATIVE_AT_POINT || placement.getVanilla().isEmpty()) { + continue; + } + if (!StructurePlacementGrid.startsInChunk(placement, cx, cz, seed, rng)) { + continue; + } + String key = placement.getVanilla().get(rng.i(0, placement.getVanilla().size() - 1)).toLowerCase(); + try { + String[] parts = key.split(":", 2); + Identifier id = parts.length > 1 ? Identifier.fromNamespaceAndPath(parts[0], parts[1]) : Identifier.fromNamespaceAndPath("minecraft", parts[0]); + Holder holder = registry.getOrThrow(ResourceKey.create(Registries.STRUCTURE, id)); + Structure structure = holder.value(); + StructureStart start = structure.generate(holder, levelKey, world.registryAccess(), this, biomeSource, randomState, templateManager, seed, chunkPos, 0, level, b -> true); + if (start == null || !start.isValid()) { + continue; + } + BoundingBox box = start.getBoundingBox(); + WorldgenRandom placeRandom = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed())); + for (int scx = box.minX() >> 4; scx <= box.maxX() >> 4; scx++) { + for (int scz = box.minZ() >> 4; scz <= box.maxZ() >> 4; scz++) { + ChunkPos target = new ChunkPos(scx, scz); + BoundingBox chunkBox = new BoundingBox(target.getMinBlockX(), minY, target.getMinBlockZ(), target.getMaxBlockX(), maxY, target.getMaxBlockZ()); + start.placeInChunk(world, structureManager, this, placeRandom, chunkBox, target); + } + } + } catch (Throwable e) { + Iris.reportError(e); + } + } + } + + @Override + public void addVanillaDecorations(WorldGenLevel level, ChunkAccess chunkAccess, StructureManager structureManager) { + SectionPos sectionPos = SectionPos.of(chunkAccess.getPos(), level.getMinSectionY()); + BlockPos blockPos = sectionPos.origin(); + + Heightmap surface = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG); + Heightmap ocean = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG); + Heightmap motion = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING); + Heightmap motionNoLeaves = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES); + + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + int wX = x + blockPos.getX(); + int wZ = z + blockPos.getZ(); + + int terrainTop = engine.getHeight(wX, wZ, false) + engine.getMinHeight() + 1; + int terrainNoFluid = engine.getHeight(wX, wZ, true) + engine.getMinHeight() + 1; + SET_HEIGHT.invoke(ocean, x, z, terrainNoFluid); + SET_HEIGHT.invoke(surface, x, z, terrainTop); + SET_HEIGHT.invoke(motion, x, z, terrainTop); + SET_HEIGHT.invoke(motionNoLeaves, x, z, terrainTop); + } + } + + Heightmap.primeHeightmaps(chunkAccess, ChunkStatus.FINAL_HEIGHTMAPS); + } + + @Override + public void spawnOriginalMobs(WorldGenRegion regionlimitedworldaccess) { + delegate.spawnOriginalMobs(regionlimitedworldaccess); + } + + @Override + public int getSpawnHeight(LevelHeightAccessor levelheightaccessor) { + return delegate.getSpawnHeight(levelheightaccessor); + } + + @Override + public int getGenDepth() { + return delegate.getGenDepth(); + } + + @Override + public int getBaseHeight(int i, int j, Heightmap.Types heightmap_type, LevelHeightAccessor levelheightaccessor, RandomState randomstate) { + return levelheightaccessor.getMinY() + engine.getHeight(i, j, !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState())) + 1; + } + + @Override + public NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelheightaccessor, RandomState randomstate) { + int block = engine.getHeight(i, j, true); + int water = engine.getHeight(i, j, false); + BlockState[] column = new BlockState[levelheightaccessor.getHeight()]; + for (int k = 0; k < column.length; k++) { + if (k <= block) column[k] = Blocks.STONE.defaultBlockState(); + else if (k <= water) column[k] = Blocks.WATER.defaultBlockState(); + else column[k] = Blocks.AIR.defaultBlockState(); + } + return new NoiseColumn(levelheightaccessor.getMinY(), column); + } + + @Override + public Optional getTypeNameForDataFixer() { + return delegate.getTypeNameForDataFixer(); + } + + @Override + public void validate() { + delegate.validate(); + } + + static { + Field biomeSource = null; + for (Field field : ChunkGenerator.class.getDeclaredFields()) { + if (!field.getType().equals(BiomeSource.class)) + continue; + biomeSource = field; + break; + } + if (biomeSource == null) + throw new RuntimeException("Could not find biomeSource field in ChunkGenerator!"); + + Method setHeight = null; + for (Method method : Heightmap.class.getDeclaredMethods()) { + var types = method.getParameterTypes(); + if (types.length != 3 || !Arrays.equals(types, new Class[]{int.class, int.class, int.class}) + || !method.getReturnType().equals(void.class)) + continue; + setHeight = method; + break; + } + if (setHeight == null) + throw new RuntimeException("Could not find setHeight method in Heightmap!"); + + BIOME_SOURCE = new WrappedField<>(ChunkGenerator.class, biomeSource.getName()); + SET_HEIGHT = new WrappedReturningMethod<>(Heightmap.class, setHeight.getName(), setHeight.getParameterTypes()); + } + + private static ChunkGenerator edit(ChunkGenerator generator, BiomeSource source) { + try { + BIOME_SOURCE.set(generator, source); + if (generator instanceof CustomChunkGenerator custom) + BIOME_SOURCE.set(custom.getDelegate(), source); + + return generator; + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } +} diff --git a/nms/v26_1_R1/src/main/java/art/arcane/iris/core/nms/v26_1_R1/NMSBinding.java b/nms/v26_1_R1/src/main/java/art/arcane/iris/core/nms/v26_1_R1/NMSBinding.java new file mode 100644 index 000000000..829b43569 --- /dev/null +++ b/nms/v26_1_R1/src/main/java/art/arcane/iris/core/nms/v26_1_R1/NMSBinding.java @@ -0,0 +1,880 @@ +package art.arcane.iris.core.nms.v26_1_R1; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import art.arcane.iris.Iris; +import art.arcane.iris.core.nms.INMSBinding; +import art.arcane.iris.core.nms.container.BiomeColor; +import art.arcane.iris.core.nms.container.Pair; +import art.arcane.iris.core.nms.container.BlockProperty; +import art.arcane.iris.core.nms.datapack.DataVersion; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.iris.util.project.agent.Agent; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.iris.util.common.format.C; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.json.JSONObject; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.math.Vector3d; +import art.arcane.volmlib.util.matter.MatterBiomeInject; +import art.arcane.iris.util.nbt.common.mca.NBTWorld; +import art.arcane.volmlib.util.nbt.mca.palette.*; +import art.arcane.volmlib.util.nbt.tag.CompoundTag; +import art.arcane.iris.util.common.scheduling.J; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.shorts.ShortList; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.matcher.ElementMatchers; +import net.minecraft.core.*; +import net.minecraft.core.Registry; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.*; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.commands.data.BlockDataAccessor; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.TagKey; +import net.minecraft.world.attribute.EnvironmentAttributes; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.level.biome.Biomes; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.chunk.status.WorldGenContext; +import net.minecraft.world.level.dimension.LevelStem; +import net.minecraft.world.level.levelgen.FlatLevelSource; +import net.minecraft.world.level.levelgen.flat.FlatLayerInfo; +import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings; +import net.minecraft.world.level.storage.LevelStorageSource; +import org.bukkit.*; +import org.bukkit.block.Biome; +import org.bukkit.block.data.BlockData; +import org.bukkit.craftbukkit.CraftChunk; +import org.bukkit.craftbukkit.CraftServer; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.craftbukkit.block.CraftBlockState; +import org.bukkit.craftbukkit.block.CraftBlockStates; +import org.bukkit.craftbukkit.block.data.CraftBlockData; +import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.craftbukkit.util.CraftMagicNumbers; +import org.bukkit.craftbukkit.util.CraftNamespacedKey; +import org.bukkit.entity.Entity; +import org.bukkit.event.entity.CreatureSpawnEvent; +import org.bukkit.generator.BiomeProvider; +import org.bukkit.generator.ChunkGenerator; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.awt.Color; +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +public class NMSBinding implements INMSBinding { + private final KMap baseBiomeCache = new KMap<>(); + private final BlockData AIR = Material.AIR.createBlockData(); + private final AtomicCache> biomeMapCache = new AtomicCache<>(); + private final AtomicBoolean injected = new AtomicBoolean(); + private final AtomicCache> registryCache = new AtomicCache<>(); + private final AtomicCache> globalCache = new AtomicCache<>(); + private final AtomicCache registryAccess = new AtomicCache<>(); + private final AtomicCache byIdRef = new AtomicCache<>(); + + private static Object getFor(Class type, Object source) { + Object o = fieldFor(type, source); + + if (o != null) { + return o; + } + + return invokeFor(type, source); + } + + private static Object invokeFor(Class returns, Object in) { + for (Method i : in.getClass().getMethods()) { + if (i.getReturnType().equals(returns)) { + i.setAccessible(true); + try { + Iris.debug("[NMS] Found " + returns.getSimpleName() + " in " + in.getClass().getSimpleName() + "." + i.getName() + "()"); + return i.invoke(in); + } catch (Throwable e) { + e.printStackTrace(); + } + } + } + + return null; + } + + private static Object fieldFor(Class returns, Object in) { + return fieldForClass(returns, in.getClass(), in); + } + + @SuppressWarnings("unchecked") + private static T fieldForClass(Class returnType, Class sourceType, Object in) { + for (Field i : sourceType.getDeclaredFields()) { + if (i.getType().equals(returnType)) { + i.setAccessible(true); + try { + Iris.debug("[NMS] Found " + returnType.getSimpleName() + " in " + sourceType.getSimpleName() + "." + i.getName()); + return (T) i.get(in); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + } + return null; + } + + private static Class getClassType(Class type, int ordinal) { + return type.getDeclaredClasses()[ordinal]; + } + + @Override + public boolean hasTile(Material material) { + return !CraftBlockState.class.equals(CraftBlockStates.getBlockStateType(material)); + } + + @Override + public boolean hasTile(Location l) { + return ((CraftWorld) l.getWorld()).getHandle().getBlockEntity(new BlockPos(l.getBlockX(), l.getBlockY(), l.getBlockZ())) != null; + } + + @Override + @SuppressWarnings("unchecked") + public KMap serializeTile(Location location) { + BlockEntity e = ((CraftWorld) location.getWorld()).getHandle().getBlockEntity(new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ())); + + if (e == null) { + return null; + } + + net.minecraft.nbt.CompoundTag tag = e.saveWithoutMetadata(registry()); + return (KMap) convertFromTag(tag, 0, 64); + } + + @Contract(value = "null, _, _ -> null", pure = true) + private Object convertFromTag(Tag tag, int depth, int maxDepth) { + if (tag == null || depth > maxDepth) return null; + return switch (tag) { + case CollectionTag collection -> { + KList list = new KList<>(); + + for (Object i : collection) { + if (i instanceof Tag t) + list.add(convertFromTag(t, depth + 1, maxDepth)); + else list.add(i); + } + yield list; + } + case net.minecraft.nbt.CompoundTag compound -> { + KMap map = new KMap<>(); + + for (String key : compound.keySet()) { + var child = compound.get(key); + if (child == null) continue; + var value = convertFromTag(child, depth + 1, maxDepth); + if (value == null) continue; + map.put(key, value); + } + yield map; + } + case NumericTag numeric -> numeric.box(); + default -> tag.asString().orElse(null); + }; + } + + @Override + public void deserializeTile(KMap map, Location pos) { + if (map == null || pos == null || pos.getWorld() == null) { + return; + } + + Tag converted = convertToTag(map, 0, 64); + if (!(converted instanceof net.minecraft.nbt.CompoundTag tag)) { + return; + } + + var level = ((CraftWorld) pos.getWorld()).getHandle(); + var blockPos = new BlockPos(pos.getBlockX(), pos.getBlockY(), pos.getBlockZ()); + if (!J.runAt(pos, () -> merge(level, blockPos, tag))) { + Iris.warn("[NMS] Failed to schedule tile deserialize at " + blockPos + " in world " + pos.getWorld().getName()); + } + } + + private void merge(ServerLevel level, BlockPos blockPos, net.minecraft.nbt.CompoundTag tag) { + if (level == null || blockPos == null || tag == null) { + return; + } + + try { + var blockEntity = level.getBlockEntity(blockPos); + if (blockEntity == null) { + Iris.warn("[NMS] BlockEntity not found at " + blockPos); + var state = level.getBlockState(blockPos); + if (!state.hasBlockEntity()) { + return; + } + + blockEntity = ((EntityBlock) state.getBlock()) + .newBlockEntity(blockPos, state); + } + + var accessor = new BlockDataAccessor(blockEntity, blockPos); + accessor.setData(accessor.getData().merge(tag)); + } catch (Throwable e) { + Iris.warn("[NMS] Failed to merge tile data at " + blockPos + ": " + e.getMessage()); + Iris.reportError(e); + } + } + + private Tag convertToTag(Object object, int depth, int maxDepth) { + if (object == null || depth > maxDepth) return EndTag.INSTANCE; + return switch (object) { + case Map map -> { + var tag = new net.minecraft.nbt.CompoundTag(); + for (var i : map.entrySet()) { + tag.put(i.getKey().toString(), convertToTag(i.getValue(), depth + 1, maxDepth)); + } + yield tag; + } + case List list -> { + var tag = new ListTag(); + for (var i : list) { + tag.add(convertToTag(i, depth + 1, maxDepth)); + } + yield tag; + } + case Byte number -> ByteTag.valueOf(number); + case Short number -> ShortTag.valueOf(number); + case Integer number -> IntTag.valueOf(number); + case Long number -> LongTag.valueOf(number); + case Float number -> FloatTag.valueOf(number); + case Double number -> DoubleTag.valueOf(number); + case String string -> StringTag.valueOf(string); + default -> EndTag.INSTANCE; + }; + } + + @Override + public CompoundTag serializeEntity(Entity location) { + return null;// TODO: + } + + @Override + public Entity deserializeEntity(CompoundTag s, Location newPosition) { + return null;// TODO: + } + + @Override + public boolean supportsCustomHeight() { + return true; + } + + private RegistryAccess registry() { + return registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())); + } + + private Registry getCustomBiomeRegistry() { + return registry().lookup(Registries.BIOME).orElse(null); + } + + private Registry getBlockRegistry() { + return registry().lookup(Registries.BLOCK).orElse(null); + } + + @Override + public Object getBiomeBaseFromId(int id) { + return getCustomBiomeRegistry().get(id); + } + + @Override + public int getMinHeight(World world) { + return world.getMinHeight(); + } + + @Override + public boolean supportsCustomBiomes() { + return true; + } + + @Override + public int getTrueBiomeBaseId(Object biomeBase) { + return getCustomBiomeRegistry().getId(((Holder) biomeBase).value()); + } + + @Override + public Object getTrueBiomeBase(Location location) { + return ((CraftWorld) location.getWorld()).getHandle().getBiome(new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ())); + } + + @Override + public String getTrueBiomeBaseKey(Location location) { + return getKeyForBiomeBase(getTrueBiomeBase(location)); + } + + @Override + public Object getCustomBiomeBaseFor(String mckey) { + return getCustomBiomeRegistry().getValue(net.minecraft.resources.Identifier.parse(mckey)); + } + + @Override + public Object getCustomBiomeBaseHolderFor(String mckey) { + return getCustomBiomeRegistry().get(getTrueBiomeBaseId(getCustomBiomeRegistry().get(net.minecraft.resources.Identifier.parse(mckey)))).orElse(null); + } + + public int getBiomeBaseIdForKey(String key) { + return getCustomBiomeRegistry().getId(getCustomBiomeRegistry().get(net.minecraft.resources.Identifier.parse(key)).map(Holder::value).orElse(null)); + } + + @Override + public String getKeyForBiomeBase(Object biomeBase) { + return getCustomBiomeRegistry().getKey((net.minecraft.world.level.biome.Biome) biomeBase).getPath(); // something, not something:something + } + + @Override + public Object getBiomeBase(World world, Biome biome) { + return biomeToBiomeBase(((CraftWorld) world).getHandle() + .registryAccess().lookup(Registries.BIOME).orElse(null), biome); + } + + @Override + public Object getBiomeBase(Object registry, Biome biome) { + Object v = baseBiomeCache.get(biome); + + if (v != null) { + return v; + } + //noinspection unchecked + v = biomeToBiomeBase((Registry) registry, biome); + if (v == null) { + // Ok so there is this new biome name called "CUSTOM" in Paper's new releases. + // But, this does NOT exist within CraftBukkit which makes it return an error. + // So, we will just return the ID that the plains biome returns instead. + //noinspection unchecked + return biomeToBiomeBase((Registry) registry, Biome.PLAINS); + } + baseBiomeCache.put(biome, v); + return v; + } + + @Override + public KList getBiomes() { + KList biomes = new KList<>(); + for (Biome biome : org.bukkit.Registry.BIOME) { + biomes.add(biome); + } + return biomes; + } + + @Override + public KList getStructureKeys() { + KList keys = new KList<>(); + try { + registry().lookupOrThrow(Registries.STRUCTURE).keySet().forEach(k -> keys.add(k.toString())); + } catch (Throwable e) { + Iris.reportError(e); + } + return keys; + } + + @Override + public KList getStructureSetKeys() { + KList keys = new KList<>(); + try { + registry().lookupOrThrow(Registries.STRUCTURE_SET).keySet().forEach(k -> keys.add(k.toString())); + } catch (Throwable e) { + Iris.reportError(e); + } + return keys; + } + + @Override + public boolean isBukkit() { + return true; + } + + @Override + public int getBiomeId(Biome biome) { + for (World i : Bukkit.getWorlds()) { + if (i.getEnvironment().equals(World.Environment.NORMAL)) { + Registry registry = ((CraftWorld) i).getHandle().registryAccess().lookup(Registries.BIOME).orElse(null); + return registry.getId((net.minecraft.world.level.biome.Biome) getBiomeBase(registry, biome)); + } + } + + List biomes = new ArrayList<>(); + for (Biome entry : org.bukkit.Registry.BIOME) { + biomes.add(entry); + } + int index = biomes.indexOf(biome); + return Math.max(index, 0); + } + + private MCAIdMap getBiomeMapping() { + return biomeMapCache.aquire(() -> new MCAIdMap<>() { + @NotNull + @Override + public Iterator iterator() { + return getCustomBiomeRegistry().iterator(); + } + + @Override + public int getId(net.minecraft.world.level.biome.Biome paramT) { + return getCustomBiomeRegistry().getId(paramT); + } + + @Override + public net.minecraft.world.level.biome.Biome byId(int paramInt) { + return (net.minecraft.world.level.biome.Biome) getBiomeBaseFromId(paramInt); + } + }); + } + + @NotNull + private MCABiomeContainer getBiomeContainerInterface(MCAIdMap biomeMapping, MCAChunkBiomeContainer base) { + return new MCABiomeContainer() { + @Override + public int[] getData() { + return base.writeBiomes(); + } + + @Override + public void setBiome(int x, int y, int z, int id) { + base.setBiome(x, y, z, biomeMapping.byId(id)); + } + + @Override + public int getBiome(int x, int y, int z) { + return biomeMapping.getId(base.getBiome(x, y, z)); + } + }; + } + + @Override + public MCABiomeContainer newBiomeContainer(int min, int max) { + MCAChunkBiomeContainer base = new MCAChunkBiomeContainer<>(getBiomeMapping(), min, max); + return getBiomeContainerInterface(getBiomeMapping(), base); + } + + @Override + public MCABiomeContainer newBiomeContainer(int min, int max, int[] data) { + MCAChunkBiomeContainer base = new MCAChunkBiomeContainer<>(getBiomeMapping(), min, max, data); + return getBiomeContainerInterface(getBiomeMapping(), base); + } + + @Override + public int countCustomBiomes() { + AtomicInteger a = new AtomicInteger(0); + + getCustomBiomeRegistry().keySet().forEach((i) -> { + if (i.getNamespace().equals("minecraft")) { + return; + } + + a.incrementAndGet(); + Iris.debug("Custom Biome: " + i); + }); + + return a.get(); + } + + public boolean supportsDataPacks() { + return true; + } + + public void setBiomes(int cx, int cz, World world, Hunk biomes) { + LevelChunk c = ((CraftWorld) world).getHandle().getChunk(cx, cz); + biomes.iterateSync((x, y, z, b) -> c.setNoiseBiome(x, y, z, (Holder) b)); + c.markUnsaved(); + } + + @Override + public MCAPaletteAccess createPalette() { + MCAIdMapper registry = registryCache.aquireNasty(() -> { + Field cf = IdMapper.class.getDeclaredField("tToId"); + Field df = IdMapper.class.getDeclaredField("idToT"); + Field bf = IdMapper.class.getDeclaredField("nextId"); + cf.setAccessible(true); + df.setAccessible(true); + bf.setAccessible(true); + IdMapper blockData = Block.BLOCK_STATE_REGISTRY; + int b = bf.getInt(blockData); + Object2IntMap c = (Object2IntMap) cf.get(blockData); + List d = (List) df.get(blockData); + return new MCAIdMapper(c, d, b); + }); + MCAPalette global = globalCache.aquireNasty(() -> new MCAGlobalPalette<>(registry, ((CraftBlockData) AIR).getState())); + MCAPalettedContainer container = new MCAPalettedContainer<>(global, registry, + i -> ((CraftBlockData) NBTWorld.getBlockData(i)).getState(), + i -> NBTWorld.getCompound(CraftBlockData.createData(i)), + ((CraftBlockData) AIR).getState()); + return new MCAWrappedPalettedContainer<>(container, + i -> NBTWorld.getCompound(CraftBlockData.createData(i)), + i -> ((CraftBlockData) NBTWorld.getBlockData(i)).getState()); + } + + @Override + public void injectBiomesFromMantle(Chunk e, Mantle mantle) { + ChunkAccess chunk = ((CraftChunk) e).getHandle(ChunkStatus.FULL); + AtomicInteger c = new AtomicInteger(); + AtomicInteger r = new AtomicInteger(); + mantle.iterateChunk(e.getX(), e.getZ(), MatterBiomeInject.class, (x, y, z, b) -> { + if (b != null) { + if (b.isCustom()) { + chunk.setNoiseBiome(x, y, z, getCustomBiomeRegistry().get(b.getBiomeId()).get()); + c.getAndIncrement(); + } else { + chunk.setNoiseBiome(x, y, z, (Holder) getBiomeBase(e.getWorld(), b.getBiome())); + r.getAndIncrement(); + } + } + }); + } + + public ItemStack applyCustomNbt(ItemStack itemStack, KMap customNbt) throws IllegalArgumentException { + if (customNbt != null && !customNbt.isEmpty()) { + net.minecraft.world.item.ItemStack s = CraftItemStack.asNMSCopy(itemStack); + + try { + net.minecraft.nbt.CompoundTag tag = TagParser.parseCompoundFully((new JSONObject(customNbt)).toString()); + tag.merge(s.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag()); + s.set(DataComponents.CUSTOM_DATA, CustomData.of(tag)); + } catch (CommandSyntaxException var5) { + throw new IllegalArgumentException(var5); + } + + return CraftItemStack.asBukkitCopy(s); + } else { + return itemStack; + } + } + + public void inject(long seed, Engine engine, World world) throws NoSuchFieldException, IllegalAccessException { + var chunkMap = ((CraftWorld)world).getHandle().getChunkSource().chunkMap; + var worldGenContextField = getField(chunkMap.getClass(), WorldGenContext.class); + worldGenContextField.setAccessible(true); + var worldGenContext = (WorldGenContext) worldGenContextField.get(chunkMap); + var dimensionType = chunkMap.level.dimensionTypeRegistration().unwrapKey().orElse(null); + String expectedDimensionType = "iris:" + engine.getDimension().getDimensionTypeKey(); + if (dimensionType != null) { + String actualDimensionType = dimensionType.identifier().toString(); + if (!dimensionType.identifier().getNamespace().equals("iris")) { + Iris.error("Loaded world %s with invalid dimension type! expected=%s actual=%s", world.getName(), expectedDimensionType, actualDimensionType); + } else { + Iris.info("Loaded world %s with Iris dimension type %s", world.getName(), actualDimensionType); + } + } else { + Iris.error("Loaded world %s with unknown dimension type! expected=%s", world.getName(), expectedDimensionType); + } + + var newContext = new WorldGenContext( + worldGenContext.level(), new IrisChunkGenerator(worldGenContext.generator(), seed, engine, world), + worldGenContext.structureManager(), worldGenContext.lightEngine(), worldGenContext.mainThreadExecutor(), worldGenContext.unsavedListener()); + + worldGenContextField.set(chunkMap, newContext); + } + + public Vector3d getBoundingbox(org.bukkit.entity.EntityType entity) { + if (entity == null) { + return null; + } + + try { + String descriptionId = "entity.minecraft." + entity.name().toLowerCase(Locale.ROOT); + Field[] fields = EntityType.class.getDeclaredFields(); + for (Field field : fields) { + if (!Modifier.isStatic(field.getModifiers()) || !field.getType().equals(EntityType.class)) { + continue; + } + + EntityType entityType = (EntityType) field.get(null); + if (entityType == null) { + continue; + } + + if (descriptionId.equals(entityType.getDescriptionId())) { + return new Vector3d(entityType.getWidth(), entityType.getHeight(), entityType.getWidth()); + } + } + return null; + } catch (Throwable e) { + Iris.error("Unable to get entity dimensions for " + entity + "!"); + Iris.reportError(e); + return null; + } + } + + + @Override + public Entity spawnEntity(Location location, org.bukkit.entity.EntityType type, CreatureSpawnEvent.SpawnReason reason) { + if (location == null || location.getWorld() == null || type == null || type.getEntityClass() == null) { + return null; + } + return ((CraftWorld) location.getWorld()).spawn(location, type.getEntityClass(), null, reason); + } + + @Override + public Color getBiomeColor(Location location, BiomeColor type) { + ServerLevel reader = ((CraftWorld) location.getWorld()).getHandle(); + var pos = new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ()); + var holder = reader.getBiome(pos); + var biome = holder.value(); + if (biome == null) throw new IllegalArgumentException("Invalid biome: " + holder.unwrapKey().orElse(null)); + + var attributes = reader.environmentAttributes(); + int rgba = switch (type) { + case FOG -> attributes.getValue(EnvironmentAttributes.FOG_COLOR, pos); + case WATER -> biome.getWaterColor(); + case WATER_FOG -> attributes.getValue(EnvironmentAttributes.WATER_FOG_COLOR, pos); + case SKY -> attributes.getValue(EnvironmentAttributes.SKY_COLOR, pos); + case FOLIAGE -> biome.getFoliageColor(); + case GRASS -> biome.getGrassColor(location.getBlockX(), location.getBlockZ()); + }; + if (rgba == 0) { + if (BiomeColor.FOLIAGE == type && biome.getSpecialEffects().foliageColorOverride().isEmpty()) + return null; + if (BiomeColor.GRASS == type && biome.getSpecialEffects().grassColorOverride().isEmpty()) + return null; + } + return new Color(rgba, true); + } + + private static Field getField(Class clazz, Class fieldType) throws NoSuchFieldException { + try { + for (Field f : clazz.getDeclaredFields()) { + if (f.getType().equals(fieldType)) + return f; + } + throw new NoSuchFieldException(fieldType.getName()); + } catch (NoSuchFieldException var4) { + Class superClass = clazz.getSuperclass(); + if (superClass == null) { + throw var4; + } else { + return getField(superClass, fieldType); + } + } + } + + public static Holder biomeToBiomeBase(Registry registry, Biome biome) { + if (registry == null || biome == null) { + return null; + } + + NamespacedKey biomeKey = resolveBiomeKey(biome); + if (biomeKey == null) { + return null; + } + + ResourceKey key = ResourceKey.create(Registries.BIOME, CraftNamespacedKey.toMinecraft(biomeKey)); + return registry.get(key).orElse(null); + } + + private static NamespacedKey resolveBiomeKey(Biome biome) { + Object keyOrNullValue = invokeNoThrow(biome, "getKeyOrNull", new Class[0]); + if (keyOrNullValue instanceof NamespacedKey namespacedKey) { + return namespacedKey; + } + + Object keyOrThrowValue = invokeNoThrow(biome, "getKeyOrThrow", new Class[0]); + if (keyOrThrowValue instanceof NamespacedKey namespacedKey) { + return namespacedKey; + } + + Object keyValue = invokeNoThrow(biome, "getKey", new Class[0]); + if (keyValue instanceof NamespacedKey namespacedKey) { + return namespacedKey; + } + + return null; + } + + private static Object invokeNoThrow(Object target, String methodName, Class[] parameterTypes, Object... args) { + if (target == null) { + return null; + } + + try { + Method method = target.getClass().getMethod(methodName, parameterTypes); + return method.invoke(target, args); + } catch (Throwable ignored) { + return null; + } + } + + @Override + public DataVersion getDataVersion() { + return DataVersion.V26_1_2; + } + + @Override + public int getSpawnChunkCount(World world) { + return 0; + } + + @Override + public boolean missingDimensionTypes(String... keys) { + var type = registry().lookupOrThrow(Registries.DIMENSION_TYPE); + return !Arrays.stream(keys) + .map(key -> Identifier.fromNamespaceAndPath("iris", key)) + .allMatch(type::containsKey); + } + + @Override + public boolean injectBukkit() { + if (injected.getAndSet(true)) + return true; + try { + Iris.info("Injecting Bukkit"); + var buddy = new ByteBuddy(); + buddy.redefine(ServerLevel.class) + .visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor() + .and(ElementMatchers.takesArgument(0, MinecraftServer.class)) + .and(ElementMatchers.takesArgument(5, LevelStem.class)))) + .make() + .load(ServerLevel.class.getClassLoader(), Agent.installed()); + for (Class clazz : List.of(ChunkAccess.class, ProtoChunk.class)) { + buddy.redefine(clazz) + .visit(Advice.to(ChunkAccessAdvice.class).on(ElementMatchers.isMethod().and(ElementMatchers.takesArguments(ShortList.class, int.class)))) + .make() + .load(clazz.getClassLoader(), Agent.installed()); + } + + return true; + } catch (Throwable e) { + Iris.error(C.RED + "Failed to inject Bukkit"); + e.printStackTrace(); + } + return false; + } + + @Override + public KMap> getBlockProperties() { + KMap> states = new KMap<>(); + + for (var block : registry().lookupOrThrow(Registries.BLOCK)) { + var state = block.defaultBlockState(); + if (state == null) state = block.getStateDefinition().any(); + final var finalState = state; + + states.put(CraftMagicNumbers.getMaterial(block), block.getStateDefinition() + .getProperties() + .stream() + .map(p -> createProperty(p, finalState)) + .toList()); + } + return states; + } + + private > BlockProperty createProperty(Property property, BlockState state) { + return new BlockProperty(property.getName(), property.getValueClass(), state.getValue(property), property.getPossibleValues(), property::getName); + } + + @Override + public Object createRuntimeLevelStem(Object registryAccess, ChunkGenerator raw) { + if (!(registryAccess instanceof RegistryAccess access)) { + throw new IllegalStateException("Runtime LevelStem creation requires a RegistryAccess instance."); + } + if (!(raw instanceof PlatformChunkGenerator generator)) { + throw new IllegalStateException("Generator is not platform chunk generator!"); + } + + Identifier dimensionKey = Identifier.fromNamespaceAndPath("iris", generator.getTarget().getDimension().getDimensionTypeKey()); + Holder.Reference dimensionType = access.lookupOrThrow(Registries.DIMENSION_TYPE) + .getOrThrow(ResourceKey.create(Registries.DIMENSION_TYPE, dimensionKey)); + return new LevelStem(dimensionType, chunkGenerator(access)); + } + + private net.minecraft.world.level.chunk.ChunkGenerator chunkGenerator(RegistryAccess access) { + var settings = new FlatLevelGeneratorSettings(Optional.empty(), access.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.THE_VOID), List.of()); + settings.getLayersInfo().add(new FlatLayerInfo(1, Blocks.AIR)); + settings.updateLayers(); + return new FlatLevelSource(settings); + } + + private static class ChunkAccessAdvice { + @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class) + static boolean enter(@Advice.This ChunkAccess access, @Advice.Argument(1) int index) { + return index >= access.getPostProcessing().length; + } + } + + private static class ServerLevelAdvice { + @Advice.OnMethodEnter + static void enter( + @Advice.Argument(0) MinecraftServer server, + @Advice.Argument(2) LevelStorageSource.LevelStorageAccess levelStorageAccess, + @Advice.Argument(value = 5, readOnly = false) LevelStem levelStem + ) { + if (levelStorageAccess == null) + return; + + try { + String levelId = levelStorageAccess.getLevelId(); + if (levelId == null || levelId.isBlank()) { + return; + } + + Object generator = Class.forName("art.arcane.iris.core.lifecycle.WorldLifecycleStaging", true, Bukkit.getPluginManager().getPlugin("Iris") + .getClass() + .getClassLoader()) + .getDeclaredMethod("consumeStemGenerator", String.class) + .invoke(null, levelId); + if (!(generator instanceof ChunkGenerator gen) || !gen.getClass().getPackageName().startsWith("art.arcane.iris")) { + return; + } + + Object bindings = Class.forName("art.arcane.iris.core.nms.INMS", true, Bukkit.getPluginManager().getPlugin("Iris") + .getClass() + .getClassLoader()) + .getDeclaredMethod("get") + .invoke(null); + if (bindings == null) { + throw new IllegalStateException("Iris failed to resolve an INMSBinding instance."); + } + + java.lang.reflect.Method stemMethod = null; + for (java.lang.reflect.Method candidate : bindings.getClass().getMethods()) { + if (candidate.getName().equals("createRuntimeLevelStem") && candidate.getParameterCount() == 2) { + stemMethod = candidate; + break; + } + } + if (stemMethod == null) { + throw new IllegalStateException("Iris binding is missing createRuntimeLevelStem."); + } + Object resolvedStem = stemMethod.invoke(bindings, server.registryAccess(), gen); + if (!(resolvedStem instanceof LevelStem runtimeStem)) { + throw new IllegalStateException("Iris runtime LevelStem binding returned " + (resolvedStem == null ? "null" : resolvedStem.getClass().getName()) + "."); + } + levelStem = runtimeStem; + } catch (Throwable e) { + throw new RuntimeException("Iris failed to replace the levelStem", e instanceof InvocationTargetException ex ? ex.getCause() : e); + } + } + } +} diff --git a/settings.gradle b/settings.gradle index c4b707a1f..3537f125b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -70,3 +70,4 @@ if (useLocalVolmLib && localVolmLibDirectory != null) { include(':core', ':core:agent') include(':nms:v1_21_R7') +include(':nms:v26_1_R1')