From 08c6f7ddf955acc41a6e7a346b54eca60852b964 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Fri, 29 May 2026 23:52:58 -0400 Subject: [PATCH] lmao --- .../iris/core/commands/CommandStructure.java | 39 +++ .../core/structure/BulkStructureImporter.java | 319 ++++++++++++++++++ .../core/structure/StructureImporter.java | 4 + .../iris/core/structure/VillageImporter.java | 35 ++ .../framework/PlacedStructurePiece.java | 6 +- .../components/IrisStructureComponent.java | 25 +- .../components/MantleObjectComponent.java | 9 +- .../arcane/iris/engine/object/IrisObject.java | 89 ++++- .../iris/engine/object/IrisStiltSettings.java | 12 + .../engine/object/IrisStructurePlacement.java | 7 +- .../iris/engine/object/ObjectPlaceMode.java | 8 + 11 files changed, 533 insertions(+), 20 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/core/structure/BulkStructureImporter.java 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 index 42acea60f..17e0f4c8e 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandStructure.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandStructure.java @@ -19,6 +19,7 @@ package art.arcane.iris.core.commands; import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.structure.BulkStructureImporter; import art.arcane.iris.core.structure.StructureImporter; import art.arcane.iris.core.structure.StructureIndexService; import art.arcane.iris.core.structure.VillageImporter; @@ -134,6 +135,44 @@ public class CommandStructure implements DirectorExecutor { } } + @Director(description = "Import EVERY vanilla structure into a pack: jigsaw structures rebuilt as pool graphs, single-template structures imported as objects. Idempotent in add-only mode.", aliases = {"import-all", "ia"}, origin = DirectorOrigin.BOTH) + public void importAllVanilla( + @Param(description = "The dimension whose pack to import into", aliases = "dim") + IrisDimension dimension, + @Param(description = "overwrite | add-only | merge", defaultValue = "add-only") + String mode, + @Param(description = "Also import non-jigsaw single-template structures", name = "include-non-jigsaw", aliases = {"single", "nbt"}, defaultValue = "true") + boolean includeNonJigsaw + ) { + IrisData data = dimension.getLoader(); + if (data == null) { + sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey()); + return; + } + BulkStructureImporter.Report report = BulkStructureImporter.importAllVanilla(data, StructureImporter.parseMode(mode), includeNonJigsaw, sender()); + if (report.imported() > 0) { + sender().sendMessage(C.GRAY + "Reference imported structures from a biome/region/dimension structures list, or run /iris structure list " + dimension.getLoadKey() + " to see the refreshed index."); + } + } + + @Director(description = "Import EVERY vanilla structure TEMPLATE (the piece NBTs under minecraft:.../...) as editable Iris objects, including the non-jigsaw templates that import-all cannot reach. Idempotent in add-only mode.", aliases = {"import-templates", "it"}, origin = DirectorOrigin.BOTH) + public void importTemplates( + @Param(description = "The dimension whose pack to import into", aliases = "dim") + IrisDimension dimension, + @Param(description = "overwrite | add-only | merge", defaultValue = "add-only") + String mode + ) { + IrisData data = dimension.getLoader(); + if (data == null) { + sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey()); + return; + } + BulkStructureImporter.Report report = BulkStructureImporter.importAllTemplates(data, StructureImporter.parseMode(mode), sender()); + if (report.imported() > 0) { + sender().sendMessage(C.GRAY + "Reference imported templates from a biome/region/dimension structures list, or run /iris structure list " + dimension.getLoadKey() + " to see the refreshed index."); + } + } + @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") diff --git a/core/src/main/java/art/arcane/iris/core/structure/BulkStructureImporter.java b/core/src/main/java/art/arcane/iris/core/structure/BulkStructureImporter.java new file mode 100644 index 000000000..d2b3583e4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/BulkStructureImporter.java @@ -0,0 +1,319 @@ +/* + * 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.core.nms.INMS; +import art.arcane.iris.util.common.format.C; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.volmlib.util.collection.KList; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeSet; +import java.util.function.Predicate; + +public final class BulkStructureImporter { + public record Report(int total, int imported, int skipped, int failed) { + } + + private BulkStructureImporter() { + } + + public static Report importAllVanilla(IrisData data, StructureImporter.Mode mode, boolean includeNonJigsaw, VolmitSender sender) { + KList keys = INMS.get().getStructureKeys(); + List vanilla = new ArrayList<>(); + for (String k : keys) { + if (k != null && k.startsWith("minecraft:")) { + vanilla.add(k); + } + } + Collections.sort(vanilla); + + int total = vanilla.size(); + int imported = 0; + int skipped = 0; + int failed = 0; + + sender.sendMessage(C.GREEN + "Importing " + C.WHITE + total + C.GREEN + " vanilla structures (mode=" + mode + ", includeNonJigsaw=" + includeNonJigsaw + ")..."); + + for (String keyString : vanilla) { + NamespacedKey nk = NamespacedKey.fromString(keyString.toLowerCase()); + if (nk == null) { + failed++; + sender.sendMessage(C.RED + "[fail] " + keyString + ": invalid key"); + continue; + } + String name = StructureImporter.deriveName(nk); + + try { + VillageImporter.Result jigsaw = VillageImporter.importVillage(data, nk, name, mode); + if (jigsaw.success()) { + imported++; + sender.sendMessage(C.GRAY + "[jigsaw] " + keyString + " -> " + name); + continue; + } + + String message = jigsaw.message() == null ? "" : jigsaw.message(); + if (message.contains("is not a jigsaw structure")) { + if (!includeNonJigsaw) { + skipped++; + continue; + } + StructureImporter.Result single = StructureImporter.importStructure(data, nk, name, mode); + if (single.success()) { + imported++; + sender.sendMessage(C.GRAY + "[single] " + keyString + " -> " + name); + } else if (single.message() != null && single.message().startsWith("Skipped")) { + skipped++; + } else { + failed++; + sender.sendMessage(C.RED + "[fail] " + keyString + ": " + single.message()); + } + continue; + } + + if (message.startsWith("Skipped")) { + skipped++; + continue; + } + + failed++; + sender.sendMessage(C.RED + "[fail] " + keyString + ": " + message); + } catch (Throwable e) { + failed++; + sender.sendMessage(C.RED + "[fail] " + keyString + ": " + e.getMessage()); + } + } + + StructureIndexService.write(data); + + sender.sendMessage(C.GREEN + "Bulk import complete: " + C.WHITE + imported + C.GREEN + " imported, " + C.WHITE + skipped + C.GREEN + " skipped, " + C.WHITE + failed + C.GREEN + " failed (" + C.WHITE + total + C.GREEN + " total)."); + return new Report(total, imported, skipped, failed); + } + + public static Report importAllTemplates(IrisData data, StructureImporter.Mode mode, VolmitSender sender) { + List templateKeys; + try { + templateKeys = enumerateTemplateKeys(); + } catch (Throwable e) { + sender.sendMessage(C.RED + "Failed to enumerate vanilla structure templates via the server ResourceManager: " + e); + return new Report(0, 0, 0, 0); + } + + List vanilla = new ArrayList<>(); + for (String key : templateKeys) { + if (key != null && key.startsWith("minecraft:")) { + vanilla.add(key); + } + } + Collections.sort(vanilla); + + int total = vanilla.size(); + int imported = 0; + int skipped = 0; + int failed = 0; + + if (total == 0) { + sender.sendMessage(C.YELLOW + "No vanilla structure templates were found under the 'structure' resource path."); + return new Report(0, 0, 0, 0); + } + + sender.sendMessage(C.GREEN + "Importing " + C.WHITE + total + C.GREEN + " vanilla structure templates (mode=" + mode + ")..."); + + for (String keyString : vanilla) { + NamespacedKey nk = NamespacedKey.fromString(keyString.toLowerCase()); + if (nk == null) { + failed++; + sender.sendMessage(C.RED + "[fail] " + keyString + ": invalid key"); + continue; + } + String name = templateNameFor(keyString); + + try { + StructureImporter.Result result = StructureImporter.importStructure(data, nk, name, mode); + if (result.success()) { + imported++; + } else if (result.message() != null && result.message().startsWith("Skipped")) { + skipped++; + } else { + failed++; + sender.sendMessage(C.RED + "[fail] " + keyString + ": " + result.message()); + } + } catch (Throwable e) { + failed++; + sender.sendMessage(C.RED + "[fail] " + keyString + ": " + e.getMessage()); + } + + int processed = imported + skipped + failed; + if (processed % 50 == 0) { + sender.sendMessage(C.GRAY + "..." + processed + "/" + total + " (" + imported + " imported, " + skipped + " skipped, " + failed + " failed)"); + } + } + + StructureIndexService.write(data); + + sender.sendMessage(C.GREEN + "Template import complete: " + C.WHITE + imported + C.GREEN + " imported, " + C.WHITE + skipped + C.GREEN + " skipped, " + C.WHITE + failed + C.GREEN + " failed (" + C.WHITE + total + C.GREEN + " total)."); + return new Report(total, imported, skipped, failed); + } + + private static String templateNameFor(String key) { + int colon = key.indexOf(':'); + String namespace = colon >= 0 ? key.substring(0, colon) : "minecraft"; + String path = colon >= 0 ? key.substring(colon + 1) : key; + String safePath = path.toLowerCase().replaceAll("[^a-z0-9_/-]", "_"); + while (safePath.contains("//")) { + safePath = safePath.replace("//", "/"); + } + if (safePath.startsWith("/")) { + safePath = safePath.substring(1); + } + return "minecraft".equals(namespace) ? safePath : namespace + "/" + safePath; + } + + private static List enumerateTemplateKeys() throws Exception { + Object craftServer = Bukkit.getServer(); + Object dedicated = invoke(craftServer, "getHandle"); + Object server = invoke(dedicated, "getServer"); + Object resourceManager = resolveResourceManager(server); + if (resourceManager == null) { + throw new IllegalStateException("Could not resolve the server ResourceManager via reflection"); + } + + Method listResources = null; + for (Method m : resourceManager.getClass().getMethods()) { + if (m.getName().equals("listResources") && m.getParameterCount() == 2 + && m.getParameterTypes()[0] == String.class + && m.getParameterTypes()[1] == Predicate.class + && Map.class.isAssignableFrom(m.getReturnType())) { + listResources = m; + break; + } + } + if (listResources == null) { + throw new NoSuchMethodException("listResources(String, Predicate) on " + resourceManager.getClass().getName()); + } + listResources.setAccessible(true); + + Predicate endsWithNbt = location -> { + String path = identifierPath(location); + return path != null && path.endsWith(".nbt"); + }; + + Object resultMap = listResources.invoke(resourceManager, "structure", endsWithNbt); + TreeSet keys = new TreeSet<>(); + if (resultMap instanceof Map map) { + for (Object location : map.keySet()) { + String namespace = identifierNamespace(location); + String path = identifierPath(location); + if (namespace == null || path == null) { + continue; + } + String stripped = path; + if (stripped.startsWith("structure/")) { + stripped = stripped.substring("structure/".length()); + } + if (stripped.endsWith(".nbt")) { + stripped = stripped.substring(0, stripped.length() - ".nbt".length()); + } + if (stripped.isEmpty()) { + continue; + } + keys.add(namespace + ":" + stripped); + } + } + return new ArrayList<>(keys); + } + + private static Object resolveResourceManager(Object server) { + try { + Class resourceManagerClass = Class.forName("net.minecraft.server.packs.resources.ResourceManager"); + Method getter = null; + for (Method m : server.getClass().getMethods()) { + if (m.getName().equals("getResourceManager") && m.getParameterCount() == 0 + && resourceManagerClass.isAssignableFrom(m.getReturnType())) { + getter = m; + break; + } + } + if (getter == null) { + for (Method m : server.getClass().getMethods()) { + if (m.getParameterCount() == 0 && resourceManagerClass.isAssignableFrom(m.getReturnType())) { + getter = m; + break; + } + } + } + if (getter == null) { + return null; + } + getter.setAccessible(true); + return getter.invoke(server); + } catch (Throwable e) { + return null; + } + } + + private static String identifierNamespace(Object location) { + try { + Method m = location.getClass().getMethod("getNamespace"); + m.setAccessible(true); + Object value = m.invoke(location); + return value == null ? null : value.toString(); + } catch (Throwable e) { + return null; + } + } + + private static String identifierPath(Object location) { + try { + Method m = location.getClass().getMethod("getPath"); + m.setAccessible(true); + Object value = m.invoke(location); + return value == null ? null : value.toString(); + } catch (Throwable e) { + return null; + } + } + + private static Object invoke(Object target, String method) throws Exception { + Class c = target.getClass(); + while (c != null) { + for (Method m : c.getDeclaredMethods()) { + if (m.getName().equals(method) && m.getParameterCount() == 0) { + m.setAccessible(true); + return m.invoke(target); + } + } + c = c.getSuperclass(); + } + for (Method m : target.getClass().getMethods()) { + if (m.getName().equals(method) && m.getParameterCount() == 0) { + m.setAccessible(true); + return m.invoke(target); + } + } + throw new NoSuchMethodException(method + " on " + target.getClass().getName()); + } +} 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 index de8595767..daf436713 100644 --- a/core/src/main/java/art/arcane/iris/core/structure/StructureImporter.java +++ b/core/src/main/java/art/arcane/iris/core/structure/StructureImporter.java @@ -66,6 +66,10 @@ public final class StructureImporter { return (key.getNamespace() + "_" + key.getKey()).replace('/', '_').replace(':', '_'); } + public static String deriveName(String key) { + return key.toLowerCase().replace('/', '_').replace(':', '_'); + } + public static Result importStructure(IrisData data, NamespacedKey key, String name, Mode mode) { Structure structure; try { 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 index 6dad17e74..adb000053 100644 --- a/core/src/main/java/art/arcane/iris/core/structure/VillageImporter.java +++ b/core/src/main/java/art/arcane/iris/core/structure/VillageImporter.java @@ -19,8 +19,10 @@ package art.arcane.iris.core.structure; import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.object.IrisObject; import com.google.gson.GsonBuilder; import org.bukkit.Bukkit; +import org.bukkit.Material; import org.bukkit.NamespacedKey; import java.io.File; @@ -98,6 +100,10 @@ public final class VillageImporter { return new Result(false, "Could not resolve the start pool key for " + structureKey, 0, 0); } + if (mode == StructureImporter.Mode.ADD_ONLY && new File(data.getDataFolder(), "structures/" + name + ".json").exists()) { + return new Result(false, "Skipped (add-only): '" + name + "' already exists", 0, 0); + } + Set visitedPools = new HashSet<>(); Set importedPieces = new HashSet<>(); Deque poolQueue = new ArrayDeque<>(); @@ -106,6 +112,7 @@ public final class VillageImporter { Map> emittedPools = new LinkedHashMap<>(); Map> emittedPieces = new LinkedHashMap<>(); List errors = new ArrayList<>(); + String emptyPieceName = null; int pieceBlocks = 0; while (!poolQueue.isEmpty()) { @@ -161,6 +168,21 @@ public final class VillageImporter { } String templateLocation = templateLocationOf(element); if (templateLocation == null) { + String elementType = element.getClass().getSimpleName(); + if (elementType.endsWith("EmptyPoolElement")) { + if (emptyPieceName == null) { + emptyPieceName = name + "/piece/empty"; + if (importedPieces.add(emptyPieceName) && writeEmptyPiece(data, emptyPieceName)) { + emittedPieces.put(emptyPieceName, pieceJson(emptyPieceName, new ArrayList>())); + } + } + Map emptyEntry = new LinkedHashMap<>(); + emptyEntry.put("piece", emptyPieceName); + emptyEntry.put("weight", weight); + pieceEntries.add(emptyEntry); + } else { + errors.add("skipped unsupported element " + elementType + " in pool " + poolKey); + } continue; } NamespacedKey pieceNbtKey = NamespacedKey.fromString(templateLocation.toLowerCase()); @@ -588,6 +610,19 @@ public final class VillageImporter { new File(data.getDataFolder(), "structures/" + pieceName + ".json").delete(); } + private static boolean writeEmptyPiece(IrisData data, String pieceName) { + try { + File objectFile = new File(data.getDataFolder(), "objects/" + pieceName + ".iob"); + objectFile.getParentFile().mkdirs(); + IrisObject object = new IrisObject(1, 1, 1); + object.setUnsigned(0, 0, 0, Material.AIR.createBlockData()); + object.write(objectFile); + return true; + } catch (Throwable e) { + return false; + } + } + private static void writeJson(File file, Map content) throws Exception { file.getParentFile().mkdirs(); String json = new GsonBuilder().setPrettyPrinting().create().toJson(content); 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 index 783217033..1955df480 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/PlacedStructurePiece.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/PlacedStructurePiece.java @@ -41,8 +41,8 @@ public class PlacedStructurePiece { 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; + 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/mantle/components/IrisStructureComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java index 067f273cc..df89ec119 100644 --- 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 @@ -86,9 +86,22 @@ public class IrisStructureComponent extends IrisMantleComponent { 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; + int baseY; + if (placement.isUnderground()) { + int worldMinY = getEngineMantle().getEngine().getMinHeight() + 1; + int worldMaxY = getEngineMantle().getEngine().getMinHeight() + getEngineMantle().getEngine().getHeight() - 1; + int bandMin = Math.max(worldMinY, Math.min(placement.getMinHeight(), placement.getMaxHeight())); + int bandMax = Math.min(worldMaxY, Math.max(placement.getMinHeight(), placement.getMaxHeight())); + if (bandMin > bandMax) { + return; + } + baseY = bandMin == bandMax ? bandMin : rng.i(bandMin, bandMax); + } else { + int surfaceY = getEngineMantle().getEngine().getHeight(sx, sz, true) + getEngineMantle().getEngine().getMinHeight(); + if (surfaceY < placement.getMinHeight() || surfaceY > placement.getMaxHeight()) { + return; + } + baseY = surfaceY; } String key = placement.getStructures().get(rng.i(0, placement.getStructures().size() - 1)); @@ -97,14 +110,14 @@ public class IrisStructureComponent extends IrisMantleComponent { return; } - StructureAssembler assembler = new StructureAssembler(getData(), structure, sx, surfaceY, sz); + StructureAssembler assembler = new StructureAssembler(getData(), structure, sx, baseY, sz); KList pieces = assembler.assemble(rng); if (pieces == null || pieces.isEmpty()) { return; } ObjectPlaceMode mode = structure.getPlaceMode(); - if (mode == ObjectPlaceMode.STRUCTURE_PIECE || mode == ObjectPlaceMode.FLOATING) { + if (placement.isUnderground() || mode == ObjectPlaceMode.STRUCTURE_PIECE || mode == ObjectPlaceMode.FLOATING) { for (PlacedStructurePiece p : pieces) { placeObject(writer, structure, p, ObjectPlaceMode.STRUCTURE_PIECE, p.getY(), rng); } @@ -115,7 +128,7 @@ public class IrisStructureComponent extends IrisMantleComponent { for (PlacedStructurePiece p : pieces) { lowest = Math.min(lowest, p.getMinY()); } - int shift = surfaceY - lowest; + int shift = baseY - lowest; for (PlacedStructurePiece p : pieces) { placeObject(writer, structure, p, ObjectPlaceMode.STRUCTURE_PIECE, p.getY() + shift, rng); } 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 1337cdfdd..7b8097361 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 @@ -507,6 +507,9 @@ public class MantleObjectComponent extends IrisMantleComponent { int density = objectPlacement.getDensity(rng, minX, minZ, getData()); KMap> anchorCache = new KMap<>(); IrisCaveAnchorMode anchorMode = resolveAnchorMode(objectPlacement, caveProfile); + if (objectPlacement.getMode() == ObjectPlaceMode.CEILING_HANG) { + anchorMode = IrisCaveAnchorMode.CEILING; + } int anchorScanStep = resolveAnchorScanStep(caveProfile); int objectMinDepthBelowSurface = resolveObjectMinDepthBelowSurface(caveProfile); int anchorSearchAttempts = resolveAnchorSearchAttempts(caveProfile); @@ -595,7 +598,11 @@ public class MantleObjectComponent extends IrisMantleComponent { try { 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) -> { + int placeY = y; + if (effectivePlacement.getMode() == ObjectPlaceMode.CEILING_HANG) { + placeY = Math.max(1, caveCeiling - 1 - Math.floorDiv(object.getH(), 2)); + } + int result = object.place(x, placeY, z, clampedPlacer, effectivePlacement, rng, (b, data) -> { wrotePlacementData.set(true); String marker = placementMarker(object, id, "cave"); if (marker != null) { 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 640e81caf..1a35751bd 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 @@ -692,9 +692,12 @@ public class IrisObject extends IrisRegistrant { boolean warped = !config.getWarp().isFlat(); boolean rawStructurePiece = config.getMode() == ObjectPlaceMode.STRUCTURE_PIECE; + boolean organicFloor = config.getMode() == ObjectPlaceMode.ORGANIC_STILT; + boolean ceilingHang = config.getMode() == ObjectPlaceMode.CEILING_HANG; + boolean organic = organicFloor || ceilingHang; boolean stilting = (config.getMode().equals(ObjectPlaceMode.STILT) || config.getMode().equals(ObjectPlaceMode.FAST_STILT) || config.getMode() == ObjectPlaceMode.MIN_STILT || config.getMode() == ObjectPlaceMode.FAST_MIN_STILT || - config.getMode() == ObjectPlaceMode.CENTER_STILT || config.getMode() == ObjectPlaceMode.ERODE_STILT); + config.getMode() == ObjectPlaceMode.CENTER_STILT || config.getMode() == ObjectPlaceMode.ERODE_STILT || organic); boolean eroding = config.getMode() == ObjectPlaceMode.ERODE_STILT; KMap heightmap = config.getSnow() > 0 ? new KMap<>() : null; int spinx = rng.imax() / 1000; @@ -717,7 +720,8 @@ public class IrisObject extends IrisRegistrant { } } } else if (yv < 0) { - if (config.getMode().equals(ObjectPlaceMode.CENTER_HEIGHT) || config.getMode() == ObjectPlaceMode.CENTER_STILT) { + if (config.getMode().equals(ObjectPlaceMode.CENTER_HEIGHT) || config.getMode() == ObjectPlaceMode.CENTER_STILT + || organic) { y = (c != null ? c.getSurface() : placer.getHighest(x, z, getLoader(), config.isUnderwater())) + rty; if (!config.isForcePlace()) { if (shouldBailForCarvingAnchor(placer, config, x, y, z)) { @@ -923,6 +927,7 @@ public class IrisObject extends IrisRegistrant { } int lowest = Integer.MAX_VALUE; + int topLayer = Integer.MIN_VALUE; y += yrand; readLock.lock(); @@ -990,10 +995,18 @@ public class IrisObject extends IrisRegistrant { BlockVector i = g.clone(); BlockData data = d.clone(); i = config.getRotation().rotate(i.clone(), spinx, spiny, spinz).clone(); + if (ceilingHang) { + i.setY(-i.getBlockY()); + } i = config.getTranslate().translate(i.clone(), config.getRotation(), spinx, spiny, spinz).clone(); - if (stilting && i.getBlockY() < lowest && shouldStilt(data)) { - lowest = i.getBlockY(); + if (stilting && shouldStilt(data)) { + if (i.getBlockY() < lowest) { + lowest = i.getBlockY(); + } + if (i.getBlockY() > topLayer) { + topLayer = i.getBlockY(); + } } if (placer.isPreventingDecay() && (data) instanceof Leaves && !((Leaves) (data)).isPersistent()) { @@ -1162,10 +1175,14 @@ public class IrisObject extends IrisRegistrant { BlockVector i = g.clone(); i = config.getRotation().rotate(i.clone(), spinx, spiny, spinz).clone(); + if (ceilingHang) { + i.setY(-i.getBlockY()); + } i = config.getTranslate().translate(i.clone(), config.getRotation(), spinx, spiny, spinz).clone(); d = config.getRotation().rotate(d, spinx, spiny, spinz); - if (i.getBlockY() != lowest) + int targetLayer = ceilingHang ? topLayer : lowest; + if (i.getBlockY() != targetLayer) continue; for (IrisObjectReplace j : config.getEdit()) { @@ -1195,14 +1212,67 @@ public class IrisObject extends IrisRegistrant { zz += config.warp(rng, i.getZ() + z, i.getY() + y, i.getX() + x, getLoader()); } + if (organic) { + int startY = targetLayer + y; + int maxScan = settings != null ? Math.max(1, settings.getOrganicMaxScan()) : 48; + int jitterMax = settings != null ? Math.max(0, settings.getOrganicJitter()) : 3; + double scratch = settings != null ? Math.max(0, Math.min(1, settings.getOrganicScratch())) : 0.55; + long colHash = ((long) xx * 341873128712L) ^ ((long) zz * 132897987541L); + int jitter = jitterMax > 0 ? (int) (Math.abs(colHash) % (jitterMax + 1)) : 0; + + if (ceilingHang) { + int scan = 0; + int solidY = startY + 1; + while (scan < maxScan && !placer.isSolid(xx, solidY, zz)) { + solidY++; + scan++; + } + int topBound = (scan < maxScan ? solidY - 1 : startY + Math.min(maxScan, 8)) - jitter; + int total = topBound - startY; + for (int j = startY; j <= topBound; j++) { + if (scratch > 0 && total > 0) { + double ratio = (double) (j - startY) / total; + if (ratio > (1.0 - scratch)) { + long sh = ((long) xx * 341873128712L) ^ ((long) j * 132897987541L) ^ ((long) zz * 735791245321L); + double skipChance = (ratio - (1.0 - scratch)) / scratch; + if ((Math.abs(sh) % 1000) / 1000.0 < skipChance * 0.7) { + continue; + } + } + } + placer.set(xx, j, zz, d); + } + } else { + int scan = 0; + int solidY = startY - 1; + while (scan < maxScan && !placer.isSolid(xx, solidY, zz)) { + solidY--; + scan++; + } + int bottomBound = (scan < maxScan ? solidY + 1 : startY - Math.min(maxScan, 8)) + jitter; + int total = startY - bottomBound; + for (int j = startY; j >= bottomBound; j--) { + if (scratch > 0 && total > 0) { + double ratio = (double) (startY - j) / total; + if (ratio > (1.0 - scratch)) { + long sh = ((long) xx * 341873128712L) ^ ((long) j * 132897987541L) ^ ((long) zz * 735791245321L); + double skipChance = (ratio - (1.0 - scratch)) / scratch; + if ((Math.abs(sh) % 1000) / 1000.0 < skipChance * 0.7) { + continue; + } + } + } + placer.set(xx, j, zz, d); + } + } + continue; + } + int highest = placer.getHighest(xx, zz, getLoader(), true); if (d instanceof Waterlogged && shouldAutoWaterlogBlock(placer, config, yv, xx, highest, zz)) ((Waterlogged) d).setWaterlogged(true); - if (yv >= 0 && config.isBottom()) - y += Math.floorDiv(h, 2); - int lowerBound = highest - 1; if (settings != null) { lowerBound -= config.getStiltSettings().getOverStilt() - rng.i(0, config.getStiltSettings().getYRand()); @@ -1221,6 +1291,9 @@ public class IrisObject extends IrisRegistrant { } for (int j = lowest + y; j > lowerBound; j--) { + if (B.isFluid(placer.get(xx, j, zz))) { + break; + } if (eroding) { int depth = (lowest + y) - j; int totalDepth = (lowest + y) - lowerBound; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisStiltSettings.java b/core/src/main/java/art/arcane/iris/engine/object/IrisStiltSettings.java index 87047efd9..518cbe945 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisStiltSettings.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisStiltSettings.java @@ -30,5 +30,17 @@ public class IrisStiltSettings { private int overStilt; @Desc("If defined, stilting will be done using this block palette rather than the last layer of the object.") private IrisMaterialPalette palette; + @MinNumber(1) + @MaxNumber(256) + @Desc("For ORGANIC_STILT / CEILING_HANG: the maximum number of blocks to scan toward the cave floor (or ceiling) looking for solid ground before giving up.") + private int organicMaxScan = 48; + @MinNumber(0) + @MaxNumber(32) + @Desc("For ORGANIC_STILT / CEILING_HANG: the maximum number of blocks each column's stilt is randomly shortened by, giving the underside a ragged organic edge instead of a flat disc.") + private int organicJitter = 3; + @MinNumber(0) + @MaxNumber(1) + @Desc("For ORGANIC_STILT / CEILING_HANG: in the deepest fraction of each stilt column, blocks are randomly skipped to break the tip up. Higher = more broken and scratchy. 0 disables.") + private double organicScratch = 0.55; } 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 index e694d077f..4cb4275b7 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisStructurePlacement.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisStructurePlacement.java @@ -90,12 +90,15 @@ public class IrisStructurePlacement { @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.") + @Desc("IRIS_PLACED: when underground=false this is the minimum surface Y the placement is allowed at (a gate); when underground=true this is the lower bound of the Y band the structure is placed within.") private int minHeight = -2032; - @Desc("The maximum world Y this structure may start at.") + @Desc("IRIS_PLACED: when underground=false this is the maximum surface Y the placement is allowed at (a gate); when underground=true this is the upper bound of the Y band the structure is placed within.") private int maxHeight = 2032; + @Desc("IRIS_PLACED only: if true the structure is placed underground at a random world Y inside [minHeight, maxHeight] (raw stamp, no terrain matching) instead of being dropped onto the terrain surface. Use this for deep structures like ancient cities in a deep cave band.") + private boolean underground = false; + @Desc("If false, this placement is skipped underwater.") private boolean underwater = false; } diff --git a/core/src/main/java/art/arcane/iris/engine/object/ObjectPlaceMode.java b/core/src/main/java/art/arcane/iris/engine/object/ObjectPlaceMode.java index 5496bb4fa..3632e2093 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/ObjectPlaceMode.java +++ b/core/src/main/java/art/arcane/iris/engine/object/ObjectPlaceMode.java @@ -66,6 +66,14 @@ public enum ObjectPlaceMode { ERODE_STILT, + @Desc("Organic stilting scans straight down from the bottom of the object to the first solid block (the cave floor, or terrain) and fills the gap with the object's own bottom block. Each column stops at a slightly different, noise-varied depth and the lower portion is randomly broken up, so the underside reads as rough organic roots rather than a flat disc. Designed for objects placed inside caves so they connect to the floor instead of floating. Tune with stilt-settings (organicMaxScan, organicJitter, organicScratch).") + + ORGANIC_STILT, + + @Desc("Hangs the object upside-down from a cave ceiling. The object is flipped vertically (its tip points down like a stalactite) and its top is anchored to the ceiling, while an organic, noise-varied stilt grows up into the ceiling so it never floats below the roof. Use inside caves; tune with stilt-settings (organicMaxScan, organicJitter, organicScratch).") + + CEILING_HANG, + @Desc("Samples the height of the terrain at every x,z position of your object and pushes it down to the surface. It's pretty much like a melt function over the terrain.") PAINT,