From 11efae428f2088e0d2581e056f8e8bd2eb087c60 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Wed, 3 Jun 2026 01:21:14 -0400 Subject: [PATCH] x --- core/plugins/Iris/cache/instance | 2 +- .../art/arcane/iris/core/IrisSettings.java | 16 + .../iris/core/commands/CommandDatapack.java | 12 +- .../iris/core/commands/CommandDeveloper.java | 1586 +---------------- .../iris/core/commands/CommandEdit.java | 8 +- .../iris/core/commands/CommandFind.java | 8 + .../iris/core/commands/CommandIris.java | 24 +- .../iris/core/commands/CommandObject.java | 20 +- .../iris/core/commands/CommandPack.java | 12 +- .../iris/core/commands/CommandPregen.java | 8 +- .../iris/core/commands/CommandStructure.java | 158 +- .../iris/core/commands/CommandStudio.java | 31 +- .../iris/core/commands/CommandWhat.java | 8 +- .../core/datapack/DatapackIngestService.java | 131 +- .../arcane/iris/core/gui/PregeneratorJob.java | 9 + .../art/arcane/iris/core/nms/INMSBinding.java | 16 + .../iris/core/nms/v1X/NMSBinding1X.java | 5 + .../core/pregenerator/IrisPregenerator.java | 1 + .../methods/AsyncPregenMethod.java | 80 + .../methods/RegenPregenMethod.java | 104 ++ .../iris/core/project/SchemaBuilder.java | 35 + .../iris/core/service/IrisEngineSVC.java | 35 +- .../core/structure/BulkStructureImporter.java | 130 +- .../core/structure/StructureImporter.java | 5 +- .../core/structure/StructureModeHandler.java | 53 - .../core/structure/StructureSchemaKeys.java | 66 + .../iris/core/structure/VillageImporter.java | 5 +- .../framework/StructureReachability.java | 122 ++ .../iris/engine/mantle/MatterGenerator.java | 171 +- .../engine/modifier/IrisCarveModifier.java | 2 +- .../engine/modifier/IrisDepositModifier.java | 29 +- .../object/IrisImportedStructureControl.java | 10 + .../engine/object/IrisStructurePlacement.java | 6 +- .../annotations/RegistryListStructure.java} | 12 +- .../engine/platform/BukkitChunkGenerator.java | 289 --- .../platform/ChunkReplacementListener.java | 33 - .../platform/ChunkReplacementOptions.java | 51 - .../platform/PlatformChunkGenerator.java | 10 - .../util/common/director/DirectorHelp.java | 118 -- .../iris/util/common/misc/RegenRuntime.java | 20 - .../WorldLifecycleRuntimeLevelStemTest.java | 7 - ...BulkStructureImporterTemplateNameTest.java | 54 + .../structure/StructureImporterModeTest.java | 71 + .../structure/StructureSchemaKeysTest.java | 157 ++ .../IrisStructureLocatorContractTest.java | 74 + .../StructureReachabilityContractTest.java | 79 + .../IrisImportedStructureControlTest.java | 180 ++ ...BukkitChunkGeneratorOverlayPolicyTest.java | 49 - .../core/nms/v1_21_R7/CustomBiomeSource.java | 122 +- .../core/nms/v1_21_R7/IrisChunkGenerator.java | 41 +- .../iris/core/nms/v1_21_R7/NMSBinding.java | 77 + .../nms/v1_21_R7/VanillaStructureBiomes.java | 77 + .../core/nms/v26_1_R1/CustomBiomeSource.java | 122 +- .../core/nms/v26_1_R1/IrisChunkGenerator.java | 41 +- .../iris/core/nms/v26_1_R1/NMSBinding.java | 77 + .../nms/v26_1_R1/VanillaStructureBiomes.java | 77 + 56 files changed, 2052 insertions(+), 2694 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/core/pregenerator/methods/RegenPregenMethod.java delete mode 100644 core/src/main/java/art/arcane/iris/core/structure/StructureModeHandler.java create mode 100644 core/src/main/java/art/arcane/iris/core/structure/StructureSchemaKeys.java create mode 100644 core/src/main/java/art/arcane/iris/engine/framework/StructureReachability.java rename core/src/main/java/art/arcane/iris/{core/commands/CommandSettings.java => engine/object/annotations/RegistryListStructure.java} (69%) delete mode 100644 core/src/main/java/art/arcane/iris/engine/platform/ChunkReplacementListener.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/platform/ChunkReplacementOptions.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/director/DirectorHelp.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/misc/RegenRuntime.java create mode 100644 core/src/test/java/art/arcane/iris/core/structure/BulkStructureImporterTemplateNameTest.java create mode 100644 core/src/test/java/art/arcane/iris/core/structure/StructureImporterModeTest.java create mode 100644 core/src/test/java/art/arcane/iris/core/structure/StructureSchemaKeysTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/framework/IrisStructureLocatorContractTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/framework/StructureReachabilityContractTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/object/IrisImportedStructureControlTest.java delete mode 100644 core/src/test/java/art/arcane/iris/engine/platform/BukkitChunkGeneratorOverlayPolicyTest.java create mode 100644 nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/VanillaStructureBiomes.java create mode 100644 nms/v26_1_R1/src/main/java/art/arcane/iris/core/nms/v26_1_R1/VanillaStructureBiomes.java diff --git a/core/plugins/Iris/cache/instance b/core/plugins/Iris/cache/instance index b9b283d16..dec88e772 100644 --- a/core/plugins/Iris/cache/instance +++ b/core/plugins/Iris/cache/instance @@ -1 +1 @@ -446774718 \ No newline at end of file +-676456073 \ No newline at end of file diff --git a/core/src/main/java/art/arcane/iris/core/IrisSettings.java b/core/src/main/java/art/arcane/iris/core/IrisSettings.java index 8813a11ea..194d4d686 100644 --- a/core/src/main/java/art/arcane/iris/core/IrisSettings.java +++ b/core/src/main/java/art/arcane/iris/core/IrisSettings.java @@ -146,11 +146,26 @@ public class IrisSettings { public int chunkLoadTimeoutSeconds = 15; public int timeoutWarnIntervalMs = 500; public int saveIntervalMs = 30_000; + public int maxResidentTectonicPlates = 96; + public int mantleBackpressureWaitMs = 25; + public int mantleBackpressureTimeoutMs = 60_000; public int getChunkLoadTimeoutSeconds() { return Math.max(5, Math.min(chunkLoadTimeoutSeconds, 120)); } + public int getMaxResidentTectonicPlates() { + return Math.max(16, maxResidentTectonicPlates); + } + + public int getMantleBackpressureWaitMs() { + return Math.max(5, Math.min(mantleBackpressureWaitMs, 1_000)); + } + + public int getMantleBackpressureTimeoutMs() { + return Math.max(5_000, Math.min(mantleBackpressureTimeoutMs, 600_000)); + } + public int getTimeoutWarnIntervalMs() { return Math.max(timeoutWarnIntervalMs, 250); } @@ -199,6 +214,7 @@ public class IrisSettings { public boolean useCustomColorsIngame = true; public boolean adjustVanillaHeight = false; public boolean autoIngestDatapacks = true; + public boolean autoImportDatapackStructures = true; public String forceMainWorld = ""; public int spinh = -20; public int spins = 7; diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandDatapack.java b/core/src/main/java/art/arcane/iris/core/commands/CommandDatapack.java index 3579f5b16..bac2757e1 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandDatapack.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandDatapack.java @@ -21,7 +21,6 @@ package art.arcane.iris.core.commands; import art.arcane.iris.core.datapack.DatapackIngestService; 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.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.scheduling.J; @@ -32,12 +31,7 @@ import java.util.List; @Director(name = "datapack", aliases = {"datapacks", "dp"}, description = "Download & manage external datapack imports (Modrinth)") public class CommandDatapack implements DirectorExecutor { - @Director(description = "Show help tree for this command group", aliases = {"?"}) - public void help() { - DirectorHelp.print(sender(), getClass()); - } - - @Director(description = "Download/update every datapack listed in a pack dimension's 'datapackImports' and install it into the world so its structures register like vanilla", aliases = {"pull", "update", "sync"}) + @Director(description = "Download/update every datapack listed in a pack dimension's 'datapackImports' and install it into the world so its structures register like vanilla", aliases = {"pull"}) public void ingest( @Param(description = "Restart the server when new datapacks are installed (required for new structures to register and generate)", defaultValue = "false") boolean restart @@ -47,7 +41,7 @@ public class CommandDatapack implements DirectorExecutor { J.a(() -> DatapackIngestService.ingestAll(sender, restart)); } - @Director(description = "List configured datapack imports and their installed versions", aliases = {"ls", "status"}) + @Director(description = "List configured datapack imports and their installed versions", aliases = {"ls"}) public void list() { VolmitSender sender = sender(); KList configured = DatapackIngestService.collectConfiguredImports(); @@ -68,7 +62,7 @@ public class CommandDatapack implements DirectorExecutor { } } - @Director(description = "Remove an installed datapack by id (also delete its URL from datapackImports to keep it gone)", aliases = {"rm", "delete"}) + @Director(description = "Remove an installed datapack by id (also delete its URL from datapackImports to keep it gone)", aliases = {"rm"}) public void remove( @Param(description = "The datapack id (folder name) shown by /iris datapack list") String id diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java b/core/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java index 645c01e87..5a836960e 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java @@ -24,9 +24,10 @@ import com.google.gson.JsonObject; import art.arcane.iris.Iris; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.ServerConfigurator; -import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.nms.INMS; import art.arcane.iris.core.nms.datapack.DataVersion; +import art.arcane.iris.core.pregenerator.PregenTask; +import art.arcane.iris.core.pregenerator.methods.RegenPregenMethod; import art.arcane.iris.core.service.IrisEngineSVC; import art.arcane.iris.core.service.StudioSVC; import art.arcane.iris.core.tools.IrisPackBenchmarking; @@ -34,48 +35,31 @@ import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.IrisEngineMantle; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IrisDimension; -import art.arcane.iris.engine.platform.ChunkReplacementListener; -import art.arcane.iris.engine.platform.ChunkReplacementOptions; import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.volmlib.util.collection.KSet; import art.arcane.iris.util.project.context.IrisContext; import art.arcane.volmlib.util.collection.KList; -import art.arcane.iris.util.common.director.DirectorContext; import art.arcane.iris.util.common.director.DirectorExecutor; -import art.arcane.iris.util.common.director.DirectorHelp; 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.iris.util.common.director.specialhandlers.NullableDimensionHandler; -import art.arcane.iris.util.common.misc.RegenRuntime; import art.arcane.iris.util.common.format.C; import art.arcane.volmlib.util.format.Form; import art.arcane.volmlib.util.io.CountingDataInputStream; -import art.arcane.volmlib.util.io.IO; import art.arcane.volmlib.util.mantle.runtime.TectonicPlate; import art.arcane.volmlib.util.math.Position2; import art.arcane.volmlib.util.math.M; import art.arcane.volmlib.util.matter.Matter; import art.arcane.iris.util.nbt.common.mca.MCAFile; import art.arcane.iris.util.nbt.common.mca.MCAUtil; -import art.arcane.iris.util.common.parallel.MultiBurst; -import art.arcane.iris.util.common.parallel.SyncExecutor; import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.scheduling.J; -import art.arcane.iris.util.common.scheduling.jobs.Job; import lombok.SneakyThrows; -import net.jpountz.lz4.LZ4BlockInputStream; -import net.jpountz.lz4.LZ4BlockOutputStream; -import net.jpountz.lz4.LZ4FrameInputStream; -import net.jpountz.lz4.LZ4FrameOutputStream; -import org.apache.commons.lang.RandomStringUtils; import org.bukkit.Bukkit; import org.bukkit.Chunk; import org.bukkit.World; -import org.bukkit.boss.BarColor; -import org.bukkit.boss.BarStyle; -import org.bukkit.boss.BossBar; import org.bukkit.entity.Player; import java.io.*; @@ -86,16 +70,9 @@ import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.*; import java.util.stream.Collectors; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; @Director(name = "Developer", origin = DirectorOrigin.BOTH, description = "Iris World Manager", aliases = {"dev"}) public class CommandDeveloper implements DirectorExecutor { - @Director(description = "Show help tree for this command group", aliases = {"?"}) - public void help() { - DirectorHelp.print(sender(), getClass()); - } - @Director(description = "Get Loaded TectonicPlates Count", origin = DirectorOrigin.BOTH, sync = true) public void EngineStatus() { Iris.service(IrisEngineSVC.class) @@ -209,53 +186,6 @@ public class CommandDeveloper implements DirectorExecutor { return z ^ (z >>> 31); } - @Director(description = "QOL command to open an overworld studio world", sync = true) - public void so() { - sender().sendMessage(C.GREEN + "Opening studio for the \"Overworld\" pack (seed: 1337)"); - Iris.service(StudioSVC.class).open(sender(), 1337, "overworld"); - } - - @Director(description = "Set aura spins") - public void aura( - @Param(description = "The h color value", defaultValue = "-20") - int h, - @Param(description = "The s color value", defaultValue = "7") - int s, - @Param(description = "The b color value", defaultValue = "8") - int b - ) { - IrisSettings.get().getGeneral().setSpinh(h); - IrisSettings.get().getGeneral().setSpins(s); - IrisSettings.get().getGeneral().setSpinb(b); - IrisSettings.get().forceSave(); - sender().sendMessage("Aura Spins updated to " + h + " " + s + " " + b); - } - - @Director(description = "Bitwise calculations") - public void bitwise( - @Param(description = "The first value to run calculations on") - int value1, - @Param(description = "The operator: | & ^ << >> %") - String operator, - @Param(description = "The second value to run calculations on") - int value2 - ) { - Integer v = null; - switch (operator) { - case "|" -> v = value1 | value2; - case "&" -> v = value1 & value2; - case "^" -> v = value1 ^ value2; - case "%" -> v = value1 % value2; - case ">>" -> v = value1 >> value2; - case "<<" -> v = value1 << value2; - } - if (v == null) { - sender().sendMessage(C.RED + "The operator you entered: (" + operator + ") is invalid!"); - return; - } - sender().sendMessage(C.GREEN + "" + value1 + " " + C.GREEN + operator.replaceAll("<", "≺").replaceAll(">", "≻").replaceAll("%", "%") + " " + C.GREEN + value2 + C.GREEN + " returns " + C.GREEN + v); - } - @Director(description = "Update the pack of a world (UNSAFE!)", name = "update-world", aliases = "^world") public void updateWorld( @Param(description = "The world to update", contextual = true) @@ -292,89 +222,11 @@ public class CommandDeveloper implements DirectorExecutor { Iris.service(StudioSVC.class).installIntoWorld(sender(), pack.getLoadKey(), folder); } - @Director(description = "Dev cmd to fix all the broken objects caused by faulty shrinkwarp") - public void fixObjects( - @Param(aliases = "dimension", description = "The dimension type to create the world with") - IrisDimension type - ) { - if (type == null) { - sender().sendMessage("Type cant be null?"); - return; - } - - IrisData dm = IrisData.get(Iris.instance.getDataFolder("packs", type.getLoadKey())); - var loader = dm.getObjectLoader(); - var objects = loader.getPossibleKeys(); - var sender = sender(); - - sender.sendMessage(C.IRIS + "Found " + objects.length + " objects in " + type.getLoadKey()); - - final int total = objects.length; - final AtomicInteger completed = new AtomicInteger(); - final AtomicInteger changed = new AtomicInteger(); - - new Job() { - @Override - public String getName() { - return "Fixing Objects"; - } - - @Override - public void execute() { - Arrays.stream(loader.getPossibleKeys()).parallel() - .map(loader::load) - .forEach(obj -> { - if (obj == null) { - completeWork(); - return; - } - - obj.shrinkwrap(); - if (obj.getShrinkOffset().isZero()) { - completeWork(); - return; - } - - try { - obj.write(obj.getLoadFile()); - completeWork(); - changed.incrementAndGet(); - } catch (IOException e) { - Iris.error("Failed to write object " + obj.getLoadKey()); - e.printStackTrace(); - } - }); - } - - @Override - public void completeWork() { - completed.incrementAndGet(); - } - - @Override - public int getTotalWork() { - return total; - } - - @Override - public int getWorkCompleted() { - return completed.get(); - } - }.execute(sender, () -> { - var failed = total - completed.get(); - if (failed != 0) sender.sendMessage(C.IRIS + "" + failed + " objects failed!"); - if (changed.get() != 0) sender.sendMessage(C.IRIS + "" + changed.get() + " objects had their offsets changed!"); - else sender.sendMessage(C.IRIS + "No objects had their offsets changed!"); - }); - } - @Director(description = "Test") public void mantle(@Param(defaultValue = "false") boolean plate, @Param(defaultValue = "21474836474") String name) throws Throwable { var base = Iris.instance.getDataFile("dump", "pv." + name + ".ttp.lz4b.bin"); var section = Iris.instance.getDataFile("dump", "pv." + name + ".section.bin"); - //extractSection(base, section, 5604930, 4397); - if (plate) { try (var in = CountingDataInputStream.wrap(new BufferedInputStream(new FileInputStream(base)))) { TectonicPlate.read(1088, in, true, IrisEngineMantle.createRuntimeDataAdapter(), IrisEngineMantle.createRuntimeHooks()); @@ -386,15 +238,6 @@ public class CommandDeveloper implements DirectorExecutor { Iris.info("Read " + (plate ? base : section).length() + " bytes from " + (plate ? base : section).getAbsolutePath()); } - private void extractSection(File source, File target, long offset, int length) throws IOException { - var raf = new RandomAccessFile(source, "r"); - var bytes = new byte[length]; - raf.seek(offset); - raf.readFully(bytes); - raf.close(); - Files.write(target.toPath(), bytes); - } - @Director(description = "Test") public void packBenchmark( @Param(description = "The pack to bench", aliases = {"pack"}, defaultValue = "overworld") @@ -429,7 +272,7 @@ public class CommandDeveloper implements DirectorExecutor { } - @Director(description = "Delete nearby chunk blocks for regen testing", name = "delete-chunk", aliases = {"delchunk", "dc"}, origin = DirectorOrigin.PLAYER, sync = true) + @Director(description = "Delete nearby chunk blocks for regen testing", name = "delete-chunk", aliases = {"dc"}, origin = DirectorOrigin.PLAYER, sync = true) public void deleteChunk( @Param(description = "Radius in chunks around your current chunk", defaultValue = "0") int radius @@ -494,49 +337,6 @@ public class CommandDeveloper implements DirectorExecutor { } } - @Director(description = "UnloadChunks for good reasons.") - public void unloadchunks() { - List IrisWorlds = new ArrayList<>(); - int chunksUnloaded = 0; - for (World world : Bukkit.getWorlds()) { - try { - if (IrisToolbelt.access(world).getEngine() != null) { - IrisWorlds.add(world); - } - } catch (Exception e) { - // no - } - } - - for (World world : IrisWorlds) { - for (Chunk chunk : world.getLoadedChunks()) { - if (chunk.isLoaded()) { - chunk.unload(); - chunksUnloaded++; - } - } - } - Iris.info(C.IRIS + "Chunks Unloaded: " + chunksUnloaded); - - } - - @Director - public void objects(@Param(defaultValue = "overworld") IrisDimension dimension) { - var loader = dimension.getLoader().getObjectLoader(); - var sender = sender(); - var keys = loader.getPossibleKeys(); - var burst = MultiBurst.burst.burst(keys.length); - AtomicInteger failed = new AtomicInteger(); - for (String key : keys) { - burst.queue(() -> { - if (loader.load(key) == null) - failed.incrementAndGet(); - }); - } - burst.complete(); - sender.sendMessage(C.RED + "Failed to load " + failed.get() + " of " + keys.length + " objects"); - } - @Director(description = "Test", aliases = {"ip"}) public void network() { try { @@ -553,108 +353,12 @@ public class CommandDeveloper implements DirectorExecutor { } } - @Director(description = "Test the compression algorithms") - public void compression( - @Param(description = "base IrisWorld") World world, - @Param(description = "raw TectonicPlate File") String path, - @Param(description = "Algorithm to Test") String algorithm, - @Param(description = "Amount of Tests") int amount, - @Param(description = "Is versioned", defaultValue = "false") boolean versioned) { - if (!IrisToolbelt.isIrisWorld(world)) { - sender().sendMessage(C.RED + "This is not an Iris world. Iris worlds: " + String.join(", ", Bukkit.getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList())); - return; - } - - File file = new File(path); - if (!file.exists()) return; - - Engine engine = IrisToolbelt.access(world).getEngine(); - if(engine != null) { - int height = engine.getTarget().getHeight(); - ExecutorService service = Executors.newFixedThreadPool(1); - VolmitSender sender = sender(); - service.submit(() -> { - try { - CountingDataInputStream raw = CountingDataInputStream.wrap(new FileInputStream(file)); - TectonicPlate plate = TectonicPlate.read(height, raw, versioned, IrisEngineMantle.createRuntimeDataAdapter(), IrisEngineMantle.createRuntimeHooks()); - raw.close(); - - double d1 = 0; - double d2 = 0; - long size = 0; - File folder = new File("tmp"); - folder.mkdirs(); - for (int i = 0; i < amount; i++) { - File tmp = new File(folder, RandomStringUtils.randomAlphanumeric(10) + "." + algorithm + ".bin"); - DataOutputStream dos = createOutput(tmp, algorithm); - long start = System.currentTimeMillis(); - plate.write(dos); - dos.close(); - d1 += System.currentTimeMillis() - start; - if (size == 0) - size = tmp.length(); - start = System.currentTimeMillis(); - CountingDataInputStream din = createInput(tmp, algorithm); - TectonicPlate.read(height, din, true, IrisEngineMantle.createRuntimeDataAdapter(), IrisEngineMantle.createRuntimeHooks()); - din.close(); - d2 += System.currentTimeMillis() - start; - tmp.delete(); - } - IO.delete(folder); - sender.sendMessage(algorithm + " is " + Form.fileSize(size) + " big after compression"); - sender.sendMessage(algorithm + " Took " + d2/amount + "ms to read"); - sender.sendMessage(algorithm + " Took " + d1/amount + "ms to write"); - } catch (Throwable e) { - e.printStackTrace(); - } - }); - service.shutdown(); - } else { - Iris.info(C.RED + "Engine is null!"); - } - } - - private CountingDataInputStream createInput(File file, String algorithm) throws Throwable { - FileInputStream in = new FileInputStream(file); - - return CountingDataInputStream.wrap(switch (algorithm) { - case "gzip" -> new GZIPInputStream(in); - case "lz4f" -> new LZ4FrameInputStream(in); - case "lz4b" -> new LZ4BlockInputStream(in); - default -> throw new IllegalStateException("Unexpected value: " + algorithm); - }); - } - - private DataOutputStream createOutput(File file, String algorithm) throws Throwable { - FileOutputStream out = new FileOutputStream(file); - - return new DataOutputStream(switch (algorithm) { - case "gzip" -> new GZIPOutputStream(out); - case "lz4f" -> new LZ4FrameOutputStream(out); - case "lz4b" -> new LZ4BlockOutputStream(out); - default -> throw new IllegalStateException("Unexpected value: " + algorithm); - }); - } - // --- Regen --- - private static final long REGEN_HEARTBEAT_MS = 5000L; - private static final int REGEN_MAX_ATTEMPTS = 2; - private static final int REGEN_STACK_LIMIT = 20; - private static final long REGEN_STALL_DUMP_IDLE_MS = 30000L; - private static final long REGEN_STALL_ABORT_IDLE_MS = 600000L; - private static final long REGEN_STACK_DUMP_INTERVAL_MS = 10000L; - private static final int REGEN_PROGRESS_BAR_WIDTH = 44; - private static final long REGEN_PROGRESS_UPDATE_MS = 200L; - private static final int REGEN_ACTION_PULSE_TICKS = 20; - private static final int REGEN_DISPLAY_FINAL_TICKS = 60; - @Director(name = "regen", aliases = {"rg"}, description = "Regenerate nearby chunks using Iris generation", origin = DirectorOrigin.PLAYER, sync = true) public void regen( @Param(name = "radius", description = "The radius of nearby chunks", defaultValue = "5") - int radius, - @Param(name = "mode", aliases = {"scope", "profile"}, description = "Regen mode: terrain or full", defaultValue = "full") - String mode + int radius ) { if (radius < 0) { sender().sendMessage(C.RED + "Radius must be 0 or greater."); @@ -667,1278 +371,36 @@ public class CommandDeveloper implements DirectorExecutor { return; } - RegenMode regenMode = RegenMode.parse(mode); - if (regenMode == null) { - sender().sendMessage(C.RED + "Unknown regen mode \"" + mode + "\". Use mode=terrain or mode=full."); + if (INMS.get().isBukkit()) { + sender().sendMessage(C.RED + "Regen requires the native chunk system; it is unavailable in Bukkit fallback mode."); + return; + } + + Engine engine = IrisToolbelt.access(world).getEngine(); + if (engine == null) { + sender().sendMessage(C.RED + "The engine access for this world is null. Generate nearby chunks first."); return; } - VolmitSender sender = sender(); int centerX = player().getLocation().getBlockX() >> 4; int centerZ = player().getLocation().getBlockZ() >> 4; - int threadCount = resolveRegenThreadCount(regenMode); - List targets = buildRegenTargets(centerX, centerZ, radius); - int chunks = targets.size(); - String runId = world.getName() + "-" + System.currentTimeMillis(); - RegenDisplay display = createRegenDisplay(sender, regenMode); + int chunks = (radius * 2 + 1) * (radius * 2 + 1); + PregenTask task = PregenTask.builder() + .center(new Position2(centerX << 4, centerZ << 4)) + .radiusX(radius << 4) + .radiusZ(radius << 4) + .gui(false) + .build(); - sender.sendMessage(C.GREEN + "Regen started (" + C.GOLD + regenMode.id() + C.GREEN + "): " - + C.GOLD + chunks + C.GREEN + " chunks, " - + C.GOLD + threadCount + C.GREEN + " worker(s). " - + C.GRAY + "Progress is shown on-screen."); - if (regenMode == RegenMode.TERRAIN) { - Iris.warn("Regen running in terrain mode; mantle overlay/object replay is skipped. Use mode=full to regenerate objects."); - } - - Iris.info("Regen run start: id=" + runId - + " world=" + world.getName() + sender().sendMessage(C.GREEN + "Regen started: " + C.GOLD + chunks + C.GREEN + + " chunk(s) around " + C.GOLD + centerX + "," + centerZ + C.GREEN + + ". Chunks purge and regenerate through the async pipeline."); + Iris.info("Regen run start: world=" + world.getName() + " center=" + centerX + "," + centerZ + " radius=" + radius - + " mode=" + regenMode.id() - + " workers=" + threadCount + " chunks=" + chunks); - if (IrisSettings.get().getGeneral().isDebug()) { - Iris.info("Regen mode config: id=" + runId - + " mode=" + regenMode.id() - + " maintenance=" + regenMode.usesMaintenance() - + " bypassMantle=" + regenMode.bypassMantleStages() - + " passes=" + regenMode.passCount() - + " fullMode=" + regenMode.isFullMode() - + " diagnostics=" + regenMode.logChunkDiagnostics()); - } - String orchestratorName = "Iris-Regen-Orchestrator-" + runId; - Thread orchestrator = new Thread(() -> runRegenOrchestrator(sender, world, targets, threadCount, regenMode, runId, display), orchestratorName); - orchestrator.setDaemon(true); - try { - orchestrator.start(); - if (IrisSettings.get().getGeneral().isDebug()) { - Iris.info("Regen worker dispatched on dedicated thread=" + orchestratorName + " id=" + runId + "."); - } - } catch (Throwable e) { - sender.sendMessage(C.RED + "Failed to start regen worker thread. See console."); - closeRegenDisplay(display, 0); - Iris.reportError(e); - } + IrisToolbelt.pregenerate(task, new RegenPregenMethod(world), engine, false); } - private int resolveRegenThreadCount(RegenMode mode) { - int workers = resolveRegenWorkerThreads(); - boolean folia = J.isFolia(); - if (mode == RegenMode.TERRAIN) { - int cap = folia ? Math.min(workers * 4, 96) : Math.min(workers * 2, 64); - return Math.max(folia ? 16 : 8, cap); - } - int cap = folia ? Math.min(workers * 2, 64) : Math.min(workers, 32); - return Math.max(folia ? 8 : 4, cap); - } - - private int resolveRegenWorkerThreads() { - try { - Class moonriseCommonClass = Class.forName("ca.spottedleaf.moonrise.common.util.MoonriseCommon"); - java.lang.reflect.Field workerPoolField = moonriseCommonClass.getDeclaredField("WORKER_POOL"); - Object workerPool = workerPoolField.get(null); - Object coreThreads = workerPool.getClass().getDeclaredMethod("getCoreThreads").invoke(workerPool); - if (coreThreads instanceof Thread[] threadsArray && threadsArray.length > 0) { - return threadsArray.length; - } - } catch (Throwable ignored) { - } - int cpus = Math.max(1, Runtime.getRuntime().availableProcessors()); - int configured = Math.max(1, IrisSettings.get().getConcurrency().getWorldGenThreads()); - return Math.max(cpus, configured); - } - - private List buildRegenTargets(int centerX, int centerZ, int radius) { - int expected = (radius * 2 + 1) * (radius * 2 + 1); - List targets = new ArrayList<>(expected); - for (int ring = 0; ring <= radius; ring++) { - for (int x = -ring; x <= ring; x++) { - for (int z = -ring; z <= ring; z++) { - if (Math.max(Math.abs(x), Math.abs(z)) != ring) { - continue; - } - targets.add(new Position2(centerX + x, centerZ + z)); - } - } - } - return targets; - } - - private void runRegenOrchestrator( - VolmitSender sender, - World world, - List targets, - int threadCount, - RegenMode mode, - String runId, - RegenDisplay display - ) { - long runStart = System.currentTimeMillis(); - AtomicBoolean setupDone = new AtomicBoolean(false); - AtomicReference setupPhase = new AtomicReference<>("bootstrap"); - AtomicLong setupPhaseSince = new AtomicLong(runStart); - Thread setupWatchdog = createRegenSetupWatchdog(world, runId, setupDone, setupPhase, setupPhaseSince); - setupWatchdog.start(); - boolean displayTerminal = false; - - Set regenThreads = ConcurrentHashMap.newKeySet(); - AtomicInteger regenThreadCounter = new AtomicInteger(); - ThreadFactory threadFactory = runnable -> { - Thread thread = new Thread(runnable, "Iris-Regen-" + runId + "-" + regenThreadCounter.incrementAndGet()); - thread.setDaemon(true); - regenThreads.add(thread); - return thread; - }; - - try { - setRegenSetupPhase(setupPhase, setupPhaseSince, "touch-context", world, runId); - updateRegenSetupDisplay(display, mode, "Touching command context", 1, 6); - DirectorContext.touch(sender); - if (mode.usesMaintenance()) { - setRegenSetupPhase(setupPhase, setupPhaseSince, "enter-maintenance", world, runId); - updateRegenSetupDisplay(display, mode, "Entering maintenance", 2, 6); - IrisToolbelt.beginWorldMaintenance(world, "regen:" + mode.id(), mode.bypassMantleStages()); - } else { - setRegenSetupPhase(setupPhase, setupPhaseSince, "maintenance-skip", world, runId); - updateRegenSetupDisplay(display, mode, "Skipping maintenance", 2, 6); - } - - ThreadPoolExecutor pool = (ThreadPoolExecutor) Executors.newFixedThreadPool(threadCount, threadFactory); - try (SyncExecutor executor = new SyncExecutor(20)) { - setRegenSetupPhase(setupPhase, setupPhaseSince, "resolve-platform", world, runId); - updateRegenSetupDisplay(display, mode, "Resolving platform", 3, 6); - PlatformChunkGenerator platform = IrisToolbelt.access(world); - setRegenSetupPhase(setupPhase, setupPhaseSince, "validate-engine", world, runId); - updateRegenSetupDisplay(display, mode, "Validating engine", 4, 6); - if (platform == null || platform.getEngine() == null) { - Iris.warn("Regen aborted: engine access is null for world=" + world.getName() + " id=" + runId + "."); - completeRegenDisplay(display, mode, true, C.RED + "Engine access is null. Generate nearby chunks first."); - displayTerminal = true; - return; - } - - setRegenSetupPhase(setupPhase, setupPhaseSince, "prepare-options", world, runId); - updateRegenSetupDisplay(display, mode, "Preparing chunk replacement", 5, 6); - - setRegenSetupPhase(setupPhase, setupPhaseSince, "dispatch", world, runId); - updateRegenSetupDisplay(display, mode, "Dispatching chunk workers", 6, 6); - RegenSummary summary = executeRegenQueue(sender, world, platform, targets, executor, pool, regenThreads, mode, runId, 1, 1, runStart, display); - - if (summary == null) { - completeRegenDisplay(display, mode, true, C.RED + "Regen failed before pass execution."); - displayTerminal = true; - return; - } - - long totalRuntime = System.currentTimeMillis() - runStart; - if (summary.failedChunks() <= 0) { - completeRegenDisplay(display, mode, false, C.GREEN + "Complete " + C.GOLD + summary.successChunks() - + C.GREEN + "/" + C.GOLD + summary.totalChunks() + C.GREEN + " in " + C.GOLD + totalRuntime + "ms"); - displayTerminal = true; - return; - } - - String failureDetail = C.RED + "Failed chunks " + C.GOLD + summary.failedChunks() + C.RED - + ", retries " + C.GOLD + summary.retryCount() - + C.RED + ", runtime " + C.GOLD + totalRuntime + "ms"; - if (summary.failurePhaseSummary() != null && !summary.failurePhaseSummary().isBlank() && !"none".equals(summary.failurePhaseSummary())) { - failureDetail = failureDetail + C.DARK_GRAY + " [phase " + summary.failurePhaseSummary() + "]"; - } - if (!summary.failedPreview().isEmpty()) { - failureDetail = failureDetail + C.DARK_GRAY + " [" + summary.failedPreview() + "]"; - } - completeRegenDisplay(display, mode, true, failureDetail); - displayTerminal = true; - } finally { - pool.shutdownNow(); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - completeRegenDisplay(display, mode, true, C.RED + "Regen interrupted."); - displayTerminal = true; - Iris.warn("Regen run interrupted: id=" + runId + " world=" + world.getName()); - } catch (Throwable e) { - String failureDetail = C.RED + "Regen failed. Check console."; - if (e.getMessage() != null && e.getMessage().contains("stalled")) { - failureDetail = C.RED + "Regen stalled. Try smaller radius or terrain mode."; - } - completeRegenDisplay(display, mode, true, failureDetail); - displayTerminal = true; - Iris.reportError(e); - e.printStackTrace(); - } finally { - setupDone.set(true); - setupWatchdog.interrupt(); - if (mode.usesMaintenance()) { - IrisToolbelt.endWorldMaintenance(world, "regen:" + mode.id()); - } - if (!displayTerminal) { - closeRegenDisplay(display, REGEN_DISPLAY_FINAL_TICKS); - } - DirectorContext.remove(); - Iris.info("Regen run closed: id=" + runId + " world=" + world.getName() + " totalMs=" + (System.currentTimeMillis() - runStart)); - } - } - - private RegenSummary executeRegenQueue( - VolmitSender sender, - World world, - PlatformChunkGenerator platform, - List targets, - SyncExecutor executor, - ThreadPoolExecutor pool, - Set regenThreads, - RegenMode mode, - String runId, - int passIndex, - int passCount, - long runStart, - RegenDisplay display - ) throws InterruptedException { - ArrayDeque pending = new ArrayDeque<>(targets.size()); - long queueTime = System.currentTimeMillis(); - for (Position2 target : targets) { - pending.addLast(new RegenChunkTask(target.getX(), target.getZ(), 1, queueTime)); - } - - ConcurrentMap activeTasks = new ConcurrentHashMap<>(); - ExecutorCompletionService completion = new ExecutorCompletionService<>(pool); - List failedChunks = new ArrayList<>(); - Map failurePhaseCounts = new HashMap<>(); - - int totalChunks = targets.size(); - int successChunks = 0; - int failedCount = 0; - int retryCount = 0; - int overlayChunks = 0; - int overlayObjectChunks = 0; - int overlayBlocks = 0; - long submittedTasks = 0L; - long finishedTasks = 0L; - int completedChunks = 0; - int inFlight = 0; - AtomicLong lastSignalMs = new AtomicLong(System.currentTimeMillis()); - long lastDump = 0L; - long lastProgressUiMs = 0L; - lastProgressUiMs = updateRegenProgressAction( - sender, - display, - mode, - passIndex, - passCount, - completedChunks, - totalChunks, - inFlight, - pending.size(), - false, - false, - false, - true, - "Queue initialized", - lastProgressUiMs - ); - - while (inFlight < pool.getMaximumPoolSize() && !pending.isEmpty()) { - RegenChunkTask task = pending.removeFirst(); - completion.submit(() -> runRegenChunk(task, world, platform, executor, activeTasks, mode, runId, lastSignalMs)); - inFlight++; - submittedTasks++; - } - - while (completedChunks < totalChunks) { - Future future = completion.poll(REGEN_HEARTBEAT_MS, TimeUnit.MILLISECONDS); - if (future == null) { - long now = System.currentTimeMillis(); - long idleMs = Math.max(0L, now - lastSignalMs.get()); - boolean stalled = idleMs >= REGEN_HEARTBEAT_MS; - String phaseSummary = summarizeActivePhases(activeTasks); - - Iris.warn("Regen heartbeat: id=" + runId - + " completed=" + completedChunks + "/" + totalChunks - + " remaining=" + (totalChunks - completedChunks) - + " queued=" + pending.size() - + " inFlight=" + inFlight - + " submitted=" + submittedTasks - + " finishedTasks=" + finishedTasks - + " retries=" + retryCount - + " failed=" + failedCount - + " poolActive=" + pool.getActiveCount() - + " poolQueue=" + pool.getQueue().size() - + " poolDone=" + pool.getCompletedTaskCount() - + " idleMs=" + idleMs - + " phases=" + phaseSummary - + " activeTasks=" + formatActiveTasks(activeTasks)); - lastProgressUiMs = updateRegenProgressAction( - sender, - display, - mode, - passIndex, - passCount, - completedChunks, - totalChunks, - inFlight, - pending.size(), - stalled, - false, - false, - true, - stalled ? "Waiting in phase " + phaseSummary : "Waiting for chunk result", - lastProgressUiMs - ); - - if (idleMs >= REGEN_STALL_DUMP_IDLE_MS && now - lastDump >= REGEN_STACK_DUMP_INTERVAL_MS) { - lastDump = now; - Iris.warn("Regen appears stalled; dumping worker stack traces for id=" + runId + "."); - dumpRegenWorkerStacks(regenThreads, world.getName()); - } - if (idleMs >= REGEN_STALL_ABORT_IDLE_MS) { - updateRegenProgressAction( - sender, - display, - mode, - passIndex, - passCount, - completedChunks, - totalChunks, - inFlight, - pending.size(), - true, - true, - true, - true, - "Stalled in phase " + phaseSummary, - lastProgressUiMs - ); - throw new IllegalStateException("Regen stalled with no chunk heartbeat or completion for " - + idleMs - + "ms (id=" + runId - + ", mode=" + mode.id() - + ", completed=" + completedChunks - + "/" + totalChunks - + ", inFlight=" + inFlight - + ", queued=" + pending.size() - + ", phase=" + phaseSummary - + ")."); - } - continue; - } - - RegenChunkResult result; - try { - result = future.get(); - } catch (ExecutionException e) { - Throwable cause = e.getCause() == null ? e : e.getCause(); - throw new IllegalStateException("Regen worker failed unexpectedly for run " + runId, cause); - } - - inFlight--; - finishedTasks++; - long duration = result.finishedAtMs() - result.startedAtMs(); - lastSignalMs.set(System.currentTimeMillis()); - - if (result.success()) { - completedChunks++; - successChunks++; - if (result.overlayAppliedBlocks() > 0) { - overlayChunks++; - } - if (result.overlayObjectKeys() > 0) { - overlayObjectChunks++; - } - overlayBlocks += result.overlayAppliedBlocks(); - if (result.task().attempt() > 1) { - Iris.warn("Regen chunk recovered after retry: id=" + runId - + " chunk=" + result.task().chunkX() + "," + result.task().chunkZ() - + " attempt=" + result.task().attempt() - + " durationMs=" + duration); - } else if (duration >= 5000L) { - Iris.warn("Regen chunk slow: id=" + runId - + " chunk=" + result.task().chunkX() + "," + result.task().chunkZ() - + " durationMs=" + duration - + " loadedAtStart=" + result.loadedAtStart()); - } - } else if (result.task().attempt() < REGEN_MAX_ATTEMPTS) { - retryCount++; - RegenChunkTask retryTask = result.task().retry(System.currentTimeMillis()); - pending.addLast(retryTask); - Iris.warn("Regen chunk retry scheduled: id=" + runId - + " chunk=" + result.task().chunkX() + "," + result.task().chunkZ() - + " failedAttempt=" + result.task().attempt() - + " nextAttempt=" + retryTask.attempt() - + " phase=" + result.failurePhase() - + " error=" + result.errorSummary()); - } else { - completedChunks++; - failedCount++; - Position2 failed = new Position2(result.task().chunkX(), result.task().chunkZ()); - failedChunks.add(failed); - String failurePhase = result.failurePhase() == null || result.failurePhase().isBlank() - ? "unknown" - : result.failurePhase(); - failurePhaseCounts.merge(failurePhase, 1, Integer::sum); - Iris.warn("Regen chunk failed terminally: id=" + runId - + " chunk=" + result.task().chunkX() + "," + result.task().chunkZ() - + " attempts=" + result.task().attempt() - + " phase=" + failurePhase - + " error=" + result.errorSummary()); - if (result.error() != null) { - Iris.reportError(result.error()); - } - } - - while (inFlight < pool.getMaximumPoolSize() && !pending.isEmpty()) { - RegenChunkTask task = pending.removeFirst(); - completion.submit(() -> runRegenChunk(task, world, platform, executor, activeTasks, mode, runId, lastSignalMs)); - inFlight++; - submittedTasks++; - } - - String phaseSummary = summarizeActivePhases(activeTasks); - lastProgressUiMs = updateRegenProgressAction( - sender, - display, - mode, - passIndex, - passCount, - completedChunks, - totalChunks, - inFlight, - pending.size(), - false, - false, - false, - false, - phaseSummary.equals("idle") ? "Generating chunks" : "Generating chunks in " + phaseSummary, - lastProgressUiMs - ); - } - - long runtimeMs = System.currentTimeMillis() - runStart; - String preview = formatFailedChunkPreview(failedChunks); - String failurePhaseSummary = formatFailurePhaseSummary(failurePhaseCounts); - Iris.info("Regen run complete: id=" + runId - + " world=" + world.getName() - + " total=" + totalChunks - + " success=" + successChunks - + " failed=" + failedCount - + " retries=" + retryCount - + " submittedTasks=" + submittedTasks - + " finishedTasks=" + finishedTasks - + " overlayChunks=" + overlayChunks - + " overlayObjectChunks=" + overlayObjectChunks - + " overlayBlocks=" + overlayBlocks - + " failurePhases=" + failurePhaseSummary - + " runtimeMs=" + runtimeMs - + " failedPreview=" + preview); - updateRegenProgressAction( - sender, - display, - mode, - passIndex, - passCount, - completedChunks, - totalChunks, - inFlight, - pending.size(), - false, - true, - failedCount > 0, - true, - failedCount > 0 ? "Completed with failures in " + failurePhaseSummary : "Pass complete", - lastProgressUiMs - ); - return new RegenSummary(totalChunks, successChunks, failedCount, retryCount, preview, failurePhaseSummary); - } - - private long updateRegenProgressAction( - VolmitSender sender, - RegenDisplay display, - RegenMode mode, - int passIndex, - int passCount, - int completed, - int total, - int inFlight, - int queued, - boolean stalled, - boolean terminal, - boolean failed, - boolean force, - String detail, - long lastUiMs - ) { - if (display == null && !sender.isPlayer()) { - return lastUiMs; - } - - long now = System.currentTimeMillis(); - if (!force && now - lastUiMs < REGEN_PROGRESS_UPDATE_MS) { - return lastUiMs; - } - - int safePassCount = Math.max(1, passCount); - int safePassIndex = Math.max(1, Math.min(passIndex, safePassCount)); - int safeTotal = Math.max(1, total); - int safeCompleted = Math.max(0, Math.min(completed, safeTotal)); - double passProgress = safeCompleted / (double) safeTotal; - double overallProgress = ((safePassIndex - 1) + passProgress) / safePassCount; - int percent = (int) Math.round(overallProgress * 100.0D); - String bar = buildRegenProgressBar(overallProgress); - C statusColor = failed ? C.RED : terminal ? C.GREEN : stalled ? C.RED : C.AQUA; - String statusLabel = failed ? "FAILED" : terminal ? "DONE" : stalled ? "STALLED" : "RUN"; - BarColor bossColor = failed ? BarColor.RED : terminal ? BarColor.GREEN : stalled ? BarColor.RED : BarColor.BLUE; - String title = C.GOLD + "Regen " + mode.id() - + C.GRAY + " " + statusColor + statusLabel - + C.GRAY + " " + C.YELLOW + percent + "%" - + C.DARK_GRAY + " P" + safePassIndex + "/" + safePassCount; - String action = bar - + C.GRAY + " " + C.YELLOW + percent + "%" - + C.DARK_GRAY + " P" + safePassIndex + "/" + safePassCount - + C.DARK_GRAY + " C" + safeCompleted + "/" + safeTotal - + C.DARK_GRAY + " Q" + queued - + C.DARK_GRAY + " F" + inFlight; - if (detail != null && !detail.isBlank()) { - action = action + C.GRAY + " | " + C.WHITE + detail; - } - - if (display != null) { - updateRegenDisplay(display, overallProgress, bossColor, title, action); - return now; - } - - if (sender.isPlayer()) { - String actionText = action; - J.runEntity(sender.player(), () -> sender.sendAction(actionText)); - } - return now; - } - - private static String buildRegenProgressBar(double progress) { - int width = REGEN_PROGRESS_BAR_WIDTH; - int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, progress)) * width); - StringBuilder bar = new StringBuilder(width * 3 + 4); - bar.append(C.DARK_GRAY).append("["); - for (int i = 0; i < width; i++) { - bar.append(i < filled ? C.GREEN : C.DARK_GRAY).append("|"); - } - bar.append(C.DARK_GRAY).append("]"); - return bar.toString(); - } - - private RegenDisplay createRegenDisplay(VolmitSender sender, RegenMode mode) { - if (!sender.isPlayer()) { - return null; - } - - Player player = sender.player(); - if (player == null) { - return null; - } - - BossBar bossBar = Bukkit.createBossBar(C.GOLD + "Regen " + mode.id(), BarColor.BLUE, BarStyle.SEGMENTED_20); - bossBar.setProgress(0.0D); - bossBar.addPlayer(player); - bossBar.setVisible(true); - RegenDisplay display = new RegenDisplay(sender, bossBar); - String title = C.GOLD + "Regen " + mode.id() + C.GRAY + " " + C.AQUA + "RUN" + C.GRAY + " " + C.YELLOW + "0%"; - String action = buildRegenProgressBar(0.0D) + C.GRAY + " " + C.YELLOW + "0%" + C.GRAY + " | " + C.WHITE + "Preparing setup"; - updateRegenDisplay(display, 0.0D, BarColor.BLUE, title, action); - pulseRegenDisplay(display); - return display; - } - - private void updateRegenSetupDisplay(RegenDisplay display, RegenMode mode, String phase, int step, int totalSteps) { - if (display == null || display.closed.get()) { - return; - } - - int safeTotalSteps = Math.max(1, totalSteps); - int safeStep = Math.max(0, Math.min(step, safeTotalSteps)); - double setupProgress = Math.max(0.0D, Math.min(0.1D, (safeStep / (double) safeTotalSteps) * 0.1D)); - int percent = (int) Math.round(setupProgress * 100.0D); - String title = C.GOLD + "Regen " + mode.id() + C.GRAY + " " + C.AQUA + "SETUP" + C.GRAY + " " + C.YELLOW + percent + "%"; - String action = buildRegenProgressBar(setupProgress) - + C.GRAY + " " + C.YELLOW + percent + "%" - + C.GRAY + " | " + C.WHITE + phase; - updateRegenDisplay(display, setupProgress, BarColor.BLUE, title, action); - } - - private void updateRegenDisplay(RegenDisplay display, double progress, BarColor color, String title, String action) { - if (display == null || display.closed.get()) { - return; - } - - display.progress = Math.max(0.0D, Math.min(1.0D, progress)); - display.color = color == null ? BarColor.BLUE : color; - display.title = title == null ? "" : title; - display.actionLine = action == null ? "" : action; - - Player player = display.sender.player(); - if (player == null) { - closeRegenDisplay(display, 0); - return; - } - - boolean scheduled = J.runEntity(player, () -> { - if (display.closed.get()) { - return; - } - - display.bossBar.setProgress(display.progress); - display.bossBar.setColor(display.color); - display.bossBar.setTitle(display.title); - if (!display.actionLine.isBlank()) { - display.sender.sendAction(display.actionLine); - } - }); - if (!scheduled) { - closeRegenDisplay(display, 0); - } - } - - private void pulseRegenDisplay(RegenDisplay display) { - if (display == null || display.closed.get()) { - return; - } - - Player player = display.sender.player(); - if (player == null) { - closeRegenDisplay(display, 0); - return; - } - - boolean scheduled = J.runEntity(player, () -> { - if (display.closed.get()) { - return; - } - - Player activePlayer = display.sender.player(); - if (activePlayer == null || !activePlayer.isOnline()) { - closeRegenDisplay(display, 0); - return; - } - - if (!display.actionLine.isBlank()) { - display.sender.sendAction(display.actionLine); - } - pulseRegenDisplay(display); - }, REGEN_ACTION_PULSE_TICKS); - - if (!scheduled) { - closeRegenDisplay(display, 0); - } - } - - private void completeRegenDisplay(RegenDisplay display, RegenMode mode, boolean failed, String detail) { - if (display == null || display.closed.get()) { - return; - } - - double progress = failed ? Math.max(0.0D, Math.min(1.0D, display.progress)) : 1.0D; - int percent = (int) Math.round(progress * 100.0D); - BarColor color = failed ? BarColor.RED : BarColor.GREEN; - String status = failed ? C.RED + "FAILED" : C.GREEN + "DONE"; - String title = C.GOLD + "Regen " + mode.id() + C.GRAY + " " + status + C.GRAY + " " + C.YELLOW + percent + "%"; - String action = buildRegenProgressBar(progress) + C.GRAY + " " + C.YELLOW + percent + "%"; - if (detail != null && !detail.isBlank()) { - action = action + C.GRAY + " | " + C.WHITE + detail; - } - - updateRegenDisplay(display, progress, color, title, action); - closeRegenDisplay(display, REGEN_DISPLAY_FINAL_TICKS); - } - - private void closeRegenDisplay(RegenDisplay display, int delayTicks) { - if (display == null || display.closed.get()) { - return; - } - - Player player = display.sender.player(); - Runnable closeTask = () -> { - if (!display.closed.compareAndSet(false, true)) { - return; - } - - display.bossBar.removeAll(); - display.bossBar.setVisible(false); - display.sender.sendAction(" "); - }; - - if (player == null) { - display.closed.set(true); - return; - } - - boolean scheduled = delayTicks > 0 - ? J.runEntity(player, closeTask, delayTicks) - : J.runEntity(player, closeTask); - if (!scheduled) { - display.closed.set(true); - } - } - - private RegenChunkResult runRegenChunk( - RegenChunkTask task, - World world, - PlatformChunkGenerator platform, - SyncExecutor executor, - ConcurrentMap activeTasks, - RegenMode mode, - String runId, - AtomicLong lastSignalMs - ) { - String worker = Thread.currentThread().getName(); - long startedAt = System.currentTimeMillis(); - boolean loadedAtStart = false; - try { - loadedAtStart = world.isChunkLoaded(task.chunkX(), task.chunkZ()); - } catch (Throwable ignored) { - } - - RegenActiveTask activeTask = new RegenActiveTask(task.chunkX(), task.chunkZ(), task.attempt(), startedAt, loadedAtStart); - activeTasks.put(worker, activeTask); - AtomicReference failurePhase = new AtomicReference<>("unknown"); - AtomicInteger overlayAppliedBlocks = new AtomicInteger(); - AtomicInteger overlayObjectKeys = new AtomicInteger(); - ChunkReplacementListener listener = new ChunkReplacementListener() { - @Override - public void onPhase(String phase, int chunkX, int chunkZ, long timestampMs) { - activeTask.updatePhase(phase, timestampMs); - lastSignalMs.set(timestampMs); - } - - @Override - public void onOverlay(int chunkX, int chunkZ, int appliedBlocks, int objectKeys, long timestampMs) { - overlayAppliedBlocks.addAndGet(appliedBlocks); - overlayObjectKeys.addAndGet(objectKeys); - activeTask.updatePhase("overlay", timestampMs); - lastSignalMs.set(timestampMs); - } - - @Override - public void onFailurePhase(String phase, int chunkX, int chunkZ, Throwable error, long timestampMs) { - String classifiedPhase = classifyRegenFailurePhase(phase); - failurePhase.set(classifiedPhase); - activeTask.updatePhase(classifiedPhase, timestampMs); - lastSignalMs.set(timestampMs); - } - }; - try { - if (mode.logChunkDiagnostics()) { - Iris.info("Regen chunk start: id=" + runId - + " chunk=" + task.chunkX() + "," + task.chunkZ() - + " attempt=" + task.attempt() - + " loadedAtStart=" + loadedAtStart - + " worker=" + worker); - } - ChunkReplacementOptions options = mode == RegenMode.FULL - ? ChunkReplacementOptions.full(runId, mode.logChunkDiagnostics()) - : ChunkReplacementOptions.terrain(runId, mode.logChunkDiagnostics()); - RegenRuntime.setRunId(runId); - try { - platform.injectChunkReplacement(world, task.chunkX(), task.chunkZ(), executor, options, listener); - } finally { - RegenRuntime.clear(); - } - if (mode.logChunkDiagnostics()) { - Iris.info("Regen chunk end: id=" + runId - + " chunk=" + task.chunkX() + "," + task.chunkZ() - + " attempt=" + task.attempt() - + " worker=" + worker - + " durationMs=" + (System.currentTimeMillis() - startedAt)); - } - long finishedAt = System.currentTimeMillis(); - activeTask.updateHeartbeat(finishedAt); - lastSignalMs.set(finishedAt); - return RegenChunkResult.success(task, worker, startedAt, finishedAt, loadedAtStart, overlayAppliedBlocks.get(), overlayObjectKeys.get()); - } catch (Throwable e) { - if (e instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - long finishedAt = System.currentTimeMillis(); - String classifiedPhase = classifyRegenFailurePhase(failurePhase.get()); - if ("unknown".equals(classifiedPhase)) { - classifiedPhase = classifyRegenFailurePhase(activeTask.phase()); - } - activeTask.updatePhase(classifiedPhase, finishedAt); - activeTask.updateHeartbeat(finishedAt); - lastSignalMs.set(finishedAt); - return RegenChunkResult.failure( - task, - worker, - startedAt, - finishedAt, - loadedAtStart, - classifiedPhase, - overlayAppliedBlocks.get(), - overlayObjectKeys.get(), - e - ); - } finally { - activeTasks.remove(worker); - } - } - - private Thread createRegenSetupWatchdog( - World world, - String runId, - AtomicBoolean setupDone, - AtomicReference setupPhase, - AtomicLong setupPhaseSince - ) { - String setupWatchdogName = "Iris-Regen-SetupWatchdog-" + runId; - Thread setupWatchdog = new Thread(() -> { - while (!setupDone.get()) { - try { - Thread.sleep(REGEN_HEARTBEAT_MS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return; - } - - if (!setupDone.get()) { - long elapsed = System.currentTimeMillis() - setupPhaseSince.get(); - Iris.warn("Regen setup heartbeat: id=" + runId - + " phase=" + setupPhase.get() - + " elapsedMs=" + elapsed - + " world=" + world.getName()); - } - } - }, setupWatchdogName); - setupWatchdog.setDaemon(true); - return setupWatchdog; - } - - private void setRegenSetupPhase( - AtomicReference setupPhase, - AtomicLong setupPhaseSince, - String nextPhase, - World world, - String runId - ) { - setupPhase.set(nextPhase); - setupPhaseSince.set(System.currentTimeMillis()); - if (IrisSettings.get().getGeneral().isDebug()) { - Iris.info("Regen setup phase: id=" + runId + " phase=" + nextPhase + " world=" + world.getName()); - } - } - - private static String formatFailedChunkPreview(List failedChunks) { - if (failedChunks.isEmpty()) { - return "[]"; - } - - StringBuilder builder = new StringBuilder("["); - int index = 0; - for (Position2 chunk : failedChunks) { - if (index > 0) { - builder.append(", "); - } - if (index >= 10) { - builder.append("..."); - break; - } - builder.append(chunk.getX()).append(",").append(chunk.getZ()); - index++; - } - builder.append("]"); - return builder.toString(); - } - - private static String summarizeActivePhases(ConcurrentMap activeTasks) { - if (activeTasks.isEmpty()) { - return "idle"; - } - - Map counts = new HashMap<>(); - for (RegenActiveTask activeTask : activeTasks.values()) { - String phase = classifyRegenFailurePhase(activeTask.phase()); - counts.merge(phase, 1, Integer::sum); - } - if (counts.isEmpty()) { - return "idle"; - } - - List> entries = new ArrayList<>(counts.entrySet()); - entries.sort((a, b) -> { - int diff = Integer.compare(b.getValue(), a.getValue()); - if (diff != 0) { - return diff; - } - return a.getKey().compareTo(b.getKey()); - }); - - StringBuilder builder = new StringBuilder(); - int emitted = 0; - for (Map.Entry entry : entries) { - if (emitted > 0) { - builder.append(", "); - } - if (emitted >= 3) { - builder.append("..."); - break; - } - builder.append(entry.getKey()).append(" x").append(entry.getValue()); - emitted++; - } - return builder.toString(); - } - - private static String formatFailurePhaseSummary(Map failurePhaseCounts) { - if (failurePhaseCounts.isEmpty()) { - return "none"; - } - - List> entries = new ArrayList<>(failurePhaseCounts.entrySet()); - entries.sort((a, b) -> { - int diff = Integer.compare(b.getValue(), a.getValue()); - if (diff != 0) { - return diff; - } - return a.getKey().compareTo(b.getKey()); - }); - - StringBuilder builder = new StringBuilder(); - int emitted = 0; - for (Map.Entry entry : entries) { - if (emitted > 0) { - builder.append(", "); - } - if (emitted >= 5) { - builder.append("..."); - break; - } - builder.append(entry.getKey()).append("=").append(entry.getValue()); - emitted++; - } - return builder.toString(); - } - - private static String classifyRegenFailurePhase(String phase) { - if (phase == null || phase.isBlank()) { - return "unknown"; - } - - String normalized = phase.toLowerCase(Locale.ROOT); - if (normalized.contains("generate")) { - return "generate"; - } - if (normalized.contains("acquire-load-lock") || normalized.contains("reset-mantle")) { - return "generate"; - } - if (normalized.contains("apply-terrain") || normalized.contains("folia-region-run")) { - return "apply-terrain"; - } - if (normalized.contains("paperlib-async-load") || normalized.contains("folia-run-region")) { - return "apply-terrain"; - } - if (normalized.contains("overlay")) { - return "overlay"; - } - if (normalized.contains("structure")) { - return "structures"; - } - if (normalized.contains("chunk-load-callback")) { - return "chunk-load-callback"; - } - return "unknown"; - } - - private static String formatActiveTasks(ConcurrentMap activeTasks) { - if (activeTasks.isEmpty()) { - return "{}"; - } - - StringBuilder builder = new StringBuilder("{"); - int count = 0; - long now = System.currentTimeMillis(); - for (Map.Entry entry : activeTasks.entrySet()) { - if (count > 0) { - builder.append(", "); - } - if (count >= 8) { - builder.append("..."); - break; - } - RegenActiveTask activeTask = entry.getValue(); - builder.append(entry.getKey()) - .append("=") - .append(activeTask.chunkX()) - .append(",") - .append(activeTask.chunkZ()) - .append("@") - .append(activeTask.attempt()) - .append("/") - .append(now - activeTask.startedAtMs()) - .append("ms") - .append(":") - .append(classifyRegenFailurePhase(activeTask.phase())) - .append("/") - .append(now - activeTask.lastHeartbeatMs()) - .append("ms") - .append(activeTask.loadedAtStart() ? ":loaded" : ":cold"); - count++; - } - builder.append("}"); - return builder.toString(); - } - - private static void dumpRegenWorkerStacks(Set explicitThreads, String worldName) { - Set threads = new LinkedHashSet<>(); - threads.addAll(explicitThreads); - for (Thread thread : Thread.getAllStackTraces().keySet()) { - if (thread == null || !thread.isAlive()) { - continue; - } - - String name = thread.getName(); - if (name.startsWith("Iris-Regen-") - || name.startsWith("Iris EngineSVC-") - || name.startsWith("Iris World Manager") - || name.contains(worldName)) { - threads.add(thread); - } - } - - for (Thread thread : threads) { - if (thread == null || !thread.isAlive()) { - continue; - } - - Iris.warn("Regen worker thread=" + thread.getName() + " state=" + thread.getState()); - StackTraceElement[] trace = thread.getStackTrace(); - int limit = Math.min(trace.length, REGEN_STACK_LIMIT); - for (int i = 0; i < limit; i++) { - Iris.warn(" at " + trace[i]); - } - } - } - - private static final class RegenDisplay { - private final VolmitSender sender; - private final BossBar bossBar; - private final AtomicBoolean closed = new AtomicBoolean(false); - private volatile String title = ""; - private volatile String actionLine = ""; - private volatile double progress = 0.0D; - private volatile BarColor color = BarColor.BLUE; - - private RegenDisplay(VolmitSender sender, BossBar bossBar) { - this.sender = sender; - this.bossBar = bossBar; - } - } - - private record RegenChunkTask(int chunkX, int chunkZ, int attempt, long queuedAtMs) { - private RegenChunkTask retry(long now) { - return new RegenChunkTask(chunkX, chunkZ, attempt + 1, now); - } - } - - private enum RegenMode { - TERRAIN("terrain", true, false, false, false), - FULL("full", true, false, true, true); - - private final String id; - private final boolean usesMaintenance; - private final boolean bypassMantleStages; - private final boolean fullMode; - private final boolean logChunkDiagnostics; - - RegenMode( - String id, - boolean usesMaintenance, - boolean bypassMantleStages, - boolean fullMode, - boolean logChunkDiagnostics - ) { - this.id = id; - this.usesMaintenance = usesMaintenance; - this.bypassMantleStages = bypassMantleStages; - this.fullMode = fullMode; - this.logChunkDiagnostics = logChunkDiagnostics; - } - - private String id() { - return id; - } - - private boolean usesMaintenance() { - return usesMaintenance; - } - - private boolean bypassMantleStages() { - return bypassMantleStages; - } - - private boolean isFullMode() { - return fullMode; - } - - private int passCount() { - return 1; - } - - private boolean logChunkDiagnostics() { - return logChunkDiagnostics && IrisSettings.get().getGeneral().isDebug(); - } - - private static RegenMode parse(String raw) { - if (raw == null) { - return FULL; - } - - String normalized = raw.trim(); - if (normalized.isEmpty()) { - return FULL; - } - - for (RegenMode mode : values()) { - if (mode.id.equalsIgnoreCase(normalized)) { - return mode; - } - } - return null; - } - } - - private static final class RegenActiveTask { - private final int chunkX; - private final int chunkZ; - private final int attempt; - private final long startedAtMs; - private final boolean loadedAtStart; - private volatile String phase; - private volatile long lastHeartbeatMs; - - private RegenActiveTask(int chunkX, int chunkZ, int attempt, long startedAtMs, boolean loadedAtStart) { - this.chunkX = chunkX; - this.chunkZ = chunkZ; - this.attempt = attempt; - this.startedAtMs = startedAtMs; - this.loadedAtStart = loadedAtStart; - this.phase = "queued"; - this.lastHeartbeatMs = startedAtMs; - } - - private void updatePhase(String nextPhase, long timestampMs) { - this.phase = nextPhase == null || nextPhase.isBlank() ? "unknown" : nextPhase; - this.lastHeartbeatMs = timestampMs; - } - - private void updateHeartbeat(long timestampMs) { - this.lastHeartbeatMs = timestampMs; - } - - private int chunkX() { - return chunkX; - } - - private int chunkZ() { - return chunkZ; - } - - private int attempt() { - return attempt; - } - - private long startedAtMs() { - return startedAtMs; - } - - private boolean loadedAtStart() { - return loadedAtStart; - } - - private String phase() { - return phase; - } - - private long lastHeartbeatMs() { - return lastHeartbeatMs; - } - } - - private record RegenChunkResult( - RegenChunkTask task, - String worker, - long startedAtMs, - long finishedAtMs, - boolean loadedAtStart, - String failurePhase, - int overlayAppliedBlocks, - int overlayObjectKeys, - boolean success, - Throwable error - ) { - private static RegenChunkResult success( - RegenChunkTask task, - String worker, - long startedAtMs, - long finishedAtMs, - boolean loadedAtStart, - int overlayAppliedBlocks, - int overlayObjectKeys - ) { - return new RegenChunkResult( - task, - worker, - startedAtMs, - finishedAtMs, - loadedAtStart, - "none", - overlayAppliedBlocks, - overlayObjectKeys, - true, - null - ); - } - - private static RegenChunkResult failure( - RegenChunkTask task, - String worker, - long startedAtMs, - long finishedAtMs, - boolean loadedAtStart, - String failurePhase, - int overlayAppliedBlocks, - int overlayObjectKeys, - Throwable error - ) { - return new RegenChunkResult( - task, - worker, - startedAtMs, - finishedAtMs, - loadedAtStart, - failurePhase == null || failurePhase.isBlank() ? "unknown" : failurePhase, - overlayAppliedBlocks, - overlayObjectKeys, - false, - error - ); - } - - private String errorSummary() { - if (error == null) { - return "unknown"; - } - String message = error.getMessage(); - if (message == null || message.isEmpty()) { - return error.getClass().getSimpleName(); - } - return error.getClass().getSimpleName() + ": " + message; - } - } - - private record RegenSummary( - int totalChunks, - int successChunks, - int failedChunks, - int retryCount, - String failedPreview, - String failurePhaseSummary - ) { - } } diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandEdit.java b/core/src/main/java/art/arcane/iris/core/commands/CommandEdit.java index 2e4ea844d..5d0a858b2 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandEdit.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandEdit.java @@ -22,7 +22,6 @@ import art.arcane.iris.Iris; import art.arcane.iris.core.service.StudioSVC; import art.arcane.iris.engine.object.*; import art.arcane.iris.util.common.director.DirectorExecutor; -import art.arcane.iris.util.common.director.DirectorHelp; import art.arcane.volmlib.util.director.DirectorOrigin; import art.arcane.volmlib.util.director.annotations.Director; import art.arcane.volmlib.util.director.annotations.Param; @@ -31,13 +30,8 @@ import art.arcane.iris.util.common.format.C; import java.awt.*; -@Director(name = "edit", origin = DirectorOrigin.PLAYER, studio = true, description = "Edit something") +@Director(name = "edit", origin = DirectorOrigin.PLAYER, description = "Edit something") public class CommandEdit implements DirectorExecutor { - @Director(description = "Show help tree for this command group", aliases = {"?"}) - public void help() { - DirectorHelp.print(sender(), getClass()); - } - private boolean noStudio() { if (!sender().isPlayer()) { sender().sendMessage(C.RED + "Players only!"); diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandFind.java b/core/src/main/java/art/arcane/iris/core/commands/CommandFind.java index ba7ecbf75..d2a2b4d4d 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandFind.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandFind.java @@ -22,6 +22,7 @@ import art.arcane.iris.Iris; import art.arcane.iris.core.service.ObjectStudioSaveService; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.IrisStructureLocator; +import art.arcane.iris.engine.framework.StructureReachability; import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.util.common.director.DirectorExecutor; @@ -29,6 +30,7 @@ import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler; import art.arcane.iris.util.common.director.specialhandlers.StructureHandler; import art.arcane.iris.util.common.format.C; import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.director.DirectorOrigin; import art.arcane.volmlib.util.director.annotations.Director; import art.arcane.volmlib.util.director.annotations.Param; @@ -129,6 +131,12 @@ public class CommandFind implements DirectorExecutor { sender().sendMessage(C.RED + "Unknown structure: " + structure); return; } + if (!StructureReachability.isReachable(e, structure)) { + KList miss = StructureReachability.missingBiomeKeys(e, structure); + sender().sendMessage(C.YELLOW + structure + " cannot generate in this world (its required biomes are not produced by this pack" + + (miss.isEmpty() ? "" : ": needs " + String.join("/", miss)) + ")."); + return; + } StructureSearchResult result = target.getWorld().locateNearestStructure(target.getLocation(), match, 100, true); if (result == null || result.getLocation() == null) { sender().sendMessage(C.YELLOW + "No " + structure + " found within range of you."); 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 b647f5450..25c3527fe 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 @@ -21,21 +21,17 @@ package art.arcane.iris.core.commands; import art.arcane.iris.Iris; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.IrisWorlds; -import art.arcane.iris.core.datapack.DatapackIngestService; import art.arcane.iris.core.lifecycle.WorldLifecycleService; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.service.StudioSVC; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IrisDimension; -import art.arcane.iris.engine.platform.ChunkReplacementListener; -import art.arcane.iris.engine.platform.ChunkReplacementOptions; import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.volmlib.util.collection.KList; import art.arcane.iris.util.common.director.DirectorContext; import art.arcane.volmlib.util.director.DirectorParameterHandler; import art.arcane.iris.util.common.director.DirectorExecutor; -import art.arcane.iris.util.common.director.DirectorHelp; import art.arcane.volmlib.util.director.DirectorOrigin; import art.arcane.volmlib.util.director.annotations.Director; import art.arcane.volmlib.util.director.annotations.Param; @@ -88,24 +84,8 @@ import static org.bukkit.Bukkit.getServer; @Director(name = "iris", aliases = {"ir", "irs"}, description = "Basic Command") public class CommandIris implements DirectorExecutor { - @Director(description = "Show help tree for this command group", aliases = {"?"}) - public void help() { - DirectorHelp.print(sender(), getClass()); - } - - @Director(description = "Download/update external datapack imports declared in pack dimensions (alias of /iris datapack ingest)", aliases = {"ingestpacks"}) - public void ingest( - @Param(description = "Restart the server when new datapacks are installed (required for new structures to register and generate)", defaultValue = "false") - boolean restart - ) { - VolmitSender ingestSender = sender(); - ingestSender.sendMessage(C.GRAY + "Starting datapack ingest..."); - J.a(() -> DatapackIngestService.ingestAll(ingestSender, restart)); - } - private CommandStudio studio; private CommandPregen pregen; - private CommandSettings settings; private CommandObject object; private CommandStructure structure; private CommandWhat what; @@ -120,7 +100,7 @@ public class CommandIris implements DirectorExecutor { String worldNameToCheck = "YourWorldName"; VolmitSender sender = Iris.getSender(); - @Director(description = "Create a new world", aliases = {"+", "c"}) + @Director(description = "Create a new world", aliases = {"c"}) public void create( @Param(aliases = "world-name", description = "The name of the world to create") String name, @@ -527,7 +507,7 @@ public class CommandIris implements DirectorExecutor { } } - @Director(description = "Remove an Iris world", aliases = {"del", "rm", "delete"}, sync = true) + @Director(description = "Remove an Iris world", aliases = {"rm"}, sync = true) public void remove( @Param(description = "The world to remove") World world, diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandObject.java b/core/src/main/java/art/arcane/iris/core/commands/CommandObject.java index 10d718b91..81bc2410b 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandObject.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandObject.java @@ -36,7 +36,6 @@ import art.arcane.volmlib.util.data.Cuboid; import art.arcane.iris.util.common.data.IrisCustomData; import art.arcane.iris.util.common.data.registry.Materials; import art.arcane.iris.util.common.director.DirectorExecutor; -import art.arcane.iris.util.common.director.DirectorHelp; import art.arcane.iris.util.common.director.specialhandlers.NullableDimensionHandler; import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.scheduling.J; @@ -61,14 +60,9 @@ import java.io.IOException; import java.text.NumberFormat; import java.util.*; -@Director(name = "object", aliases = "o", origin = DirectorOrigin.PLAYER, studio = true, description = "Iris object manipulation") +@Director(name = "object", aliases = "o", origin = DirectorOrigin.PLAYER, description = "Iris object manipulation") public class CommandObject implements DirectorExecutor { - @Director(description = "Show help tree for this command group", aliases = {"?"}) - public void help() { - DirectorHelp.print(sender(), getClass()); - } - - @Director(description = "Open an object studio world (grid of every object; dimension optional, defaults to all packs)", aliases = {"std", "s"}, sync = true) + @Director(description = "Open an object studio world (grid of every object; dimension optional, defaults to all packs)", sync = true) public void studio( @Param(defaultValue = "null", description = "Optional dimension whose object pack to lay out; omit to aggregate objects from every pack", aliases = "dim", customHandler = NullableDimensionHandler.class) IrisDimension dimension, @@ -334,7 +328,7 @@ public class CommandObject implements DirectorExecutor { } @Director(description = "Make tree leaves vanilla-decay-plausible (every leaf within 6 blocks of a log)", - origin = DirectorOrigin.BOTH, studio = false) + origin = DirectorOrigin.BOTH) public void plausibilize( @Param(description = "Object key, prefix (trees/), or filesystem path", customHandler = ObjectTargetHandler.class) @@ -536,7 +530,7 @@ public class CommandObject implements DirectorExecutor { } - @Director(description = "Get a powder that reveals objects", studio = true, aliases = "d") + @Director(description = "Get a powder that reveals objects", aliases = "d") public void dust() { player().getInventory().addItem(WandSVC.createDust()); sender().playSound(Sound.AMBIENT_SOUL_SAND_VALLEY_ADDITIONS, 1f, 1.5f); @@ -720,7 +714,7 @@ public class CommandObject implements DirectorExecutor { sender().sendMessage(C.GREEN + "Successfully object to saved: " + dimension.getLoadKey() + "/objects/" + name); } - @Director(description = "Shift a selection in your looking direction", aliases = "-") + @Director(description = "Shift a selection in your looking direction") public void shift( @Param(description = "The amount to shift by", defaultValue = "1") int amount @@ -751,7 +745,7 @@ public class CommandObject implements DirectorExecutor { sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f); } - @Director(description = "Undo a number of pastes", aliases = "-") + @Director(description = "Undo a number of pastes", aliases = "u") public void undo( @Param(description = "The amount of pastes to undo", defaultValue = "1") int amount @@ -762,7 +756,7 @@ public class CommandObject implements DirectorExecutor { sender().sendMessage(C.BLUE + "Reverted " + actualReverts + C.BLUE +" pastes!"); } - @Director(description = "Gets an object wand and grabs the current WorldEdit selection.", aliases = "we", origin = DirectorOrigin.PLAYER, studio = true) + @Director(description = "Gets an object wand and grabs the current WorldEdit selection.", aliases = "we", origin = DirectorOrigin.PLAYER) public void we() { if (!Bukkit.getPluginManager().isPluginEnabled("WorldEdit")) { sender().sendMessage(C.RED + "You can't get a WorldEdit selection without WorldEdit, you know."); diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandPack.java b/core/src/main/java/art/arcane/iris/core/commands/CommandPack.java index a8491d43d..fedf7b75a 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandPack.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandPack.java @@ -23,7 +23,6 @@ import art.arcane.iris.core.pack.PackValidationRegistry; import art.arcane.iris.core.pack.PackValidationResult; import art.arcane.iris.core.pack.PackValidator; 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.util.common.plugin.VolmitSender; import art.arcane.volmlib.util.director.annotations.Director; @@ -33,12 +32,7 @@ import java.io.File; @Director(name = "pack", aliases = {"pk"}, description = "Pack validation and maintenance") public class CommandPack implements DirectorExecutor { - @Director(description = "Show help tree for this command group", aliases = {"?"}) - public void help() { - DirectorHelp.print(sender(), getClass()); - } - - @Director(description = "Validate a pack (or all packs) and re-publish results", aliases = {"v", "check"}) + @Director(description = "Validate a pack (or all packs) and re-publish results", aliases = {"v"}) public void validate( @Param(description = "The pack folder name to validate (leave empty for all)", defaultValue = "") String pack @@ -75,7 +69,7 @@ public class CommandPack implements DirectorExecutor { runValidate(s, target); } - @Director(description = "Restore most recent trashed files for a pack", aliases = {"r", "undelete"}) + @Director(description = "Restore most recent trashed files for a pack", aliases = {"r"}) public void restore( @Param(description = "The pack folder name to restore") String pack @@ -99,7 +93,7 @@ public class CommandPack implements DirectorExecutor { s.sendMessage(C.GRAY + "Re-run /iris pack validate " + pack + " to re-check."); } - @Director(description = "Show cached validation status for a pack", aliases = {"s", "info"}) + @Director(description = "Show cached validation status for a pack", aliases = {"s"}) public void status( @Param(description = "The pack folder name", defaultValue = "") String pack diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandPregen.java b/core/src/main/java/art/arcane/iris/core/commands/CommandPregen.java index 3c584422a..a68b4ff5c 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandPregen.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandPregen.java @@ -23,7 +23,6 @@ import art.arcane.iris.core.gui.PregeneratorJob; import art.arcane.iris.core.pregenerator.PregenTask; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.util.common.director.DirectorExecutor; -import art.arcane.iris.util.common.director.DirectorHelp; import art.arcane.volmlib.util.director.annotations.Director; import art.arcane.volmlib.util.director.annotations.Param; import art.arcane.iris.util.common.format.C; @@ -33,11 +32,6 @@ import org.bukkit.util.Vector; @Director(name = "pregen", aliases = "pregenerate", description = "Pregenerate your Iris worlds!") public class CommandPregen implements DirectorExecutor { - @Director(description = "Show help tree for this command group", aliases = {"?"}) - public void help() { - DirectorHelp.print(sender(), getClass()); - } - @Director(description = "Pregenerate a world") public void start( @Param(description = "The radius of the pregen in blocks", aliases = "size") @@ -81,7 +75,7 @@ public class CommandPregen implements DirectorExecutor { } } - @Director(description = "Pause / continue the active pregeneration task", aliases = {"t", "resume", "unpause"}) + @Director(description = "Pause / continue the active pregeneration task", aliases = {"resume"}) public void pause() { if (PregeneratorJob.pauseResume()) { sender().sendMessage(C.GREEN + "Paused/unpaused pregeneration task, now: " + (PregeneratorJob.isPaused() ? "Paused" : "Running") + "."); 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 d48206d5e..8ec197613 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 @@ -22,10 +22,11 @@ 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.StructureModeHandler; -import art.arcane.iris.core.structure.VillageImporter; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.IrisStructureLocator; import art.arcane.iris.engine.framework.PlacedStructurePiece; import art.arcane.iris.engine.framework.StructureAssembler; +import art.arcane.iris.engine.framework.StructureReachability; import art.arcane.iris.engine.object.IObjectPlacer; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisObjectPlacement; @@ -33,7 +34,6 @@ import art.arcane.iris.engine.object.IrisStructure; import art.arcane.iris.engine.object.ObjectPlaceMode; 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.volmlib.util.director.DirectorOrigin; import art.arcane.volmlib.util.director.annotations.Director; @@ -51,17 +51,14 @@ import org.bukkit.block.data.BlockData; import org.bukkit.util.StructureSearchResult; import java.io.File; +import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Set; -@Director(name = "structure", aliases = {"struct", "str"}, studio = true, description = "Iris structure tools (index, import, info)") +@Director(name = "structure", aliases = {"struct", "str"}, 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) + @Director(description = "Regenerate structure-index.json listing all vanilla, datapack & iris structures", aliases = {"ls"}, origin = DirectorOrigin.BOTH) public void list( @Param(description = "The dimension whose pack to index", aliases = "dim") IrisDimension dimension @@ -75,106 +72,9 @@ public class CommandStructure implements DirectorExecutor { 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( + @Director(name = "import", description = "Import EVERY structure - vanilla AND ingested datapacks - into this pack as editable Iris resources, always overwriting. Rebuilds jigsaw structures (villages, outposts, datapack jigsaws) as editable pool/piece graphs, imports every structure template NBT as an object, and assembles the multi-template structures (shipwrecks, ruined portals, ocean ruins, nether fossils). Run after ingesting a datapack and restarting. Regenerate chunks or use a fresh world for the imported copies to place.", aliases = {"import-all", "reimport", "imp", "all"}, origin = DirectorOrigin.BOTH) + public void importAll( @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", customHandler = StructureModeHandler.class) - 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", customHandler = StructureModeHandler.class) - 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 = "Import EVERY registered structure - vanilla AND ingested datapacks - 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", customHandler = StructureModeHandler.class) - 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", customHandler = StructureModeHandler.class) - 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 = "Re-ingest EVERY registered structure - vanilla AND ingested datapacks - plus every jigsaw template from scratch (overwrite), regenerating all .iob objects so they pick up the latest jigsaw/structure-block conversion. Use this after updating Iris or after ingesting datapacks if imported structures show raw markers.", aliases = {"reimport", "ri"}, origin = DirectorOrigin.BOTH) - public void reingest( - @Param(description = "The dimension whose pack to re-ingest", aliases = "dim") IrisDimension dimension ) { IrisData data = dimension.getLoader(); @@ -182,17 +82,17 @@ public class CommandStructure implements DirectorExecutor { sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey()); return; } - sender().sendMessage(C.GREEN + "Re-ingesting all vanilla structures and jigsaws for " + C.WHITE + dimension.getLoadKey() + C.GREEN + " (overwrite)..."); + sender().sendMessage(C.GREEN + "Importing all vanilla & datapack structures into " + C.WHITE + dimension.getLoadKey() + C.GREEN + " (overwrite)..."); BulkStructureImporter.Report jigsaws = BulkStructureImporter.importAllVanilla(data, StructureImporter.Mode.OVERWRITE, true, sender()); BulkStructureImporter.Report templates = BulkStructureImporter.importAllTemplates(data, StructureImporter.Mode.OVERWRITE, sender()); BulkStructureImporter.Report groups = BulkStructureImporter.importTemplateGroups(data, StructureImporter.Mode.OVERWRITE, sender()); int imported = jigsaws.imported() + templates.imported() + groups.imported(); int failed = jigsaws.failed() + templates.failed() + groups.failed(); - sender().sendMessage(C.GREEN + "Re-ingest complete: " + C.WHITE + imported + C.GREEN + " objects rewritten, " + C.WHITE + failed + C.GREEN + " failed."); - sender().sendMessage(C.GRAY + "Regenerate chunks (or use a fresh world) for structures to place from the rewritten objects. Run /iris structure list " + dimension.getLoadKey() + " to refresh the index."); + sender().sendMessage(C.GREEN + "Import complete: " + C.WHITE + imported + C.GREEN + " structures/objects written, " + C.WHITE + failed + C.GREEN + " failed."); + sender().sendMessage(C.GRAY + "Reference them from a biome/region/dimension 'structures' list, or run /iris structure list " + dimension.getLoadKey() + " to refresh the index. Regenerate chunks for changes to take effect."); } - @Director(description = "Locate every vanilla/datapack/iris structure to verify which are locatable in this world. Heavy synchronous search per structure - keep the radius modest. 'Not found' can mean rarer than the radius, a different dimension (nether/end structures never appear in the overworld), or a structure that generates but cannot be located; generation itself happens during chunk decoration, independent of locate.", aliases = {"verify", "test", "locateall"}, origin = DirectorOrigin.BOTH, sync = true) + @Director(description = "Locate every vanilla/datapack/iris structure to verify which are locatable in this world. Heavy synchronous search per structure - keep the radius modest. 'Not found' can mean rarer than the radius, a different dimension (nether/end structures never appear in the overworld), or a structure that generates but cannot be located; generation itself happens during chunk decoration, independent of locate.", aliases = {"locateall"}, origin = DirectorOrigin.BOTH, sync = true) public void verify( @Param(description = "The dimension to verify", aliases = "dim") IrisDimension dimension, @@ -210,18 +110,39 @@ public class CommandStructure implements DirectorExecutor { sender().sendMessage(C.GREEN + "Verifying structures in " + C.WHITE + world.getName() + C.GREEN + " from " + center.getBlockX() + "," + center.getBlockZ() + " within " + searchRadius + " chunks..."); + Engine engine = null; + PlatformChunkGenerator access = IrisToolbelt.access(world); + if (access != null) { + engine = access.getEngine(); + } + Set reachable = engine == null ? Collections.emptySet() : StructureReachability.reachableKeys(engine); + int found = 0; int missing = 0; + int unreachable = 0; + int irisPlaced = 0; KList notFound = new KList<>(); + KList cannotGenerate = new KList<>(); for (org.bukkit.generator.structure.Structure structure : Registry.STRUCTURE) { NamespacedKey key = structure.getKey(); String keyName = key == null ? structure.toString() : key.toString(); + boolean isIrisPlaced = engine != null && IrisStructureLocator.suppressesVanilla(engine, keyName); + boolean isReachable = engine != null && reachable.contains(keyName.toLowerCase()); + if (engine != null && !isIrisPlaced && !isReachable) { + unreachable++; + KList miss = StructureReachability.missingBiomeKeys(engine, keyName); + cannotGenerate.add(keyName + (miss.isEmpty() ? "" : " (needs " + String.join("/", miss) + ")")); + continue; + } + if (isIrisPlaced) { + irisPlaced++; + } try { StructureSearchResult result = world.locateNearestStructure(center, structure, searchRadius, true); if (result != null && result.getLocation() != null) { found++; Location l = result.getLocation(); - sender().sendMessage(C.GREEN + "[ok] " + C.WHITE + keyName + C.GREEN + " @ " + l.getBlockX() + "," + l.getBlockZ()); + sender().sendMessage((isIrisPlaced ? C.AQUA + "[iris] " : C.GREEN + "[ok] ") + C.WHITE + keyName + C.GREEN + " @ " + l.getBlockX() + "," + l.getBlockZ()); } else { missing++; notFound.add(keyName); @@ -232,9 +153,14 @@ public class CommandStructure implements DirectorExecutor { } } - sender().sendMessage(C.GREEN + "Structure verify: " + C.WHITE + found + C.GREEN + " generate, " + C.WHITE + missing + C.GREEN + " not found within " + searchRadius + " chunks."); + sender().sendMessage(C.GREEN + "Structure verify: " + C.WHITE + found + C.GREEN + " located (" + irisPlaced + " iris-placed), " + + C.WHITE + unreachable + C.GREEN + " cannot generate here, " + + C.WHITE + missing + C.GREEN + " reachable-but-not-found within " + searchRadius + " chunks."); + if (!cannotGenerate.isEmpty()) { + sender().sendMessage(C.RED + "Cannot generate (required biomes absent from this pack): " + C.GRAY + String.join(", ", cannotGenerate)); + } if (!notFound.isEmpty()) { - sender().sendMessage(C.YELLOW + "Not found (rarer than radius, disabled, or non-generating): " + C.GRAY + String.join(", ", notFound)); + sender().sendMessage(C.YELLOW + "Reachable but not found (rarer than radius): " + C.GRAY + String.join(", ", notFound)); } } 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 14af8b6fa..26698011a 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 @@ -28,15 +28,12 @@ import art.arcane.iris.core.service.StudioSVC; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.*; -import art.arcane.iris.engine.platform.ChunkReplacementListener; -import art.arcane.iris.engine.platform.ChunkReplacementOptions; import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KSet; import art.arcane.iris.util.common.director.DirectorContext; import art.arcane.iris.util.common.director.DirectorExecutor; -import art.arcane.iris.util.common.director.DirectorHelp; import art.arcane.iris.util.common.director.handlers.DimensionHandler; import art.arcane.iris.util.common.director.specialhandlers.NullableDimensionHandler; import art.arcane.volmlib.util.director.DirectorOrigin; @@ -89,13 +86,8 @@ import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; -@Director(name = "studio", aliases = {"std", "s"}, description = "Studio Commands", studio = true) +@Director(name = "studio", aliases = {"std", "s"}, description = "Studio Commands") public class CommandStudio implements DirectorExecutor { - @Director(description = "Show help tree for this command group", aliases = {"?"}) - public void help() { - DirectorHelp.print(sender(), getClass()); - } - private CommandEdit edit; //private CommandDeepSearch deepSearch; @@ -103,21 +95,6 @@ public class CommandStudio implements DirectorExecutor { return duration.toString().substring(2).replaceAll("(\\d[HMS])(?!$)", "$1 ").toLowerCase(); } - //TODO fix pack trimming - @Director(description = "Download a project.", aliases = "dl") - public void download( - @Param(name = "pack", description = "The pack to download", defaultValue = "overworld", aliases = "project") - String pack, - @Param(name = "branch", description = "The branch to download from", defaultValue = "stable") - String branch, - //@Param(name = "trim", description = "Whether or not to download a trimmed version (do not enable when editing)", defaultValue = "false") - //boolean trim, - @Param(name = "overwrite", description = "Whether or not to overwrite the pack with the downloaded one", aliases = "force", defaultValue = "false") - boolean overwrite - ) { - new CommandIris().download(pack, branch, overwrite); - } - @Director(description = "Open a new studio world", aliases = "o", sync = true) public void open( @Param(defaultValue = "default", description = "The dimension to open a studio for", aliases = "dim", customHandler = DimensionHandler.class) @@ -128,7 +105,7 @@ public class CommandStudio implements DirectorExecutor { Iris.service(StudioSVC.class).open(sender(), seed, dimension.getLoadKey()); } - @Director(description = "Open VSCode for a dimension", aliases = {"vsc", "edit"}) + @Director(description = "Open VSCode for a dimension", aliases = {"vsc"}) public void vscode( @Param(defaultValue = "default", description = "The dimension to open VSCode for", aliases = "dim", customHandler = DimensionHandler.class) IrisDimension dimension @@ -137,7 +114,7 @@ public class CommandStudio implements DirectorExecutor { Iris.service(StudioSVC.class).openVSCode(sender(), dimension.getLoadKey()); } - @Director(description = "Close an open studio project", aliases = {"x", "c"}, sync = true) + @Director(description = "Close an open studio project", aliases = {"x"}, sync = true) public void close() { VolmitSender commandSender = sender(); if (!Iris.service(StudioSVC.class).isProjectOpen()) { @@ -191,7 +168,7 @@ public class CommandStudio implements DirectorExecutor { sender().sendMessage(C.GREEN + "The \"" + dimension.getName() + "\" pack has version: " + dimension.getVersion()); } - @Director(description = "Open the noise explorer (External GUI)", aliases = {"nmap", "n", "generator", "gen"}) + @Director(description = "Open the noise explorer (External GUI)", aliases = {"nmap"}) public void noise( @Param(description = "Optional pack generator to preview", defaultValue = "null", contextual = true) IrisGenerator generator, diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandWhat.java b/core/src/main/java/art/arcane/iris/core/commands/CommandWhat.java index 936a9be91..59b72b4bf 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandWhat.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandWhat.java @@ -27,7 +27,6 @@ import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.util.common.data.B; import art.arcane.iris.util.common.director.DirectorExecutor; -import art.arcane.iris.util.common.director.DirectorHelp; import art.arcane.volmlib.util.director.DirectorOrigin; import art.arcane.volmlib.util.director.annotations.Director; import art.arcane.volmlib.util.director.annotations.Param; @@ -44,13 +43,8 @@ import java.lang.reflect.Method; import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; -@Director(name = "what", origin = DirectorOrigin.PLAYER, studio = true, description = "Iris What?") +@Director(name = "what", origin = DirectorOrigin.PLAYER, description = "Iris What?") public class CommandWhat implements DirectorExecutor { - @Director(description = "Show help tree for this command group", aliases = {"?"}) - public void help() { - DirectorHelp.print(sender(), getClass()); - } - @Director(description = "What is in my hand?", origin = DirectorOrigin.PLAYER) public void hand() { try { diff --git a/core/src/main/java/art/arcane/iris/core/datapack/DatapackIngestService.java b/core/src/main/java/art/arcane/iris/core/datapack/DatapackIngestService.java index ddefa0d4a..52e90bec7 100644 --- a/core/src/main/java/art/arcane/iris/core/datapack/DatapackIngestService.java +++ b/core/src/main/java/art/arcane/iris/core/datapack/DatapackIngestService.java @@ -24,7 +24,10 @@ import art.arcane.iris.core.ServerConfigurator; import art.arcane.iris.core.datapack.ModrinthResolver.ResolvedDatapack; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.project.IrisProject; +import art.arcane.iris.core.structure.BulkStructureImporter; +import art.arcane.iris.core.structure.StructureImporter; import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisImportedStructureControl; import art.arcane.iris.util.common.format.C; import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.volmlib.util.collection.KList; @@ -53,10 +56,12 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; public final class DatapackIngestService { private static final String USER_AGENT = "VolmitSoftware/Iris (datapack-ingest)"; + private static final String OVERRIDES_STRIPPED_MARKER = ".iris-overrides-stripped"; private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); private DatapackIngestService() { @@ -78,6 +83,7 @@ public final class DatapackIngestService { } if (!restarting) { refreshWorkspaces(); + autoImportDatapackStructures(); } } @@ -114,12 +120,13 @@ public final class DatapackIngestService { KList worldFolders = ServerConfigurator.getDatapacksFolder(); String mcVersion = serverMcVersion(); Manifest manifest = readManifest(root); + boolean stripOverrides = resolveStripOverrides(); - message(sender, C.GRAY + "Ingesting " + C.WHITE + urls.size() + C.GRAY + " datapack import(s)" + (mcVersion == null ? "" : " for MC " + mcVersion) + "..."); + message(sender, C.GRAY + "Ingesting " + C.WHITE + urls.size() + C.GRAY + " datapack import(s)" + (mcVersion == null ? "" : " for MC " + mcVersion) + (stripOverrides ? C.GRAY + " (datapackOverrides=false: vanilla-key overrides will be stripped)" : "") + "..."); for (String url : urls) { try { - ingestSingle(sender, url, mcVersion, cacheDir, stagingDir, worldFolders, manifest, report); + ingestSingle(sender, url, mcVersion, cacheDir, stagingDir, worldFolders, manifest, report, stripOverrides); } catch (Exception e) { report.failed.add(url + " - " + e.getMessage()); message(sender, C.RED + " Failed: " + C.WHITE + url + C.RED + " - " + e.getMessage()); @@ -132,8 +139,8 @@ public final class DatapackIngestService { if (report.changed()) { message(sender, C.YELLOW + "New datapack structures were installed. A server restart is required for them to register and generate."); - message(sender, C.GRAY + "Disable a vanilla structure via the dimension 'importedStructures.disabled' list to let an imported replacement take over, or place any imported key through a 'structures' placement."); - message(sender, C.GRAY + "To edit or manually place them as Iris resources, after the restart run /iris structure importAllVanilla - it imports vanilla and datapack structures together as editable jigsaw pools, pieces & objects."); + message(sender, C.GRAY + "After the restart their jigsaw pools, pieces & objects are imported automatically (set general.autoImportDatapackStructures=false to disable), or run /iris structure import to import everything on demand. Reference an imported key from a 'structures' placement to position it manually."); + message(sender, C.GRAY + "Datapacks replace matching vanilla structure keys and generate their own structures by default; set the dimension 'importedStructures.datapackOverrides' to false to keep vanilla untouched and stop ALL datapack structures from generating - they stay importable for manual placement only."); if (restart) { ServerConfigurator.restart(); } else { @@ -153,12 +160,13 @@ public final class DatapackIngestService { if (staged == null || staged.length == 0) { return; } + boolean stripOverrides = resolveStripOverrides(); for (File stagedDir : staged) { if (!new File(stagedDir, "pack.mcmeta").isFile()) { continue; } try { - install(stagedDir, worldFolders, stagedDir.getName(), false); + install(stagedDir, worldFolders, stagedDir.getName(), false, stripOverrides); } catch (IOException e) { Iris.reportError(e); } @@ -210,7 +218,7 @@ public final class DatapackIngestService { return readManifest(Iris.instance.getDataFolder("datapacks")).entries; } - private static void ingestSingle(VolmitSender sender, String url, String mcVersion, File cacheDir, File stagingDir, KList worldFolders, Manifest manifest, Report report) throws IOException { + private static void ingestSingle(VolmitSender sender, String url, String mcVersion, File cacheDir, File stagingDir, KList worldFolders, Manifest manifest, Report report, boolean stripOverrides) throws IOException { ResolvedDatapack resolved = ModrinthResolver.resolve(url, mcVersion); String id = deriveId(resolved); File stagedDir = new File(stagingDir, id); @@ -222,7 +230,7 @@ public final class DatapackIngestService { && new File(stagedDir, "pack.mcmeta").isFile(); if (sameVersion) { - install(stagedDir, worldFolders, id, false); + install(stagedDir, worldFolders, id, false, stripOverrides); report.upToDate.add(id + " (" + safe(resolved.getVersionNumber()) + ")"); message(sender, C.GRAY + " Up to date: " + C.WHITE + id + C.GRAY + " " + safe(resolved.getVersionNumber())); return; @@ -247,7 +255,7 @@ public final class DatapackIngestService { throw new IOException(id + " is not a valid datapack (missing pack.mcmeta)"); } - install(stagedDir, worldFolders, id, true); + install(stagedDir, worldFolders, id, true, stripOverrides); Entry entry = existing != null ? existing : new Entry(); entry.url = url; @@ -257,6 +265,7 @@ public final class DatapackIngestService { entry.sha1 = checksum; entry.filename = resolved.getFileName(); entry.installedEpoch = System.currentTimeMillis(); + entry.structuresImported = false; manifest.put(entry); report.updated.add(id + " (" + safe(resolved.getVersionNumber()) + ")"); @@ -299,10 +308,13 @@ public final class DatapackIngestService { return false; } - private static void install(File stagedDir, KList worldFolders, String id, boolean force) throws IOException { + private static void install(File stagedDir, KList worldFolders, String id, boolean force, boolean stripOverrides) throws IOException { for (File worldFolder : worldFolders) { File target = new File(worldFolder, id); - if (!force && target.isDirectory() && new File(target, "pack.mcmeta").isFile()) { + File marker = new File(target, OVERRIDES_STRIPPED_MARKER); + boolean installed = target.isDirectory() && new File(target, "pack.mcmeta").isFile(); + boolean stripStateMatches = marker.isFile() == stripOverrides; + if (!force && installed && stripStateMatches) { continue; } if (!worldFolder.exists()) { @@ -310,6 +322,104 @@ public final class DatapackIngestService { } IO.delete(target); IO.copyDirectory(stagedDir.toPath(), target.toPath()); + if (stripOverrides) { + stripVanillaStructureOverrides(target); + writeMarker(marker); + } + } + } + + private static boolean resolveStripOverrides() { + try (Stream stream = ServerConfigurator.allPacks()) { + return stream.anyMatch(DatapackIngestService::packDisablesOverrides); + } + } + + private static boolean packDisablesOverrides(IrisData data) { + if (data == null || data.getDimensionLoader() == null) { + return false; + } + for (IrisDimension dimension : data.getDimensionLoader().loadAll(data.getDimensionLoader().getPossibleKeys())) { + if (dimension == null) { + continue; + } + IrisImportedStructureControl control = dimension.getImportedStructures(); + if (control != null && !control.isDatapackOverrides()) { + return true; + } + } + return false; + } + + private static void stripVanillaStructureOverrides(File datapackRoot) { + File minecraftData = new File(new File(datapackRoot, "data"), "minecraft"); + if (!minecraftData.isDirectory()) { + return; + } + String[] relativeTrees = { + "worldgen" + File.separator + "structure_set", + "worldgen" + File.separator + "structure", + "worldgen" + File.separator + "template_pool", + "structure" + }; + for (String tree : relativeTrees) { + File dir = new File(minecraftData, tree); + if (dir.exists()) { + IO.delete(dir); + } + } + } + + private static void writeMarker(File marker) { + try { + Files.writeString(marker.toPath(), "stripped", StandardCharsets.UTF_8); + } catch (IOException e) { + Iris.reportError(e); + } + } + + private static void autoImportDatapackStructures() { + if (!IrisSettings.get().getGeneral().autoImportDatapackStructures) { + return; + } + File root = Iris.instance.getDataFolder("datapacks"); + Manifest manifest = readManifest(root); + if (manifest.entries.isEmpty()) { + return; + } + boolean pending = false; + for (Entry entry : manifest.entries) { + if (!entry.structuresImported) { + pending = true; + break; + } + } + if (!pending) { + return; + } + + Iris.info("Importing datapack structures (jigsaw pools, pieces & objects) into packs that declare datapackImports..."); + AtomicInteger packs = new AtomicInteger(); + try (Stream stream = ServerConfigurator.allPacks()) { + stream.forEach(data -> { + if (data == null || !hasImports(data)) { + return; + } + try { + BulkStructureImporter.importDatapackStructures(data, StructureImporter.Mode.ADD_ONLY, Iris.getSender()); + packs.incrementAndGet(); + } catch (Throwable e) { + Iris.reportError(e); + } + }); + } + + for (Entry entry : manifest.entries) { + entry.structuresImported = true; + } + writeManifest(root, manifest); + if (packs.get() > 0) { + Iris.info("Datapack structure import finished for " + packs.get() + " pack(s). Reference the imported keys from a 'structures' placement to position them manually."); } } @@ -540,6 +650,7 @@ public final class DatapackIngestService { public String sha1; public String filename; public long installedEpoch; + public boolean structuresImported; } private static final class Manifest { diff --git a/core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java b/core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java index 78101f79f..40a1a9815 100644 --- a/core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java +++ b/core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java @@ -169,6 +169,15 @@ public class PregeneratorJob implements PregenListener { return targetName != null && targetName.equalsIgnoreCase(world.getName()); } + public boolean targetsWorldName(String worldName) { + if (worldName == null || engine == null || engine.getWorld() == null) { + return false; + } + + String targetName = engine.getWorld().name(); + return targetName != null && targetName.equalsIgnoreCase(worldName); + } + private static Color parseColor(String c) { String v = (c.startsWith("#") ? c : "#" + c).trim(); try { 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 7b30c2c19..78ec058a8 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 @@ -97,6 +97,18 @@ public interface INMSBinding { return new KList<>(); } + default KList getReachableStructureKeys(World world) { + return new KList<>(); + } + + default KList getStructureBiomeKeys(String structureKey) { + return new KList<>(); + } + + default KList getPossibleBiomeKeys(World world) { + return new KList<>(); + } + boolean isBukkit(); int getBiomeId(Biome biome); @@ -161,6 +173,10 @@ public interface INMSBinding { return 441; } + default boolean purgeChunk(World world, int x, int z) { + throw new UnsupportedOperationException("The active NMS binding does not support chunk purge (regen)."); + } + boolean missingDimensionTypes(String... keys); default boolean injectBukkit() { diff --git a/core/src/main/java/art/arcane/iris/core/nms/v1X/NMSBinding1X.java b/core/src/main/java/art/arcane/iris/core/nms/v1X/NMSBinding1X.java index 0af51949b..91fd301f1 100644 --- a/core/src/main/java/art/arcane/iris/core/nms/v1X/NMSBinding1X.java +++ b/core/src/main/java/art/arcane/iris/core/nms/v1X/NMSBinding1X.java @@ -117,6 +117,11 @@ public class NMSBinding1X implements INMSBinding { return false; } + @Override + public boolean purgeChunk(World world, int x, int z) { + throw new UnsupportedOperationException("Regen is unavailable: Iris is running in Bukkit fallback mode (NMS disabled). Regen requires the native chunk system."); + } + @Override public KMap> getBlockProperties() { KMap> map = new KMap<>(); diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java b/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java index 341328fe2..5a94fa9fd 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java @@ -284,6 +284,7 @@ public class IrisPregenerator { Mantle mantle = getMantle(); if (mantle != null) { mantle.trim(0, 0); + mantle.unloadTectonicPlate(0); } } diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java index eea9ba83d..d5665e80f 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java @@ -87,6 +87,12 @@ public class AsyncPregenMethod implements PregeneratorMethod { private final AtomicLong lastChunkCleanup = new AtomicLong(M.ms()); private final Object permitMonitor = new Object(); private volatile Engine metricsEngine; + private volatile Mantle cachedMantle; + private final int maxResidentTectonicPlates; + private final int mantleBackpressureWaitMs; + private final long mantleBackpressureTimeoutMs; + private final long mantleReclaimIntervalMs; + private final AtomicLong lastMantleReclaim = new AtomicLong(0L); public AsyncPregenMethod(World world, int unusedThreads) { if (!PaperLib.isPaper()) { @@ -136,6 +142,10 @@ public class AsyncPregenMethod implements PregeneratorMethod { this.lastUse = new ConcurrentHashMap<>(); this.adaptiveInFlightLimit = new AtomicInteger(this.threads); this.adaptiveMinInFlightLimit = Math.max(4, Math.min(16, Math.max(1, this.threads / 4))); + this.maxResidentTectonicPlates = pregen.getMaxResidentTectonicPlates(); + this.mantleBackpressureWaitMs = pregen.getMantleBackpressureWaitMs(); + this.mantleBackpressureTimeoutMs = pregen.getMantleBackpressureTimeoutMs(); + this.mantleReclaimIntervalMs = 1_000L; } private IrisPaperLikeBackendMode resolvePaperLikeBackendMode(IrisSettings.IrisSettingsPregen pregen) { @@ -467,6 +477,75 @@ public class AsyncPregenMethod implements PregeneratorMethod { } } + private Mantle resolveMantle() { + Mantle cached = cachedMantle; + if (cached != null) { + return cached; + } + + Mantle resolved = getMantle(); + if (resolved != null) { + cachedMantle = resolved; + } + return resolved; + } + + private void enforceMantleBudget() { + int cap = maxResidentTectonicPlates; + if (cap <= 0) { + return; + } + + Mantle mantle = resolveMantle(); + if (mantle == null) { + return; + } + + long now = M.ms(); + long lastReclaimAt = lastMantleReclaim.get(); + if (now - lastReclaimAt >= mantleReclaimIntervalMs && lastMantleReclaim.compareAndSet(lastReclaimAt, now)) { + mantle.trim(0L, 0); + mantle.unloadTectonicPlate(0); + } + + if (mantle.getLoadedRegionCount() <= cap) { + return; + } + + long waitStart = M.ms(); + long lastLog = 0L; + while (mantle.getLoadedRegionCount() > cap) { + mantle.trim(0L, 0); + int freed = mantle.unloadTectonicPlate(0); + int resident = mantle.getLoadedRegionCount(); + if (resident <= cap) { + break; + } + + long elapsed = M.ms() - waitStart; + if (elapsed >= mantleBackpressureTimeoutMs) { + Iris.warn("Pregen mantle backpressure exceeded " + mantleBackpressureTimeoutMs + "ms with " + resident + + " tectonic plates resident (cap " + cap + "); proceeding to avoid deadlock. " + + "Raise pregen.maxResidentTectonicPlates if this persists. " + metricsSnapshot()); + return; + } + + long logNow = M.ms(); + if (logNow - lastLog >= 5_000L) { + lastLog = logNow; + Iris.warn("Pregen mantle backpressure: " + resident + " tectonic plates resident (cap " + cap + + "), freed " + freed + " last pass, waited " + elapsed + "ms."); + } + + try { + Thread.sleep(mantleBackpressureWaitMs); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + } + @Override public void init() { Iris.info("Async pregen init: world=" + world.getName() @@ -522,6 +601,7 @@ public class AsyncPregenMethod implements PregeneratorMethod { public void generateChunk(int x, int z, PregenListener listener) { listener.onChunkGenerating(x, z); periodicChunkCleanup(); + enforceMantleBudget(); try { long waitStart = M.ms(); synchronized (permitMonitor) { diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/RegenPregenMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/RegenPregenMethod.java new file mode 100644 index 000000000..697d48448 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/RegenPregenMethod.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.core.pregenerator.methods; + +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.core.pregenerator.PregenListener; +import art.arcane.iris.core.pregenerator.PregeneratorMethod; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import org.bukkit.World; + +public class RegenPregenMethod implements PregeneratorMethod { + private final World world; + private final AsyncPregenMethod delegate; + + public RegenPregenMethod(World world) { + this.world = world; + this.delegate = new AsyncPregenMethod(world, 0); + } + + @Override + public void init() { + delegate.init(); + } + + @Override + public void close() { + delegate.close(); + } + + @Override + public void save() { + delegate.save(); + } + + @Override + public boolean supportsRegions(int x, int z, PregenListener listener) { + return delegate.supportsRegions(x, z, listener); + } + + @Override + public String getMethod(int x, int z) { + return "Regen"; + } + + @Override + public boolean isAsyncChunkMode() { + return delegate.isAsyncChunkMode(); + } + + @Override + public void generateRegion(int x, int z, PregenListener listener) { + delegate.generateRegion(x, z, listener); + } + + @Override + public void generateChunk(int x, int z, PregenListener listener) { + purge(x, z); + delegate.generateChunk(x, z, listener); + } + + @Override + public Mantle getMantle() { + return delegate.getMantle(); + } + + private void purge(int x, int z) { + if (!IrisToolbelt.isIrisWorld(world)) { + return; + } + + Engine engine = IrisToolbelt.access(world).getEngine(); + if (engine == null) { + return; + } + + int radius = Math.max(0, engine.getMantle().getRealRadius()); + Mantle mantle = engine.getMantle().getMantle(); + for (int dx = -radius; dx <= radius; dx++) { + for (int dz = -radius; dz <= radius; dz++) { + mantle.deleteChunk(x + dx, z + dz); + } + } + + INMS.get().purgeChunk(world, x, z); + } +} 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 89896e740..de690dd75 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 @@ -25,6 +25,7 @@ import art.arcane.iris.core.loader.IrisData; 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.core.structure.StructureSchemaKeys; import art.arcane.iris.engine.object.annotations.*; import art.arcane.iris.core.nms.INMS; import art.arcane.volmlib.util.collection.KList; @@ -43,6 +44,7 @@ import java.awt.*; import java.lang.reflect.Field; import java.lang.reflect.InaccessibleObjectException; import java.lang.reflect.Modifier; +import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Map; @@ -245,6 +247,22 @@ public class SchemaBuilder { } else { Iris.error("Cannot find Registry Loader for type " + rr.value() + " used in " + k.getDeclaringClass().getCanonicalName() + " in field " + k.getName()); } + } else if (k.isAnnotationPresent(RegistryListStructure.class)) { + String key = "enum-iris-structure-placement"; + + if (!definitions.containsKey(key)) { + JSONObject j = new JSONObject(); + j.put("enum", new JSONArray(StructureSchemaKeys.collect( + INMS.get().getStructureKeys(), + Arrays.asList(data.getStructureLoader().getPossibleKeys()), + Arrays.asList(data.getJigsawPieceLoader().getPossibleKeys())).toArray(new String[0]))); + definitions.put(key, j); + } + + fancyType = "Structure"; + prop.put("$ref", "#/definitions/" + key); + description.add(SYMBOL_TYPE__N + " Must be a valid vanilla, datapack, or imported Iris structure (use ctrl+space for auto complete!)"); + } else if (k.isAnnotationPresent(RegistryListBlockType.class)) { String key = "enum-block-type"; @@ -522,6 +540,23 @@ public class SchemaBuilder { } else { Iris.error("Cannot find Registry Loader for type (list schema) " + rr.value() + " used in " + k.getDeclaringClass().getCanonicalName() + " in field " + k.getName()); } + } else if (k.isAnnotationPresent(RegistryListStructure.class)) { + fancyType = "List"; + String key = "enum-iris-structure-placement"; + + if (!definitions.containsKey(key)) { + JSONObject j = new JSONObject(); + j.put("enum", new JSONArray(StructureSchemaKeys.collect( + INMS.get().getStructureKeys(), + Arrays.asList(data.getStructureLoader().getPossibleKeys()), + Arrays.asList(data.getJigsawPieceLoader().getPossibleKeys())).toArray(new String[0]))); + definitions.put(key, j); + } + + JSONObject items = new JSONObject(); + items.put("$ref", "#/definitions/" + key); + prop.put("items", items); + description.add(SYMBOL_TYPE__N + " Must be a valid vanilla, datapack, or imported Iris structure (use ctrl+space for auto complete!)"); } else if (k.isAnnotationPresent(RegistryListBlockType.class)) { fancyType = "List of Block Types"; String key = "enum-block-type"; diff --git a/core/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java b/core/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java index 45a96e457..d566cbc41 100644 --- a/core/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java +++ b/core/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java @@ -308,7 +308,7 @@ public class IrisEngineSVC implements IrisService { } try { - engine.getMantle().trim(activeIdleDuration(engineWorld), activeTectonicLimit(engineWorld)); + engine.getMantle().trim(activeIdleDuration(engine), activeTectonicLimit(engine)); } catch (Throwable e) { if (isMantleClosed(e)) { close(); @@ -336,7 +336,7 @@ public class IrisEngineSVC implements IrisService { try { long unloadStart = System.currentTimeMillis(); - int count = engine.getMantle().unloadTectonicPlate(IrisSettings.get().getPerformance().getEngineSVC().forceMulticoreWrite ? 0 : activeTectonicLimit(engineWorld)); + int count = engine.getMantle().unloadTectonicPlate(IrisSettings.get().getPerformance().getEngineSVC().forceMulticoreWrite ? 0 : activeTectonicLimit(engine)); if (count > 0) { Iris.debug(C.GOLD + "Unloaded " + C.YELLOW + count + " TectonicPlates in " + C.RED + Form.duration(System.currentTimeMillis() - unloadStart, 2)); } @@ -357,27 +357,26 @@ public class IrisEngineSVC implements IrisService { return tectonicLimit.get() / Math.max(worlds.size(), 1); } - private int activeTectonicLimit(@Nullable World world) { - int limit = tectonicLimit(); - if (world == null) { - return limit; + private boolean pregenTargets(Engine engine) { + if (engine == null || engine.getWorld() == null) { + return false; } PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance(); - if (pregeneratorJob == null || !pregeneratorJob.targetsWorld(world)) { + return pregeneratorJob != null && pregeneratorJob.targetsWorldName(engine.getWorld().name()); + } + + private int activeTectonicLimit(Engine engine) { + int limit = tectonicLimit(); + if (!pregenTargets(engine)) { return limit; } return Math.max(1, Math.min(limit, Math.max(2, limit / 8))); } - private long activeIdleDuration(@Nullable World world) { - if (world == null) { - return TimeUnit.SECONDS.toMillis(IrisSettings.get().getPerformance().getMantleKeepAlive()); - } - - PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance(); - if (pregeneratorJob == null || !pregeneratorJob.targetsWorld(world)) { + private long activeIdleDuration(Engine engine) { + if (!pregenTargets(engine)) { return TimeUnit.SECONDS.toMillis(IrisSettings.get().getPerformance().getMantleKeepAlive()); } @@ -411,13 +410,7 @@ public class IrisEngineSVC implements IrisService { return true; } - World world = engine.getWorld().realWorld(); - if (world == null) { - return false; - } - - PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance(); - return pregeneratorJob != null && pregeneratorJob.targetsWorld(world); + return pregenTargets(engine); } private boolean shouldSkipForMaintenance(@Nullable World world) { 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 index 0f6a9cf51..0486e215e 100644 --- a/core/src/main/java/art/arcane/iris/core/structure/BulkStructureImporter.java +++ b/core/src/main/java/art/arcane/iris/core/structure/BulkStructureImporter.java @@ -157,31 +157,31 @@ public final class BulkStructureImporter { try { templateKeys = enumerateTemplateKeys(); } catch (Throwable e) { - sender.sendMessage(C.RED + "Failed to enumerate vanilla structure templates via the server ResourceManager: " + e); + sender.sendMessage(C.RED + "Failed to enumerate structure templates via the server ResourceManager: " + e); return new Report(0, 0, 0, 0); } - List vanilla = new ArrayList<>(); + List all = new ArrayList<>(); for (String key : templateKeys) { - if (key != null && key.startsWith("minecraft:")) { - vanilla.add(key); + if (key != null && !key.isBlank()) { + all.add(key); } } - Collections.sort(vanilla); + Collections.sort(all); - int total = vanilla.size(); + int total = all.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."); + sender.sendMessage(C.YELLOW + "No 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 + ")..."); + sender.sendMessage(C.GREEN + "Importing " + C.WHITE + total + C.GREEN + " structure templates (mode=" + mode + ")..."); - for (String keyString : vanilla) { + for (String keyString : all) { NamespacedKey nk = NamespacedKey.fromString(keyString.toLowerCase()); if (nk == null) { failed++; @@ -217,7 +217,117 @@ public final class BulkStructureImporter { return new Report(total, imported, skipped, failed); } - private static String templateNameFor(String key) { + public static Report importDatapackStructures(IrisData data, StructureImporter.Mode mode, VolmitSender sender) { + KList keys = INMS.get().getStructureKeys(); + List datapack = new ArrayList<>(); + for (String k : keys) { + if (k != null && !k.isBlank() && !k.startsWith("minecraft:")) { + datapack.add(k); + } + } + Collections.sort(datapack); + + int total = datapack.size(); + int imported = 0; + int skipped = 0; + int failed = 0; + + if (total == 0) { + sender.sendMessage(C.YELLOW + "No datapack (non-minecraft) structures are registered. Ingest a datapack and restart first, then run this again."); + } else { + sender.sendMessage(C.GREEN + "Importing " + C.WHITE + total + C.GREEN + " datapack structures (mode=" + mode + ")..."); + for (String keyString : datapack) { + 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")) { + 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 if (single.message() != null && single.message().contains("No loadable structure NBT")) { + 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()); + } + } + } + + try { + List templateKeys = enumerateTemplateKeys(); + List datapackTemplates = new ArrayList<>(); + for (String key : templateKeys) { + if (key != null && !key.startsWith("minecraft:")) { + datapackTemplates.add(key); + } + } + Collections.sort(datapackTemplates); + if (!datapackTemplates.isEmpty()) { + sender.sendMessage(C.GREEN + "Importing " + C.WHITE + datapackTemplates.size() + C.GREEN + " datapack structure templates..."); + for (String keyString : datapackTemplates) { + NamespacedKey nk = NamespacedKey.fromString(keyString.toLowerCase()); + if (nk == null) { + failed++; + continue; + } + String name = templateNameFor(keyString); + try { + StructureImporter.Result result = StructureImporter.importStructure(data, nk, name, mode, true); + 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()); + } + } + } + } catch (Throwable e) { + sender.sendMessage(C.YELLOW + "Could not enumerate datapack templates via the server ResourceManager: " + e.getMessage()); + } + + StructureIndexService.write(data); + sender.sendMessage(C.GREEN + "Datapack structure import complete: " + C.WHITE + imported + C.GREEN + " imported, " + C.WHITE + skipped + C.GREEN + " skipped, " + C.WHITE + failed + C.GREEN + " failed."); + return new Report(total, imported, skipped, failed); + } + + 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; 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 3a8c58c10..da87a40dc 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 @@ -21,6 +21,7 @@ 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 art.arcane.volmlib.util.io.IO; import com.google.gson.GsonBuilder; import org.bukkit.Bukkit; import org.bukkit.Location; @@ -150,8 +151,8 @@ public final class StructureImporter { object.write(objectFile); writeJson(pieceFile, pieceJson(name)); if (objectOnly) { - poolFile.delete(); - structureFile.delete(); + IO.deleteUp(poolFile); + IO.deleteUp(structureFile); } else { writeJson(poolFile, poolJson(name)); writeJson(structureFile, structureJson(name, key.toString(), Math.max(w, d))); diff --git a/core/src/main/java/art/arcane/iris/core/structure/StructureModeHandler.java b/core/src/main/java/art/arcane/iris/core/structure/StructureModeHandler.java deleted file mode 100644 index 754c42ca7..000000000 --- a/core/src/main/java/art/arcane/iris/core/structure/StructureModeHandler.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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.volmlib.util.collection.KList; -import art.arcane.volmlib.util.director.DirectorParameterHandler; - -public class StructureModeHandler implements DirectorParameterHandler { - @Override - public KList getPossibilities() { - KList options = new KList<>(); - options.add("overwrite"); - options.add("add-only"); - options.add("merge"); - return options; - } - - @Override - public String toString(String value) { - return value; - } - - @Override - public String parse(String in, boolean force) { - return in == null || in.isBlank() ? "overwrite" : in.trim(); - } - - @Override - public boolean supports(Class type) { - return type.equals(String.class); - } - - @Override - public String getRandomDefault() { - return "overwrite"; - } -} diff --git a/core/src/main/java/art/arcane/iris/core/structure/StructureSchemaKeys.java b/core/src/main/java/art/arcane/iris/core/structure/StructureSchemaKeys.java new file mode 100644 index 000000000..6df4f2e4e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/StructureSchemaKeys.java @@ -0,0 +1,66 @@ +/* + * 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.volmlib.util.collection.KList; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.TreeSet; + +public final class StructureSchemaKeys { + private StructureSchemaKeys() { + } + + public static KList collect(Collection vanillaStructureKeys, Collection importedStructureKeys, Collection jigsawPieceKeys) { + Set pieces = new HashSet<>(); + if (jigsawPieceKeys != null) { + for (String piece : jigsawPieceKeys) { + if (piece != null && !piece.isBlank()) { + pieces.add(piece); + } + } + } + + TreeSet merged = new TreeSet<>(); + if (vanillaStructureKeys != null) { + for (String key : vanillaStructureKeys) { + if (key == null || key.isBlank()) { + continue; + } + merged.add(StructureImporter.deriveName(key)); + } + } + + if (importedStructureKeys != null) { + for (String key : importedStructureKeys) { + if (key == null || key.isBlank()) { + continue; + } + if (pieces.contains(key)) { + continue; + } + merged.add(key); + } + } + + return new KList<>(merged); + } +} 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 adb000053..a98aa5014 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 @@ -20,6 +20,7 @@ package art.arcane.iris.core.structure; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.object.IrisObject; +import art.arcane.volmlib.util.io.IO; import com.google.gson.GsonBuilder; import org.bukkit.Bukkit; import org.bukkit.Material; @@ -606,8 +607,8 @@ public final class VillageImporter { } 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(); + IO.deleteUp(new File(data.getDataFolder(), "jigsaw-pools/" + pieceName + ".json")); + IO.deleteUp(new File(data.getDataFolder(), "structures/" + pieceName + ".json")); } private static boolean writeEmptyPiece(IrisData data, String pieceName) { diff --git a/core/src/main/java/art/arcane/iris/engine/framework/StructureReachability.java b/core/src/main/java/art/arcane/iris/engine/framework/StructureReachability.java new file mode 100644 index 000000000..1a90a3384 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/StructureReachability.java @@ -0,0 +1,122 @@ +/* + * 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.core.loader.IrisData; +import art.arcane.iris.core.nms.INMS; +import art.arcane.volmlib.util.collection.KList; +import org.bukkit.World; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; + +/** + * Determines which vanilla/datapack structures can ever generate in a world, using the same biome + * gate vanilla applies: a structure is reachable when its biome filter intersects the set of vanilla + * biomes the pack actually produces (its {@code possibleBiomes}). Structures whose required biomes are + * never produced cannot generate and must not be scanned for by {@code /locate} or {@code /iris find} + * (an unbounded scan for an absent biome is what stalls the server). + * + *

The reachable set is fixed per pack/world, so it is cached per {@link IrisData}. + */ +public final class StructureReachability { + private static final Map> REACHABLE_CACHE = Collections.synchronizedMap(new WeakHashMap<>()); + + private StructureReachability() { + } + + public static Set reachableKeys(Engine engine) { + if (engine == null) { + return Collections.emptySet(); + } + IrisData data = engine.getData(); + if (data == null) { + return Collections.emptySet(); + } + Set cached = REACHABLE_CACHE.get(data); + if (cached != null) { + return cached; + } + Set built = build(engine); + REACHABLE_CACHE.put(data, built); + return built; + } + + public static boolean isReachable(Engine engine, String structureKey) { + if (structureKey == null || structureKey.isEmpty()) { + return false; + } + return reachableKeys(engine).contains(structureKey.toLowerCase()); + } + + public static void invalidate(Engine engine) { + if (engine == null) { + return; + } + IrisData data = engine.getData(); + if (data != null) { + REACHABLE_CACHE.remove(data); + } + } + + private static Set build(Engine engine) { + World world = engine.getWorld() == null ? null : engine.getWorld().realWorld(); + if (world == null) { + return Collections.emptySet(); + } + Set reachable = new LinkedHashSet<>(); + for (String key : INMS.get().getReachableStructureKeys(world)) { + if (key != null && !key.isEmpty()) { + reachable.add(key.toLowerCase()); + } + } + return reachable; + } + + /** + * The vanilla biome keys (e.g. {@code minecraft:taiga}) from a structure's filter that the pack + * does not produce. An empty list means the structure is reachable. Used by the structure verify + * diagnostic to explain why a structure cannot generate. + */ + public static KList missingBiomeKeys(Engine engine, String structureKey) { + KList missing = new KList<>(); + if (engine == null || structureKey == null || structureKey.isEmpty()) { + return missing; + } + World world = engine.getWorld() == null ? null : engine.getWorld().realWorld(); + if (world == null) { + return missing; + } + Set possible = new LinkedHashSet<>(); + for (String key : INMS.get().getPossibleBiomeKeys(world)) { + if (key != null) { + possible.add(key.toLowerCase()); + } + } + for (String biomeKey : INMS.get().getStructureBiomeKeys(structureKey)) { + if (biomeKey != null && !possible.contains(biomeKey.toLowerCase())) { + missing.add(biomeKey); + } + } + return missing; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/MatterGenerator.java b/core/src/main/java/art/arcane/iris/engine/mantle/MatterGenerator.java index e0602ece4..1cb429872 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/MatterGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/MatterGenerator.java @@ -1,38 +1,24 @@ package art.arcane.iris.engine.mantle; -import art.arcane.iris.Iris; -import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.nms.container.Pair; import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.util.common.misc.RegenRuntime; import art.arcane.iris.util.common.parallel.MultiBurst; import art.arcane.iris.util.project.context.ChunkContext; -import art.arcane.iris.util.project.matter.TileWrapper; import art.arcane.volmlib.util.documentation.ChunkCoordinates; import art.arcane.volmlib.util.mantle.flag.MantleFlag; import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.matter.Matter; -import org.bukkit.block.data.BlockData; import java.util.ArrayList; import java.util.HashSet; -import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; -import java.util.stream.Collectors; public interface MatterGenerator { - long REGEN_PASS_CACHE_TTL_MS = 600000L; Executor DISPATCHER = MultiBurst.burst; - ConcurrentHashMap> REGEN_GENERATED_CHUNKS_BY_PASS = new ConcurrentHashMap<>(); - ConcurrentHashMap> REGEN_CLEARED_CHUNKS_BY_PASS = new ConcurrentHashMap<>(); - ConcurrentHashMap> REGEN_PLANNED_CHUNKS_BY_PASS = new ConcurrentHashMap<>(); - ConcurrentHashMap REGEN_PASS_TOUCHED_MS = new ConcurrentHashMap<>(); Engine getEngine(); @@ -50,84 +36,22 @@ public interface MatterGenerator { return; } - boolean useMulticore = multicore; - String threadName = Thread.currentThread().getName(); - boolean regenThread = threadName.startsWith("Iris-Regen-"); - boolean traceRegen = regenThread && IrisSettings.get().getGeneral().isDebug(); - boolean forceRegen = regenThread; - String regenPassKey = forceRegen ? resolveRegenPassKey(threadName) : null; - boolean optimizedRegen = forceRegen && !IrisSettings.get().getGeneral().isDebug() && regenPassKey != null; - int writeRadius = optimizedRegen ? Math.min(getRadius(), getRealRadius()) : getRadius(); - Set clearedChunks = optimizedRegen ? getRegenPassSet(REGEN_CLEARED_CHUNKS_BY_PASS, regenPassKey) : new HashSet<>(); - Set partialChunks = forceRegen ? null : new HashSet<>(); - Set plannedChunks = optimizedRegen ? getRegenPassSet(REGEN_PLANNED_CHUNKS_BY_PASS, regenPassKey) : null; + int writeRadius = getRadius(); + Set partialChunks = new HashSet<>(); - if (optimizedRegen) { - touchRegenPass(regenPassKey); - } - - if (traceRegen) { - Iris.info("Regen matter start: center=" + x + "," + z - + " radius=" + getRadius() - + " realRadius=" + getRealRadius() - + " writeRadius=" + writeRadius - + " multicore=" + useMulticore - + " components=" + getComponents().size() - + " optimized=" + optimizedRegen - + " passKey=" + (regenPassKey == null ? "none" : regenPassKey) - + " thread=" + threadName); - } - - try (MantleWriter writer = new MantleWriter(getEngine().getMantle(), getMantle(), x, z, writeRadius, useMulticore)) { + try (MantleWriter writer = new MantleWriter(getEngine().getMantle(), getMantle(), x, z, writeRadius, multicore)) { for (Pair, Integer> pair : getComponents()) { - int rawPassRadius = pair.getB(); - int passRadius = optimizedRegen ? Math.min(rawPassRadius, writeRadius) : rawPassRadius; - String passFlags = pair.getA().stream().map(component -> component.getFlag().toString()).collect(Collectors.joining(",")); - String passFlagKey = optimizedRegen ? regenPassKey + "|" + passFlags : null; - Set generatedChunks = passFlagKey == null ? null : getRegenPassSet(REGEN_GENERATED_CHUNKS_BY_PASS, passFlagKey); - int visitedChunks = 0; - int clearedCount = 0; - int plannedSkipped = 0; - int componentSkipped = 0; - int componentForcedReset = 0; - int launchedLayers = 0; - int dedupSkipped = 0; - List> launchedTasks = useMulticore ? new ArrayList<>() : null; - - if (passFlagKey != null) { - touchRegenPass(passFlagKey); - } - if (traceRegen) { - Iris.info("Regen matter pass start: center=" + x + "," + z - + " passRadius=" + passRadius - + " rawPassRadius=" + rawPassRadius - + " flags=[" + passFlags + "]"); - } + int passRadius = pair.getB(); + List> launchedTasks = multicore ? new ArrayList<>() : null; for (int i = -passRadius; i <= passRadius; i++) { for (int j = -passRadius; j <= passRadius; j++) { int passX = x + i; int passZ = z + j; - visitedChunks++; long passKey = chunkKey(passX, passZ); - if (generatedChunks != null && !generatedChunks.add(passKey)) { - dedupSkipped++; - continue; - } MantleChunk chunk = writer.acquireChunk(passX, passZ); - if (forceRegen) { - if (clearedChunks.add(passKey)) { - chunk.deleteSlices(BlockData.class); - chunk.deleteSlices(String.class); - chunk.deleteSlices(TileWrapper.class); - chunk.flag(MantleFlag.PLANNED, false); - clearedCount++; - } - } - - if (!forceRegen && chunk.isFlagged(MantleFlag.PLANNED)) { - plannedSkipped++; + if (chunk.isFlagged(MantleFlag.PLANNED)) { continue; } @@ -136,9 +60,7 @@ public interface MatterGenerator { continue; } - boolean componentAlreadyGenerated = !forceRegen && chunk.isFlagged(component.getFlag()); - if (componentAlreadyGenerated) { - componentSkipped++; + if (chunk.isFlagged(component.getFlag())) { continue; } @@ -166,12 +88,6 @@ public interface MatterGenerator { } } - if (forceRegen && chunk.isFlagged(component.getFlag())) { - chunk.flag(component.getFlag(), false); - componentForcedReset++; - } - - launchedLayers++; int finalPassX = passX; int finalPassZ = passZ; MantleChunk finalChunk = chunk; @@ -179,7 +95,7 @@ public interface MatterGenerator { Runnable task = () -> finalChunk.raiseFlagUnchecked(finalComponent.getFlag(), () -> finalComponent.generateLayer(writer, finalPassX, finalPassZ, context)); - if (useMulticore) { + if (multicore) { launchedTasks.add(CompletableFuture.runAsync(task, DISPATCHER)); } else { task.run(); @@ -188,25 +104,11 @@ public interface MatterGenerator { } } - if (useMulticore) { + if (multicore) { for (CompletableFuture launchedTask : launchedTasks) { launchedTask.join(); } } - - if (traceRegen) { - Iris.info("Regen matter pass done: center=" + x + "," + z - + " passRadius=" + passRadius - + " rawPassRadius=" + rawPassRadius - + " visited=" + visitedChunks - + " cleared=" + clearedCount - + " dedupSkipped=" + dedupSkipped - + " plannedSkipped=" + plannedSkipped - + " componentSkipped=" + componentSkipped - + " componentForcedReset=" + componentForcedReset - + " launchedLayers=" + launchedLayers - + " flags=[" + passFlags + "]"); - } } for (int i = -getRealRadius(); i <= getRealRadius(); i++) { @@ -214,69 +116,16 @@ public interface MatterGenerator { int realX = x + i; int realZ = z + j; long realKey = chunkKey(realX, realZ); - if (plannedChunks != null && !plannedChunks.add(realKey)) { - continue; - } - if (partialChunks != null && partialChunks.contains(realKey)) { + if (partialChunks.contains(realKey)) { continue; } writer.acquireChunk(realX, realZ).flag(MantleFlag.PLANNED, true); } } } - - if (traceRegen) { - Iris.info("Regen matter done: center=" + x + "," + z - + " markedRealRadius=" + getRealRadius() - + " forceRegen=" + forceRegen); - } } private static long chunkKey(int x, int z) { return (((long) x) << 32) ^ (z & 0xffffffffL); } - - private static Set getRegenPassSet(ConcurrentHashMap> store, String passKey) { - return store.computeIfAbsent(passKey, key -> ConcurrentHashMap.newKeySet()); - } - - private static String resolveRegenPassKey(String threadName) { - String runtimeKey = RegenRuntime.getRunId(); - if (runtimeKey != null && !runtimeKey.isBlank()) { - return runtimeKey; - } - - if (!threadName.startsWith("Iris-Regen-")) { - return null; - } - - String suffix = threadName.substring("Iris-Regen-".length()); - int lastDash = suffix.lastIndexOf('-'); - if (lastDash <= 0) { - return suffix; - } - return suffix.substring(0, lastDash); - } - - private static void touchRegenPass(String passKey) { - long now = System.currentTimeMillis(); - REGEN_PASS_TOUCHED_MS.put(passKey, now); - if (REGEN_PASS_TOUCHED_MS.size() <= 64) { - return; - } - - Iterator> iterator = REGEN_PASS_TOUCHED_MS.entrySet().iterator(); - while (iterator.hasNext()) { - Map.Entry entry = iterator.next(); - if (now - entry.getValue() <= REGEN_PASS_CACHE_TTL_MS) { - continue; - } - - String key = entry.getKey(); - iterator.remove(); - REGEN_GENERATED_CHUNKS_BY_PASS.remove(key); - REGEN_CLEARED_CHUNKS_BY_PASS.remove(key); - REGEN_PLANNED_CHUNKS_BY_PASS.remove(key); - } - } } diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java index 17caf4a3e..f486800c9 100644 --- a/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java @@ -72,7 +72,6 @@ public class IrisCarveModifier extends EngineAssignedModifier { public void onModify(int x, int z, Hunk output, boolean multicore, ChunkContext context) { PrecisionStopwatch p = PrecisionStopwatch.start(); Mantle mantle = getEngine().getMantle().getMantle(); - MantleChunk mc = mantle.getChunk(x, z).use(); IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State(); Long2ObjectOpenHashMap caveBiomeCache = new Long2ObjectOpenHashMap<>(2048); CarveScratch scratch = SCRATCH.get(); @@ -101,6 +100,7 @@ public class IrisCarveModifier extends EngineAssignedModifier { } } + MantleChunk mc = mantle.getChunk(x, z).use(); try { PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start(); mc.iterate(MatterCavern.class, (xx, yy, zz, c) -> { diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisDepositModifier.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisDepositModifier.java index 8b06783e8..26df4ed46 100644 --- a/core/src/main/java/art/arcane/iris/engine/modifier/IrisDepositModifier.java +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisDepositModifier.java @@ -57,22 +57,25 @@ public class IrisDepositModifier extends EngineAssignedModifier { long seed = x * 341873128712L + z * 132897987541L; long mask = 0; MantleChunk chunk = getEngine().getMantle().getMantle().getChunk(x, z).use(); - for (IrisDepositGenerator k : getDimension().getDeposits()) { - long finalSeed = seed * ++mask; - burst.queue(() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context)); - } + try { + for (IrisDepositGenerator k : getDimension().getDeposits()) { + long finalSeed = seed * ++mask; + burst.queue(() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context)); + } - for (IrisDepositGenerator k : region.getDeposits()) { - long finalSeed = seed * ++mask; - burst.queue(() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context)); - } + for (IrisDepositGenerator k : region.getDeposits()) { + long finalSeed = seed * ++mask; + burst.queue(() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context)); + } - for (IrisDepositGenerator k : biome.getDeposits()) { - long finalSeed = seed * ++mask; - burst.queue(() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context)); + for (IrisDepositGenerator k : biome.getDeposits()) { + long finalSeed = seed * ++mask; + burst.queue(() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context)); + } + burst.complete(); + } finally { + chunk.release(); } - burst.complete(); - chunk.release(); } public void generate(IrisDepositGenerator k, MantleChunk chunk, Hunk data, RNG rng, int cx, int cz, boolean safe, ChunkContext context) { diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisImportedStructureControl.java b/core/src/main/java/art/arcane/iris/engine/object/IrisImportedStructureControl.java index 8c646506e..d3d32b2df 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisImportedStructureControl.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisImportedStructureControl.java @@ -53,17 +53,27 @@ public class IrisImportedStructureControl { @Desc("Vertical block offset applied only to UNDERGROUND vanilla structures (the UNDERGROUND_STRUCTURES and STRONGHOLDS generation steps: strongholds, trial chambers, mineshafts, ancient cities, etc.). Surface structures (villages, outposts, etc.) are never shifted. Use a negative value to push deep structures lower when your dimension's sea/terrain level differs from vanilla's (e.g. -64 if you lowered the fluid height to 0). 0 = no shift.") private int undergroundYShift = 0; + @Desc("When true (the default), ingested datapacks generate normally: a datapack that redefines a VANILLA structure key (e.g. an ancient-city datapack overriding 'minecraft:ancient_city') REPLACES the vanilla placement, structure definition, jigsaw pools and pieces exactly as the datapack intends, and the datapack's OWN new structures generate too. When false, datapack structures do NOT generate at all - Iris strips the vanilla-key overrides from the installed datapack copy so the original vanilla structures generate untouched, and holds every datapack-namespaced structure out of natural generation so it never appears over or beside the vanilla one. Datapacks stay installed and importable either way, so when false the only way to get a datapack structure is to run '/iris structure import' and place it manually from a biome/region/dimension 'structures' list. Resolved globally across every loaded pack: if ANY dimension sets this false, vanilla-key overrides are stripped for every installed datapack.") + private boolean datapackOverrides = true; + public boolean active() { return mode == VanillaStructureMode.ALL_ON || !enabled.isEmpty(); } public boolean shouldGenerate(String key) { + if (!datapackOverrides && isDatapackKey(key)) { + return false; + } if (mode == VanillaStructureMode.ALL_ON) { return !matches(disabled, key); } return matches(enabled, key); } + private static boolean isDatapackKey(String key) { + return key != null && key.contains(":") && !key.startsWith("minecraft:"); + } + private boolean matches(KList list, String key) { if (key == null) { return false; 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 e7c01c04d..f577f189e 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 @@ -22,7 +22,7 @@ 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.RegistryListStructure; import art.arcane.volmlib.util.collection.KList; import lombok.AllArgsConstructor; import lombok.Data; @@ -36,8 +36,8 @@ import lombok.experimental.Accessors; @Data public class IrisStructurePlacement { @ArrayType(type = String.class, min = 1) - @RegistryListResource(IrisStructure.class) - @Desc("Iris structures to place here. Use this for imported or hand-authored structures with full block control.") + @RegistryListStructure + @Desc("Structures to place here. Any vanilla, datapack, or imported Iris structure key (e.g. minecraft_village_plains, minecraft_stronghold). Jigsaw component pieces are not valid here.") private KList structures = new KList<>(); @Desc("How start positions are scattered.") diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandSettings.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListStructure.java similarity index 69% rename from core/src/main/java/art/arcane/iris/core/commands/CommandSettings.java rename to core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListStructure.java index 44f4a8623..052eec8da 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandSettings.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListStructure.java @@ -16,10 +16,16 @@ * along with this program. If not, see . */ -package art.arcane.iris.core.commands; +package art.arcane.iris.engine.object.annotations; -import art.arcane.iris.util.common.director.DirectorExecutor; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; -public class CommandSettings implements DirectorExecutor { +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({PARAMETER, TYPE, FIELD}) +public @interface RegistryListStructure { } 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 0e9e5cfa0..e2a93a498 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 @@ -37,14 +37,10 @@ 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; -import art.arcane.iris.util.project.matter.TileWrapper; import art.arcane.volmlib.util.collection.KList; import art.arcane.iris.util.project.hunk.Hunk; import art.arcane.iris.util.project.hunk.view.ChunkDataHunkHolder; import art.arcane.volmlib.util.io.ReactiveFolder; -import art.arcane.volmlib.util.mantle.flag.MantleFlag; -import art.arcane.volmlib.util.mantle.runtime.MantleChunk; -import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.scheduling.ChronoLatch; import art.arcane.iris.util.common.scheduling.J; import art.arcane.volmlib.util.scheduling.Looper; @@ -55,8 +51,6 @@ import lombok.Setter; import org.bukkit.*; import org.bukkit.block.Biome; import org.bukkit.block.data.BlockData; -import org.bukkit.entity.Entity; -import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; @@ -273,289 +267,6 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun }); } - @Override - public void injectChunkReplacement( - World world, - int x, - int z, - Executor syncExecutor, - ChunkReplacementOptions options, - ChunkReplacementListener listener - ) { - boolean acquired = false; - ChunkReplacementOptions effectiveOptions = Objects.requireNonNull(options, "options"); - ChunkReplacementListener effectiveListener = Objects.requireNonNull(listener, "listener"); - AtomicReference phaseRef = new AtomicReference<>("start"); - try { - setChunkReplacementPhase(phaseRef, effectiveListener, "acquire-load-lock", x, z); - long acquireStart = System.currentTimeMillis(); - while (!loadLock.tryAcquire(5, TimeUnit.SECONDS)) { - Iris.warn("Chunk replacement waiting for load lock at " + x + "," + z - + " for " + (System.currentTimeMillis() - acquireStart) + "ms."); - effectiveListener.onPhase(phaseRef.get(), x, z, System.currentTimeMillis()); - } - acquired = true; - long acquireWait = System.currentTimeMillis() - acquireStart; - if (acquireWait >= 5000L) { - Iris.warn("Chunk replacement waited " + acquireWait + "ms for load lock at " + x + "," + z + "."); - } - TerrainChunk tc = TerrainChunk.create(world); - this.world.bind(world); - - if (effectiveOptions.isFullMode()) { - setChunkReplacementPhase(phaseRef, effectiveListener, "reset-mantle", x, z); - resetMantleChunkForFullRegen(x, z); - } - - setChunkReplacementPhase(phaseRef, effectiveListener, "generate", x, z); - long generateStart = System.currentTimeMillis(); - boolean useMulticore = false; - AtomicBoolean generateDone = new AtomicBoolean(false); - AtomicLong generationWatchdogStart = new AtomicLong(System.currentTimeMillis()); - Thread generateThread = Thread.currentThread(); - J.a(() -> { - while (!generateDone.get()) { - if (!J.sleep(5000)) { - return; - } - if (generateDone.get()) { - return; - } - - Iris.warn("Chunk replacement still generating at " + x + "," + z - + " for " + (System.currentTimeMillis() - generationWatchdogStart.get()) + "ms" - + " thread=" + generateThread.getName() - + " state=" + generateThread.getState()); - effectiveListener.onPhase(phaseRef.get(), x, z, System.currentTimeMillis()); - } - }); - try { - getEngine().generate(x << 4, z << 4, tc, useMulticore); - } finally { - generateDone.set(true); - } - long generateTook = System.currentTimeMillis() - generateStart; - if (generateTook >= 5000L) { - Iris.warn("Chunk replacement terrain generation took " + generateTook + "ms at " + x + "," + z + "."); - } - - if (J.isFolia()) { - setChunkReplacementPhase(phaseRef, effectiveListener, "folia-run-region", x, z); - CountDownLatch latch = new CountDownLatch(1); - Throwable[] failure = new Throwable[1]; - long regionScheduleStart = System.currentTimeMillis(); - if (!J.runRegion(world, x, z, () -> { - try { - setChunkReplacementPhase(phaseRef, effectiveListener, "apply-terrain", x, z); - phaseUnsafeSet("folia-region-run", x, z); - Chunk c = world.getChunkAt(x, z); - Iris.tickets.addTicket(c); - try { - for (Entity ee : c.getEntities()) { - if (ee instanceof Player) { - continue; - } - - ee.remove(); - } - - for (int i = getEngine().getHeight() >> 4; i >= 0; i--) { - int finalI = i << 4; - for (int xx = 0; xx < 16; xx++) { - for (int yy = 0; yy < 16; yy++) { - for (int zz = 0; zz < 16; zz++) { - if (yy + finalI >= engine.getHeight() || yy + finalI < 0) { - continue; - } - int y = yy + finalI + world.getMinHeight(); - c.getBlock(xx, y, zz).setBlockData(tc.getBlockData(xx, y, zz), false); - } - } - } - } - - if (effectiveOptions.isFullMode()) { - setChunkReplacementPhase(phaseRef, effectiveListener, "overlay", x, z); - OverlayMetrics overlayMetrics = applyMantleOverlay(c, world, x, z); - effectiveListener.onOverlay(x, z, overlayMetrics.appliedBlocks(), overlayMetrics.objectKeys(), System.currentTimeMillis()); - } - - setChunkReplacementPhase(phaseRef, effectiveListener, "chunk-load-callback", x, z); - engine.getWorldManager().onChunkLoad(c, true); - world.refreshChunk(c.getX(), c.getZ()); - } finally { - Iris.tickets.removeTicket(c); - } - } catch (Throwable e) { - failure[0] = e; - } finally { - latch.countDown(); - } - })) { - throw new IllegalStateException("Failed to schedule region task for chunk replacement at " + x + "," + z); - } - long regionScheduleTook = System.currentTimeMillis() - regionScheduleStart; - if (regionScheduleTook >= 1000L) { - Iris.verbose("Chunk replacement region task scheduling took " + regionScheduleTook + "ms at " + x + "," + z + "."); - } - - long regionWaitStart = System.currentTimeMillis(); - while (!latch.await(5, TimeUnit.SECONDS)) { - Iris.warn("Chunk replacement waiting on region task at " + x + "," + z - + " for " + (System.currentTimeMillis() - regionWaitStart) + "ms."); - effectiveListener.onPhase(phaseRef.get(), x, z, System.currentTimeMillis()); - } - long regionWaitTook = System.currentTimeMillis() - regionWaitStart; - if (regionWaitTook >= 5000L) { - Iris.warn("Chunk replacement region task completed after " + regionWaitTook + "ms at " + x + "," + z + "."); - } - if (failure[0] != null) { - effectiveListener.onFailurePhase(phaseRef.get(), x, z, failure[0], System.currentTimeMillis()); - throw failure[0]; - } - } else { - setChunkReplacementPhase(phaseRef, effectiveListener, "paperlib-async-load", x, z); - long loadChunkStart = System.currentTimeMillis(); - Chunk c = PaperLib.getChunkAtAsync(world, x, z).get(); - long loadChunkTook = System.currentTimeMillis() - loadChunkStart; - if (loadChunkTook >= 5000L) { - Iris.warn("Chunk replacement chunk load took " + loadChunkTook + "ms at " + x + "," + z + "."); - } - - setChunkReplacementPhase(phaseRef, effectiveListener, "apply-terrain", x, z); - Iris.tickets.addTicket(c); - try { - CompletableFuture.runAsync(() -> { - for (Entity ee : c.getEntities()) { - if (ee instanceof Player) { - continue; - } - - ee.remove(); - } - }, syncExecutor).get(); - - KList> futures = new KList<>(1 + getEngine().getHeight() >> 4); - for (int i = getEngine().getHeight() >> 4; i >= 0; i--) { - int finalI = i << 4; - futures.add(CompletableFuture.runAsync(() -> { - for (int xx = 0; xx < 16; xx++) { - for (int yy = 0; yy < 16; yy++) { - for (int zz = 0; zz < 16; zz++) { - if (yy + finalI >= engine.getHeight() || yy + finalI < 0) { - continue; - } - int y = yy + finalI + world.getMinHeight(); - c.getBlock(xx, y, zz).setBlockData(tc.getBlockData(xx, y, zz), false); - } - } - } - }, syncExecutor)); - } - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get(); - if (effectiveOptions.isFullMode()) { - CompletableFuture.runAsync(() -> { - setChunkReplacementPhase(phaseRef, effectiveListener, "overlay", x, z); - OverlayMetrics overlayMetrics = applyMantleOverlay(c, world, x, z); - effectiveListener.onOverlay(x, z, overlayMetrics.appliedBlocks(), overlayMetrics.objectKeys(), System.currentTimeMillis()); - }, syncExecutor).get(); - } - CompletableFuture.runAsync(() -> { - setChunkReplacementPhase(phaseRef, effectiveListener, "chunk-load-callback", x, z); - engine.getWorldManager().onChunkLoad(c, true); - world.refreshChunk(c.getX(), c.getZ()); - }, syncExecutor).get(); - } finally { - Iris.tickets.removeTicket(c); - } - } - - Iris.debug("Regenerated " + x + " " + z); - } catch (Throwable e) { - effectiveListener.onFailurePhase(phaseRef.get(), x, z, e, System.currentTimeMillis()); - Iris.error("======================================"); - Iris.error("Chunk replacement failed at phase=" + phaseRef.get() + " chunk=" + x + "," + z); - e.printStackTrace(); - Iris.reportErrorChunk(x, z, e, "CHUNK"); - Iris.error("======================================"); - throw new IllegalStateException("Chunk replacement failed at phase=" + phaseRef.get() + " chunk=" + x + "," + z, e); - } finally { - if (acquired) { - loadLock.release(); - } - } - } - - private static void phaseUnsafeSet(String phase, int x, int z) { - Iris.verbose("Chunk replacement phase=" + phase + " chunk=" + x + "," + z); - } - - private static void setChunkReplacementPhase( - AtomicReference phaseRef, - ChunkReplacementListener listener, - String phase, - int x, - int z - ) { - phaseRef.set(phase); - listener.onPhase(phase, x, z, System.currentTimeMillis()); - } - - private void resetMantleChunkForFullRegen(int chunkX, int chunkZ) { - MantleChunk mantleChunk = getEngine().getMantle().getMantle().getChunk(chunkX, chunkZ).use(); - try { - mantleChunk.deleteSlices(BlockData.class); - mantleChunk.deleteSlices(String.class); - mantleChunk.deleteSlices(TileWrapper.class); - mantleChunk.flag(MantleFlag.PLANNED, false); - mantleChunk.flag(MantleFlag.OBJECT, false); - mantleChunk.flag(MantleFlag.REAL, false); - } finally { - mantleChunk.release(); - } - } - - private OverlayMetrics applyMantleOverlay(Chunk chunk, World world, int chunkX, int chunkZ) { - int minWorldY = world.getMinHeight(); - int maxWorldY = world.getMaxHeight(); - AtomicInteger appliedBlocks = new AtomicInteger(); - AtomicInteger objectKeys = new AtomicInteger(); - MantleChunk mantleChunk = getEngine().getMantle().getMantle().getChunk(chunkX, chunkZ).use(); - try { - mantleChunk.iterate(String.class, (x, y, z, value) -> { - if (value != null && !value.isEmpty() && value.indexOf('@') > 0) { - objectKeys.incrementAndGet(); - } - }); - mantleChunk.iterate(BlockData.class, (x, y, z, blockData) -> { - if (!shouldApplyMantleOverlayBlock(blockData)) { - return; - } - int worldY = y + minWorldY; - if (worldY < minWorldY || worldY >= maxWorldY) { - return; - } - chunk.getBlock(x & 15, worldY, z & 15).setBlockData(blockData, false); - appliedBlocks.incrementAndGet(); - }); - } finally { - mantleChunk.release(); - } - return new OverlayMetrics(appliedBlocks.get(), objectKeys.get()); - } - - static boolean shouldApplyMantleOverlayBlock(BlockData blockData) { - if (blockData == null) { - return false; - } - - Material material = blockData.getMaterial(); - return material != null && material != Material.AIR && material != Material.CAVE_AIR && material != Material.VOID_AIR; - } - - private record OverlayMetrics(int appliedBlocks, int objectKeys) { - } - private Engine getEngine(WorldInfo world) { if (setup.get()) { return getEngine(); diff --git a/core/src/main/java/art/arcane/iris/engine/platform/ChunkReplacementListener.java b/core/src/main/java/art/arcane/iris/engine/platform/ChunkReplacementListener.java deleted file mode 100644 index 9d251943e..000000000 --- a/core/src/main/java/art/arcane/iris/engine/platform/ChunkReplacementListener.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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.platform; - -public interface ChunkReplacementListener { - ChunkReplacementListener NO_OP = new ChunkReplacementListener() { - }; - - default void onPhase(String phase, int chunkX, int chunkZ, long timestampMs) { - } - - default void onOverlay(int chunkX, int chunkZ, int appliedBlocks, int objectKeys, long timestampMs) { - } - - default void onFailurePhase(String phase, int chunkX, int chunkZ, Throwable error, long timestampMs) { - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/platform/ChunkReplacementOptions.java b/core/src/main/java/art/arcane/iris/engine/platform/ChunkReplacementOptions.java deleted file mode 100644 index a23742f4b..000000000 --- a/core/src/main/java/art/arcane/iris/engine/platform/ChunkReplacementOptions.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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.platform; - -public final class ChunkReplacementOptions { - private final String runId; - private final boolean fullMode; - private final boolean diagnostics; - - private ChunkReplacementOptions(String runId, boolean fullMode, boolean diagnostics) { - this.runId = runId == null ? "unknown" : runId; - this.fullMode = fullMode; - this.diagnostics = diagnostics; - } - - public static ChunkReplacementOptions terrain(String runId, boolean diagnostics) { - return new ChunkReplacementOptions(runId, false, diagnostics); - } - - public static ChunkReplacementOptions full(String runId, boolean diagnostics) { - return new ChunkReplacementOptions(runId, true, diagnostics); - } - - public String runId() { - return runId; - } - - public boolean isFullMode() { - return fullMode; - } - - public boolean diagnostics() { - return diagnostics; - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/platform/PlatformChunkGenerator.java b/core/src/main/java/art/arcane/iris/engine/platform/PlatformChunkGenerator.java index 74aaa6340..c330aedbc 100644 --- a/core/src/main/java/art/arcane/iris/engine/platform/PlatformChunkGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/platform/PlatformChunkGenerator.java @@ -29,7 +29,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; public interface PlatformChunkGenerator extends Hotloadable, DataProvider { @Nullable @@ -43,15 +42,6 @@ public interface PlatformChunkGenerator extends Hotloadable, DataProvider { @NotNull EngineTarget getTarget(); - void injectChunkReplacement( - World world, - int x, - int z, - Executor syncExecutor, - ChunkReplacementOptions options, - ChunkReplacementListener listener - ); - void close(); default CompletableFuture closeAsync() { diff --git a/core/src/main/java/art/arcane/iris/util/common/director/DirectorHelp.java b/core/src/main/java/art/arcane/iris/util/common/director/DirectorHelp.java deleted file mode 100644 index 090be36b1..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/director/DirectorHelp.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * 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.util.common.director; - -import art.arcane.iris.util.common.format.C; -import art.arcane.iris.util.common.plugin.VolmitSender; -import art.arcane.volmlib.util.director.annotations.Director; -import art.arcane.volmlib.util.director.annotations.Param; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Parameter; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; - -public final class DirectorHelp { - private DirectorHelp() { - } - - public static void print(VolmitSender sender, Class commandRoot) { - Director rootAnnotation = commandRoot.getAnnotation(Director.class); - String rootName = rootAnnotation == null || rootAnnotation.name().isEmpty() - ? lowercaseDefault(commandRoot.getSimpleName()) - : rootAnnotation.name(); - String rootDesc = rootAnnotation == null ? "" : rootAnnotation.description(); - - sender.sendMessage(C.IRIS + "/" + rootName + C.GRAY + " — " + rootDesc); - - List methods = new ArrayList<>(); - for (Method m : commandRoot.getDeclaredMethods()) { - if (m.isAnnotationPresent(Director.class)) { - methods.add(m); - } - } - methods.sort(Comparator.comparing(m -> methodName(m))); - - for (Method m : methods) { - Director d = m.getAnnotation(Director.class); - String name = methodName(m); - String aliases = formatAliases(d.aliases()); - sender.sendMessage(C.WHITE + " " + name + aliases + C.GRAY + " — " + d.description()); - for (Parameter p : m.getParameters()) { - Param pa = p.getAnnotation(Param.class); - if (pa == null) continue; - String key = pa.name().isEmpty() ? p.getName() : pa.name(); - String type = simpleTypeName(p.getType()); - String def = pa.defaultValue().isEmpty() ? "" : C.GRAY + " (default: " + pa.defaultValue() + ")"; - String pAliases = formatAliases(pa.aliases()); - sender.sendMessage(C.GRAY + " " + C.AQUA + key + "=" + C.YELLOW + "<" + type + ">" - + C.GRAY + pAliases + C.GRAY + " — " + pa.description() + def); - } - } - - List subGroups = new ArrayList<>(); - for (Field f : commandRoot.getDeclaredFields()) { - if (f.getType().isAnnotationPresent(Director.class)) { - subGroups.add(f); - } - } - if (!subGroups.isEmpty()) { - sender.sendMessage(C.IRIS + " Subcommand groups:"); - for (Field f : subGroups) { - Director sub = f.getType().getAnnotation(Director.class); - String subName = sub.name().isEmpty() ? lowercaseDefault(f.getType().getSimpleName()) : sub.name(); - sender.sendMessage(C.WHITE + " /" + rootName + " " + subName - + C.GRAY + " — " + sub.description() + C.GRAY + " (try: /" + rootName + " " + subName + " help)"); - } - } - } - - private static String methodName(Method m) { - Director d = m.getAnnotation(Director.class); - if (d != null && !d.name().isEmpty()) return d.name(); - return m.getName(); - } - - private static String formatAliases(String[] aliases) { - if (aliases == null || aliases.length == 0) return ""; - List valid = new ArrayList<>(); - for (String a : aliases) { - if (a != null && !a.isEmpty()) valid.add(a); - } - if (valid.isEmpty()) return ""; - return C.GRAY + " [" + String.join(", ", valid) + "]"; - } - - private static String simpleTypeName(Class type) { - if (type.isEnum()) { - Object[] constants = type.getEnumConstants(); - List names = new ArrayList<>(); - for (Object c : constants) names.add(((Enum) c).name()); - return String.join("|", names); - } - return type.getSimpleName(); - } - - private static String lowercaseDefault(String simpleName) { - String s = simpleName.startsWith("Command") ? simpleName.substring("Command".length()) : simpleName; - return s.isEmpty() ? simpleName.toLowerCase() : s.toLowerCase(); - } -} diff --git a/core/src/main/java/art/arcane/iris/util/common/misc/RegenRuntime.java b/core/src/main/java/art/arcane/iris/util/common/misc/RegenRuntime.java deleted file mode 100644 index 0b17e9b17..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/misc/RegenRuntime.java +++ /dev/null @@ -1,20 +0,0 @@ -package art.arcane.iris.util.common.misc; - -public final class RegenRuntime { - private static final ThreadLocal RUN_ID = new ThreadLocal<>(); - - private RegenRuntime() { - } - - public static void setRunId(String runId) { - RUN_ID.set(runId); - } - - public static String getRunId() { - return RUN_ID.get(); - } - - public static void clear() { - RUN_ID.remove(); - } -} diff --git a/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleRuntimeLevelStemTest.java b/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleRuntimeLevelStemTest.java index 2efa9bd0a..2d50c9a6e 100644 --- a/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleRuntimeLevelStemTest.java +++ b/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleRuntimeLevelStemTest.java @@ -4,8 +4,6 @@ import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.nms.INMSBinding; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.EngineTarget; -import art.arcane.iris.engine.platform.ChunkReplacementListener; -import art.arcane.iris.engine.platform.ChunkReplacementOptions; import art.arcane.iris.engine.platform.PlatformChunkGenerator; import org.bukkit.Location; import org.bukkit.World; @@ -15,7 +13,6 @@ import org.junit.Test; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicReference; import static org.junit.Assert.assertEquals; @@ -108,10 +105,6 @@ public class WorldLifecycleRuntimeLevelStemTest { return null; } - @Override - public void injectChunkReplacement(World world, int x, int z, Executor syncExecutor, ChunkReplacementOptions options, ChunkReplacementListener listener) { - } - @Override public void close() { } diff --git a/core/src/test/java/art/arcane/iris/core/structure/BulkStructureImporterTemplateNameTest.java b/core/src/test/java/art/arcane/iris/core/structure/BulkStructureImporterTemplateNameTest.java new file mode 100644 index 000000000..7ffadd9a9 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/structure/BulkStructureImporterTemplateNameTest.java @@ -0,0 +1,54 @@ +/* + * 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 org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class BulkStructureImporterTemplateNameTest { + @Test + public void minecraftKeysDropTheNamespacePrefix() { + assertEquals("village/plains", BulkStructureImporter.templateNameFor("minecraft:village/plains")); + assertEquals("igloo/top", BulkStructureImporter.templateNameFor("minecraft:igloo/top")); + } + + @Test + public void datapackKeysKeepNamespaceAsDirectory() { + assertEquals("nova_structures/temple/large", BulkStructureImporter.templateNameFor("nova_structures:temple/large")); + assertEquals("aquaculture/treasure", BulkStructureImporter.templateNameFor("aquaculture:treasure")); + } + + @Test + public void keyWithoutNamespaceDefaultsToMinecraft() { + assertEquals("village", BulkStructureImporter.templateNameFor("village")); + } + + @Test + public void pathIsLowercasedAndSanitized() { + assertEquals("nova/foo_bar", BulkStructureImporter.templateNameFor("nova:Foo Bar")); + assertEquals("nova/weird_name", BulkStructureImporter.templateNameFor("nova:weird@name")); + } + + @Test + public void duplicateSlashesAreCollapsedAndLeadingSlashStripped() { + assertEquals("nova/a/b", BulkStructureImporter.templateNameFor("nova:a//b")); + assertEquals("nova/leading", BulkStructureImporter.templateNameFor("nova:/leading")); + } +} diff --git a/core/src/test/java/art/arcane/iris/core/structure/StructureImporterModeTest.java b/core/src/test/java/art/arcane/iris/core/structure/StructureImporterModeTest.java new file mode 100644 index 000000000..724c7c53b --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/structure/StructureImporterModeTest.java @@ -0,0 +1,71 @@ +/* + * 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 org.bukkit.NamespacedKey; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class StructureImporterModeTest { + @Test + public void parseModeDefaultsToOverwriteForNull() { + assertEquals(StructureImporter.Mode.OVERWRITE, StructureImporter.parseMode(null)); + } + + @Test + public void parseModeDefaultsToOverwriteForUnknownAndEmpty() { + assertEquals(StructureImporter.Mode.OVERWRITE, StructureImporter.parseMode("")); + assertEquals(StructureImporter.Mode.OVERWRITE, StructureImporter.parseMode("garbage")); + assertEquals(StructureImporter.Mode.OVERWRITE, StructureImporter.parseMode("overwrite")); + } + + @Test + public void parseModeRecognizesAllAddOnlyAliases() { + assertEquals(StructureImporter.Mode.ADD_ONLY, StructureImporter.parseMode("add")); + assertEquals(StructureImporter.Mode.ADD_ONLY, StructureImporter.parseMode("addonly")); + assertEquals(StructureImporter.Mode.ADD_ONLY, StructureImporter.parseMode("add_only")); + assertEquals(StructureImporter.Mode.ADD_ONLY, StructureImporter.parseMode("add-only")); + } + + @Test + public void parseModeIsCaseInsensitive() { + assertEquals(StructureImporter.Mode.ADD_ONLY, StructureImporter.parseMode("ADD")); + assertEquals(StructureImporter.Mode.MERGE, StructureImporter.parseMode("MERGE")); + assertEquals(StructureImporter.Mode.MERGE, StructureImporter.parseMode("merge")); + } + + @Test + public void deriveNameFromStringLowercasesAndNormalizesSeparators() { + assertEquals("minecraft_village_plains", StructureImporter.deriveName("minecraft:village/plains")); + assertEquals("nova_structures_temple", StructureImporter.deriveName("Nova_Structures:Temple")); + assertEquals("minecraft_ancient_city", StructureImporter.deriveName("minecraft:ancient_city")); + } + + @Test + public void deriveNameFromNamespacedKeyJoinsNamespaceAndPath() { + assertEquals("minecraft_village", StructureImporter.deriveName(NamespacedKey.minecraft("village"))); + } + + @Test + public void deriveNameFromNamespacedKeyNormalizesSlashesInPath() { + NamespacedKey key = NamespacedKey.fromString("nova_structures:temple/large"); + assertEquals("nova_structures_temple_large", StructureImporter.deriveName(key)); + } +} diff --git a/core/src/test/java/art/arcane/iris/core/structure/StructureSchemaKeysTest.java b/core/src/test/java/art/arcane/iris/core/structure/StructureSchemaKeysTest.java new file mode 100644 index 000000000..556c28000 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/structure/StructureSchemaKeysTest.java @@ -0,0 +1,157 @@ +/* + * 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.volmlib.util.collection.KList; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class StructureSchemaKeysTest { + @Test + public void vanillaKeysAreNormalizedToIrisLoadKeyForm() { + KList result = StructureSchemaKeys.collect( + Arrays.asList("minecraft:village_plains", "minecraft:stronghold", "minecraft:ancient_city"), + Collections.emptyList(), + Collections.emptyList()); + + assertTrue(result.contains("minecraft_village_plains")); + assertTrue(result.contains("minecraft_stronghold")); + assertTrue(result.contains("minecraft_ancient_city")); + } + + @Test + public void strongholdIsAlwaysOfferedEvenWithoutAnImportedFile() { + KList result = StructureSchemaKeys.collect( + Arrays.asList("minecraft:stronghold", "minecraft:fortress", "minecraft:monument"), + Collections.emptyList(), + Collections.emptyList()); + + assertTrue(result.contains("minecraft_stronghold")); + assertTrue(result.contains("minecraft_fortress")); + assertTrue(result.contains("minecraft_monument")); + } + + @Test + public void datapackStructuresAreNormalizedAndIncluded() { + KList result = StructureSchemaKeys.collect( + Arrays.asList("nova_structures:temple", "Nova_Structures:Big/Castle"), + Collections.emptyList(), + Collections.emptyList()); + + assertTrue(result.contains("nova_structures_temple")); + assertTrue(result.contains("nova_structures_big_castle")); + } + + @Test + public void jigsawComponentPiecesAreExcluded() { + List pieces = Arrays.asList( + "village/taiga/houses/taiga_small_house_5", + "bastion/units/rampart_plates/plate_0", + "village/common/animals/cat_black", + "trial_chambers/corridor/straight_1", + "end_city/third_floor_1", + "igloo/middle"); + + KList result = StructureSchemaKeys.collect( + Arrays.asList("minecraft:village_taiga", "minecraft:bastion_remnant"), + pieces, + pieces); + + for (String piece : pieces) { + assertFalse("component piece must not be a valid structure key: " + piece, result.contains(piece)); + } + assertTrue(result.contains("minecraft_village_taiga")); + assertTrue(result.contains("minecraft_bastion_remnant")); + } + + @Test + public void importedCustomStructuresAreKept() { + KList result = StructureSchemaKeys.collect( + Collections.emptyList(), + Arrays.asList("mypack_castle", "custom/cool_tower"), + Collections.emptyList()); + + assertTrue(result.contains("mypack_castle")); + assertTrue(result.contains("custom/cool_tower")); + } + + @Test + public void importedStructureThatCollidesWithAPieceIsDropped() { + KList result = StructureSchemaKeys.collect( + Collections.emptyList(), + Arrays.asList("village/taiga/houses/taiga_small_house_5", "minecraft_village_plains"), + Collections.singletonList("village/taiga/houses/taiga_small_house_5")); + + assertFalse(result.contains("village/taiga/houses/taiga_small_house_5")); + assertTrue(result.contains("minecraft_village_plains")); + } + + @Test + public void vanillaAndImportedEquivalentsDeduplicate() { + KList result = StructureSchemaKeys.collect( + Collections.singletonList("minecraft:ancient_city"), + Collections.singletonList("minecraft_ancient_city"), + Collections.emptyList()); + + int occurrences = 0; + for (String key : result) { + if (key.equals("minecraft_ancient_city")) { + occurrences++; + } + } + assertEquals(1, occurrences); + } + + @Test + public void resultIsSorted() { + KList result = StructureSchemaKeys.collect( + Arrays.asList("minecraft:zombie_village", "minecraft:ancient_city", "minecraft:monument"), + Collections.emptyList(), + Collections.emptyList()); + + KList sorted = new KList<>(result); + Collections.sort(sorted); + assertEquals(sorted, result); + } + + @Test + public void nullCollectionsAreHandled() { + KList result = StructureSchemaKeys.collect(null, null, null); + assertTrue(result.isEmpty()); + } + + @Test + public void blankAndNullEntriesAreIgnored() { + KList result = StructureSchemaKeys.collect( + Arrays.asList("minecraft:village_plains", "", null, " "), + Arrays.asList("custom_one", "", null), + Arrays.asList("", null)); + + assertTrue(result.contains("minecraft_village_plains")); + assertTrue(result.contains("custom_one")); + assertEquals(2, result.size()); + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/framework/IrisStructureLocatorContractTest.java b/core/src/test/java/art/arcane/iris/engine/framework/IrisStructureLocatorContractTest.java new file mode 100644 index 000000000..99af5e6ca --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/framework/IrisStructureLocatorContractTest.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.framework; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +public class IrisStructureLocatorContractTest { + @Test + public void placedKeysIsEmptyForNullEngine() { + assertTrue(IrisStructureLocator.placedKeys(null).isEmpty()); + } + + @Test + public void isPlacedIsFalseForNullEngine() { + assertFalse(IrisStructureLocator.isPlaced(null, "minecraft:ancient_city")); + } + + @Test + public void isPlacedIsFalseForNullOrEmptyKey() { + Engine engine = mock(Engine.class); + assertFalse(IrisStructureLocator.isPlaced(engine, null)); + assertFalse(IrisStructureLocator.isPlaced(engine, "")); + } + + @Test + public void suppressesVanillaIsFalseForNullEngine() { + assertFalse(IrisStructureLocator.suppressesVanilla(null, "minecraft:ancient_city")); + } + + @Test + public void suppressesVanillaIsFalseForNullOrEmptyKey() { + Engine engine = mock(Engine.class); + assertFalse(IrisStructureLocator.suppressesVanilla(engine, null)); + assertFalse(IrisStructureLocator.suppressesVanilla(engine, "")); + } + + @Test + public void startsInChunkIsFalseForNullEngine() { + assertFalse(IrisStructureLocator.startsInChunk(null, "minecraft:ancient_city", 0, 0)); + } + + @Test + public void locateReturnsNullForNullEngine() { + assertNull(IrisStructureLocator.locate(null, "minecraft:village_taiga", 0, 0, 100)); + } + + @Test + public void locateReturnsNullForNullOrEmptyKey() { + Engine engine = mock(Engine.class); + assertNull(IrisStructureLocator.locate(engine, null, 0, 0, 100)); + assertNull(IrisStructureLocator.locate(engine, "", 0, 0, 100)); + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/framework/StructureReachabilityContractTest.java b/core/src/test/java/art/arcane/iris/engine/framework/StructureReachabilityContractTest.java new file mode 100644 index 000000000..11cd9e403 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/framework/StructureReachabilityContractTest.java @@ -0,0 +1,79 @@ +/* + * 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.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +public class StructureReachabilityContractTest { + @Test + public void reachableKeysIsEmptyForNullEngine() { + assertTrue(StructureReachability.reachableKeys(null).isEmpty()); + } + + @Test + public void reachableKeysIsEmptyWhenEngineHasNoData() { + Engine engine = mock(Engine.class); + assertTrue(StructureReachability.reachableKeys(engine).isEmpty()); + } + + @Test + public void isReachableIsFalseForNullEngine() { + assertFalse(StructureReachability.isReachable(null, "minecraft:village_taiga")); + } + + @Test + public void isReachableIsFalseForNullOrEmptyKey() { + Engine engine = mock(Engine.class); + assertFalse(StructureReachability.isReachable(engine, null)); + assertFalse(StructureReachability.isReachable(engine, "")); + } + + @Test + public void isReachableIsFalseWhenNothingIsReachable() { + Engine engine = mock(Engine.class); + assertFalse(StructureReachability.isReachable(engine, "minecraft:village_taiga")); + } + + @Test + public void missingBiomeKeysIsEmptyForNullEngine() { + assertTrue(StructureReachability.missingBiomeKeys(null, "minecraft:village_taiga").isEmpty()); + } + + @Test + public void missingBiomeKeysIsEmptyForNullKey() { + Engine engine = mock(Engine.class); + assertTrue(StructureReachability.missingBiomeKeys(engine, null).isEmpty()); + } + + @Test + public void missingBiomeKeysIsEmptyWhenWorldUnavailable() { + Engine engine = mock(Engine.class); + assertTrue(StructureReachability.missingBiomeKeys(engine, "minecraft:village_taiga").isEmpty()); + } + + @Test + public void invalidateIsNullSafe() { + StructureReachability.invalidate(null); + StructureReachability.invalidate(mock(Engine.class)); + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisImportedStructureControlTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisImportedStructureControlTest.java new file mode 100644 index 000000000..6913e96b5 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisImportedStructureControlTest.java @@ -0,0 +1,180 @@ +/* + * 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.volmlib.util.collection.KList; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class IrisImportedStructureControlTest { + private static KList keys(String... values) { + KList list = new KList<>(); + for (String value : values) { + list.add(value); + } + return list; + } + + @Test + public void defaultModeGeneratesEveryVanillaStructure() { + IrisImportedStructureControl control = new IrisImportedStructureControl(); + assertTrue(control.shouldGenerate("minecraft:village_plains")); + assertTrue(control.shouldGenerate("minecraft:stronghold")); + assertTrue(control.shouldGenerate("minecraft:ocean_monument")); + } + + @Test + public void defaultModeGeneratesDatapackStructures() { + IrisImportedStructureControl control = new IrisImportedStructureControl(); + assertTrue(control.isDatapackOverrides()); + assertTrue(control.shouldGenerate("nova_structures:desert_temple")); + } + + @Test + public void disabledBlacklistMatchesExactKey() { + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setDisabled(keys("minecraft:stronghold")); + assertFalse(control.shouldGenerate("minecraft:stronghold")); + assertTrue(control.shouldGenerate("minecraft:village_plains")); + } + + @Test + public void disabledBlacklistMatchesNamespacePrefix() { + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setDisabled(keys("minecraft:village")); + assertFalse(control.shouldGenerate("minecraft:village_plains")); + assertFalse(control.shouldGenerate("minecraft:village_taiga")); + assertFalse(control.shouldGenerate("minecraft:village_snowy")); + assertTrue(control.shouldGenerate("minecraft:stronghold")); + } + + @Test + public void allOffWhitelistMatchesExactKey() { + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setMode(VanillaStructureMode.ALL_OFF) + .setEnabled(keys("minecraft:village_plains")); + assertTrue(control.shouldGenerate("minecraft:village_plains")); + assertFalse(control.shouldGenerate("minecraft:village_taiga")); + assertFalse(control.shouldGenerate("minecraft:stronghold")); + } + + @Test + public void allOffWhitelistMatchesNamespacePrefix() { + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setMode(VanillaStructureMode.ALL_OFF) + .setEnabled(keys("minecraft:village")); + assertTrue(control.shouldGenerate("minecraft:village_taiga")); + assertTrue(control.shouldGenerate("minecraft:village_desert")); + assertFalse(control.shouldGenerate("minecraft:stronghold")); + } + + @Test + public void customModeBehavesLikeWhitelist() { + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setMode(VanillaStructureMode.CUSTOM) + .setEnabled(keys("minecraft:village_plains")); + assertTrue(control.shouldGenerate("minecraft:village_plains")); + assertFalse(control.shouldGenerate("minecraft:stronghold")); + } + + @Test + public void datapackOverridesFalseBlocksDatapackKeysWhileAllOn() { + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setDatapackOverrides(false); + assertFalse(control.shouldGenerate("nova_structures:desert_temple")); + assertFalse(control.shouldGenerate("aquaculture:treasure_vault")); + assertTrue(control.shouldGenerate("minecraft:village_plains")); + } + + @Test + public void datapackOverridesFalseBlocksDatapackKeyEvenWhenWhitelisted() { + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setMode(VanillaStructureMode.ALL_OFF) + .setEnabled(keys("nova_structures:desert_temple", "minecraft:village_plains")) + .setDatapackOverrides(false); + assertFalse(control.shouldGenerate("nova_structures:desert_temple")); + assertTrue(control.shouldGenerate("minecraft:village_plains")); + } + + @Test + public void datapackOverridesTrueAllowsDatapackKeysWhileAllOn() { + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setDatapackOverrides(true); + assertTrue(control.shouldGenerate("nova_structures:desert_temple")); + } + + @Test + public void keyWithoutNamespaceIsNotTreatedAsDatapack() { + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setDatapackOverrides(false); + assertTrue(control.shouldGenerate("village_plains")); + } + + @Test + public void minecraftKeyIsNeverTreatedAsDatapack() { + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setDatapackOverrides(false); + assertTrue(control.shouldGenerate("minecraft:ancient_city")); + } + + @Test + public void activeTrueWhenAllOn() { + assertTrue(new IrisImportedStructureControl().active()); + } + + @Test + public void activeFalseWhenAllOffWithoutWhitelist() { + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setMode(VanillaStructureMode.ALL_OFF); + assertFalse(control.active()); + } + + @Test + public void activeTrueWhenAllOffWithWhitelist() { + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setMode(VanillaStructureMode.ALL_OFF) + .setEnabled(keys("minecraft:village_plains")); + assertTrue(control.active()); + } + + @Test + public void activeFalseWhenCustomWithoutWhitelist() { + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setMode(VanillaStructureMode.CUSTOM); + assertFalse(control.active()); + } + + @Test + public void emptyOrNullListEntriesNeverMatchEveryKey() { + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setDisabled(keys("", null)); + assertTrue(control.shouldGenerate("minecraft:village_plains")); + assertTrue(control.shouldGenerate("minecraft:stronghold")); + } + + @Test + public void nullKeyIsNullSafeAndNotWhitelisted() { + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setMode(VanillaStructureMode.ALL_OFF) + .setEnabled(keys("minecraft:village_plains")); + assertFalse(control.shouldGenerate(null)); + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/platform/BukkitChunkGeneratorOverlayPolicyTest.java b/core/src/test/java/art/arcane/iris/engine/platform/BukkitChunkGeneratorOverlayPolicyTest.java deleted file mode 100644 index 8ea0228bc..000000000 --- a/core/src/test/java/art/arcane/iris/engine/platform/BukkitChunkGeneratorOverlayPolicyTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package art.arcane.iris.engine.platform; - -import org.bukkit.Material; -import org.bukkit.block.data.BlockData; -import org.junit.Test; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; - -public class BukkitChunkGeneratorOverlayPolicyTest { - @Test - public void skipsAirOverlayBlocks() { - BlockData air = mock(BlockData.class); - doReturn(Material.AIR).when(air).getMaterial(); - - assertFalse(BukkitChunkGenerator.shouldApplyMantleOverlayBlock(air)); - } - - @Test - public void skipsCaveAirOverlayBlocks() { - BlockData air = mock(BlockData.class); - doReturn(Material.CAVE_AIR).when(air).getMaterial(); - - assertFalse(BukkitChunkGenerator.shouldApplyMantleOverlayBlock(air)); - } - - @Test - public void skipsVoidAirOverlayBlocks() { - BlockData air = mock(BlockData.class); - doReturn(Material.VOID_AIR).when(air).getMaterial(); - - assertFalse(BukkitChunkGenerator.shouldApplyMantleOverlayBlock(air)); - } - - @Test - public void appliesSolidOverlayBlocks() { - BlockData stone = mock(BlockData.class); - doReturn(Material.STONE).when(stone).getMaterial(); - - assertTrue(BukkitChunkGenerator.shouldApplyMantleOverlayBlock(stone)); - } - - @Test - public void skipsNullOverlayBlocks() { - assertFalse(BukkitChunkGenerator.shouldApplyMantleOverlayBlock(null)); - } -} diff --git a/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/CustomBiomeSource.java b/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/CustomBiomeSource.java index ab5b213bd..c95ce7a6d 100644 --- a/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/CustomBiomeSource.java +++ b/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/CustomBiomeSource.java @@ -42,6 +42,7 @@ public class CustomBiomeSource extends BiomeSource { private final KMap> customBiomes; private final Holder fallbackBiome; private final ConcurrentHashMap> noiseBiomeCache = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> structureBiomeCache = new ConcurrentHashMap<>(); public CustomBiomeSource(long seed, Engine engine, World world) { this.engine = engine; @@ -173,13 +174,33 @@ public class CustomBiomeSource extends BiomeSource { @Override public Holder getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) { + long cacheKey = packNoiseKey(x, y, z); + Holder cachedHolder = structureBiomeCache.get(cacheKey); + if (cachedHolder != null) { + return cachedHolder; + } + + Holder resolvedHolder = resolveStructureBiomeHolder(x, y, z); + Holder existingHolder = structureBiomeCache.putIfAbsent(cacheKey, resolvedHolder); + if (existingHolder != null) { + return existingHolder; + } + + if (structureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) { + structureBiomeCache.clear(); + } + + return resolvedHolder; + } + + public Holder getVisibleNoiseBiome(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 resolvedHolder = resolveVisibleBiomeHolder(x, y, z); Holder existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder); if (existingHolder != null) { return existingHolder; @@ -192,15 +213,66 @@ public class CustomBiomeSource extends BiomeSource { return resolvedHolder; } - private Holder resolveNoiseBiomeHolder(int x, int y, int z) { - if (engine == null || engine.isClosed()) { + private Holder resolveStructureBiomeHolder(int x, int y, int z) { + BiomeResolution resolution = resolveBiomeResolution(x, y, z); + if (resolution == null) { return getFallbackBiome(); } - if (engine.getComplex() == null) { + if (resolution.irisBiome.isCustom()) { + return resolveCustomHolder(resolution); + } + + Holder holder = NMSBinding.biomeToBiomeBase(biomeRegistry, resolution.irisBiome.getVanillaDerivative()); + if (holder != null) { + return holder; + } + + return getFallbackBiome(); + } + + private Holder resolveVisibleBiomeHolder(int x, int y, int z) { + BiomeResolution resolution = resolveBiomeResolution(x, y, z); + if (resolution == null) { return getFallbackBiome(); } + if (resolution.irisBiome.isCustom()) { + return resolveCustomHolder(resolution); + } + + org.bukkit.block.Biome vanillaBiome = resolution.underground + ? resolution.irisBiome.getGroundBiome(resolution.rng, resolution.blockX, resolution.blockY, resolution.blockZ) + : resolution.irisBiome.getSkyBiome(resolution.rng, resolution.blockX, resolution.blockY, resolution.blockZ); + Holder holder = NMSBinding.biomeToBiomeBase(biomeRegistry, vanillaBiome); + if (holder != null) { + return holder; + } + + return getFallbackBiome(); + } + + private Holder resolveCustomHolder(BiomeResolution resolution) { + IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, resolution.blockX, resolution.blockY, resolution.blockZ); + if (customBiome != null) { + Holder holder = customBiomes.get(customBiome.getId()); + if (holder != null) { + return holder; + } + } + + return getFallbackBiome(); + } + + private BiomeResolution resolveBiomeResolution(int x, int y, int z) { + if (engine == null || engine.isClosed()) { + return null; + } + + if (engine.getComplex() == null) { + return null; + } + int blockX = x << 2; int blockZ = z << 2; int blockY = y << 2; @@ -218,7 +290,7 @@ public class CustomBiomeSource extends BiomeSource { irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ); } if (irisBiome == null) { - return getFallbackBiome(); + return null; } RNG noiseRng = new RNG(seed @@ -226,27 +298,7 @@ public class CustomBiomeSource extends BiomeSource { ^ (((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(); + return new BiomeResolution(irisBiome, underground, blockX, blockY, blockZ, noiseRng); } private Holder getFallbackBiome() { @@ -328,4 +380,22 @@ public class CustomBiomeSource extends BiomeSource { return null; } + + private static final class BiomeResolution { + private final IrisBiome irisBiome; + private final boolean underground; + private final int blockX; + private final int blockY; + private final int blockZ; + private final RNG rng; + + private BiomeResolution(IrisBiome irisBiome, boolean underground, int blockX, int blockY, int blockZ, RNG rng) { + this.irisBiome = irisBiome; + this.underground = underground; + this.blockX = blockX; + this.blockY = blockY; + this.blockZ = blockZ; + this.rng = rng; + } + } } 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 ad3b981be..b8d5ce5ca 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 @@ -50,11 +50,18 @@ public class IrisChunkGenerator extends CustomChunkGenerator { private static final WrappedReturningMethod SET_HEIGHT; private final ChunkGenerator delegate; private final Engine engine; + private final CustomBiomeSource customBiomeSource; + private volatile Set reachableStructureKeysCache; public IrisChunkGenerator(ChunkGenerator delegate, long seed, Engine engine, World world) { - super(((CraftWorld) world).getHandle(), edit(delegate, new CustomBiomeSource(seed, engine, world)), null); + this(delegate, engine, world, new CustomBiomeSource(seed, engine, world)); + } + + private IrisChunkGenerator(ChunkGenerator delegate, Engine engine, World world, CustomBiomeSource customBiomeSource) { + super(((CraftWorld) world).getHandle(), edit(delegate, customBiomeSource), null); this.delegate = delegate; this.engine = engine; + this.customBiomeSource = customBiomeSource; } @Override @@ -91,13 +98,40 @@ public class IrisChunkGenerator extends CustomChunkGenerator { if (!importedControl().active()) { return null; } + HolderSet reachable = filterReachableStructures(level, holders); + if (reachable == null || reachable.size() == 0) { + return null; + } try { - return delegate.findNearestMapStructure(level, holders, pos, radius, findUnexplored); + return delegate.findNearestMapStructure(level, reachable, pos, radius, findUnexplored); } catch (Throwable e) { return null; } } + private HolderSet filterReachableStructures(ServerLevel level, HolderSet holders) { + Set reachable = reachableStructureKeysCache; + if (reachable == null) { + reachable = VanillaStructureBiomes.reachableStructureKeys(level, delegate.getBiomeSource()); + reachableStructureKeysCache = reachable; + } + if (reachable.isEmpty()) { + return holders; + } + Registry registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE); + List> kept = new ArrayList<>(); + for (Holder holder : holders) { + Object id = registry.getKey(holder.value()); + if (id != null && reachable.contains(id.toString())) { + kept.add(holder); + } + } + if (kept.size() == holders.size()) { + return holders; + } + return HolderSet.direct(kept); + } + @Override protected MapCodec codec() { return MapCodec.unit(null); @@ -144,7 +178,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator { @Override public CompletableFuture createBiomes(RandomState randomstate, Blender blender, StructureManager structuremanager, ChunkAccess ichunkaccess) { - return delegate.createBiomes(randomstate, blender, structuremanager, ichunkaccess); + ichunkaccess.fillBiomesFromNoise(customBiomeSource::getVisibleNoiseBiome, randomstate.sampler()); + return CompletableFuture.completedFuture(ichunkaccess); } @Override 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 0f639a8b0..e7877b98b 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 @@ -44,6 +44,7 @@ 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.BiomeSource; import net.minecraft.world.level.biome.Biomes; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; @@ -89,9 +90,12 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.*; import java.util.List; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; public class NMSBinding implements INMSBinding { private final KMap baseBiomeCache = new KMap<>(); @@ -412,6 +416,43 @@ public class NMSBinding implements INMSBinding { return keys; } + @Override + public KList getReachableStructureKeys(World world) { + KList keys = new KList<>(); + try { + ServerLevel level = ((CraftWorld) world).getHandle(); + BiomeSource source = level.getChunkSource().getGenerator().getBiomeSource(); + keys.addAll(VanillaStructureBiomes.reachableStructureKeys(level, source)); + } catch (Throwable e) { + Iris.reportError(e); + } + return keys; + } + + @Override + public KList getStructureBiomeKeys(String structureKey) { + KList keys = new KList<>(); + try { + keys.addAll(VanillaStructureBiomes.structureBiomeKeys(registry(), structureKey)); + } catch (Throwable e) { + Iris.reportError(e); + } + return keys; + } + + @Override + public KList getPossibleBiomeKeys(World world) { + KList keys = new KList<>(); + try { + ServerLevel level = ((CraftWorld) world).getHandle(); + BiomeSource source = level.getChunkSource().getGenerator().getBiomeSource(); + keys.addAll(VanillaStructureBiomes.possibleBiomeKeys(source)); + } catch (Throwable e) { + Iris.reportError(e); + } + return keys; + } + @Override public boolean isBukkit() { return true; @@ -555,6 +596,42 @@ public class NMSBinding implements INMSBinding { }); } + @Override + public boolean purgeChunk(World world, int x, int z) { + ServerLevel level = ((CraftWorld) world).getHandle(); + try { + CountDownLatch latch = new CountDownLatch(1); + Runnable unload = () -> { + try { + if (world.isChunkLoaded(x, z)) { + world.unloadChunk(x, z, false); + } + } finally { + latch.countDown(); + } + }; + if (!J.runRegion(world, x, z, unload)) { + return false; + } + if (!latch.await(15, TimeUnit.SECONDS)) { + Iris.warn("Chunk purge timed out waiting to unload " + x + "," + z + " in " + world.getName() + "."); + return false; + } + + for (MoonriseRegionFileIO.RegionFileType type : MoonriseRegionFileIO.RegionFileType.values()) { + MoonriseRegionFileIO.scheduleSave(level, x, z, null, type); + } + MoonriseRegionFileIO.flush(level); + return true; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } catch (Throwable e) { + Iris.reportError(e); + return false; + } + } + public ItemStack applyCustomNbt(ItemStack itemStack, KMap customNbt) throws IllegalArgumentException { if (customNbt != null && !customNbt.isEmpty()) { net.minecraft.world.item.ItemStack s = CraftItemStack.asNMSCopy(itemStack); diff --git a/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/VanillaStructureBiomes.java b/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/VanillaStructureBiomes.java new file mode 100644 index 000000000..9ade4057b --- /dev/null +++ b/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/VanillaStructureBiomes.java @@ -0,0 +1,77 @@ +package art.arcane.iris.core.nms.v1_21_R7; + +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.server.level.ServerLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.levelgen.structure.Structure; + +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +final class VanillaStructureBiomes { + private VanillaStructureBiomes() { + } + + static Set possibleBiomeKeys(BiomeSource source) { + Set keys = new LinkedHashSet<>(); + if (source == null) { + return keys; + } + for (Holder holder : source.possibleBiomes()) { + Optional> key = holder.unwrapKey(); + if (key.isPresent()) { + keys.add(key.get().identifier().toString()); + } + } + return keys; + } + + static Set structureBiomeKeys(RegistryAccess access, String structureKey) { + Set keys = new LinkedHashSet<>(); + if (access == null || structureKey == null || structureKey.isEmpty()) { + return keys; + } + Registry registry = access.lookupOrThrow(Registries.STRUCTURE); + Structure structure = registry.getValue(Identifier.parse(structureKey)); + if (structure == null) { + return keys; + } + for (Holder holder : structure.biomes()) { + Optional> key = holder.unwrapKey(); + if (key.isPresent()) { + keys.add(key.get().identifier().toString()); + } + } + return keys; + } + + static Set reachableStructureKeys(ServerLevel level, BiomeSource source) { + Set reachable = new LinkedHashSet<>(); + if (level == null || source == null) { + return reachable; + } + Set possible = possibleBiomeKeys(source); + if (possible.isEmpty()) { + return reachable; + } + Registry registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE); + for (Map.Entry, Structure> entry : registry.entrySet()) { + for (Holder holder : entry.getValue().biomes()) { + Optional> key = holder.unwrapKey(); + if (key.isPresent() && possible.contains(key.get().identifier().toString())) { + reachable.add(entry.getKey().identifier().toString()); + break; + } + } + } + return reachable; + } +} 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 index c49725dbf..5aefe7c64 100644 --- 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 @@ -42,6 +42,7 @@ public class CustomBiomeSource extends BiomeSource { private final KMap> customBiomes; private final Holder fallbackBiome; private final ConcurrentHashMap> noiseBiomeCache = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> structureBiomeCache = new ConcurrentHashMap<>(); public CustomBiomeSource(long seed, Engine engine, World world) { this.engine = engine; @@ -173,13 +174,33 @@ public class CustomBiomeSource extends BiomeSource { @Override public Holder getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) { + long cacheKey = packNoiseKey(x, y, z); + Holder cachedHolder = structureBiomeCache.get(cacheKey); + if (cachedHolder != null) { + return cachedHolder; + } + + Holder resolvedHolder = resolveStructureBiomeHolder(x, y, z); + Holder existingHolder = structureBiomeCache.putIfAbsent(cacheKey, resolvedHolder); + if (existingHolder != null) { + return existingHolder; + } + + if (structureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) { + structureBiomeCache.clear(); + } + + return resolvedHolder; + } + + public Holder getVisibleNoiseBiome(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 resolvedHolder = resolveVisibleBiomeHolder(x, y, z); Holder existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder); if (existingHolder != null) { return existingHolder; @@ -192,15 +213,66 @@ public class CustomBiomeSource extends BiomeSource { return resolvedHolder; } - private Holder resolveNoiseBiomeHolder(int x, int y, int z) { - if (engine == null || engine.isClosed()) { + private Holder resolveStructureBiomeHolder(int x, int y, int z) { + BiomeResolution resolution = resolveBiomeResolution(x, y, z); + if (resolution == null) { return getFallbackBiome(); } - if (engine.getComplex() == null) { + if (resolution.irisBiome.isCustom()) { + return resolveCustomHolder(resolution); + } + + Holder holder = NMSBinding.biomeToBiomeBase(biomeRegistry, resolution.irisBiome.getVanillaDerivative()); + if (holder != null) { + return holder; + } + + return getFallbackBiome(); + } + + private Holder resolveVisibleBiomeHolder(int x, int y, int z) { + BiomeResolution resolution = resolveBiomeResolution(x, y, z); + if (resolution == null) { return getFallbackBiome(); } + if (resolution.irisBiome.isCustom()) { + return resolveCustomHolder(resolution); + } + + org.bukkit.block.Biome vanillaBiome = resolution.underground + ? resolution.irisBiome.getGroundBiome(resolution.rng, resolution.blockX, resolution.blockY, resolution.blockZ) + : resolution.irisBiome.getSkyBiome(resolution.rng, resolution.blockX, resolution.blockY, resolution.blockZ); + Holder holder = NMSBinding.biomeToBiomeBase(biomeRegistry, vanillaBiome); + if (holder != null) { + return holder; + } + + return getFallbackBiome(); + } + + private Holder resolveCustomHolder(BiomeResolution resolution) { + IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, resolution.blockX, resolution.blockY, resolution.blockZ); + if (customBiome != null) { + Holder holder = customBiomes.get(customBiome.getId()); + if (holder != null) { + return holder; + } + } + + return getFallbackBiome(); + } + + private BiomeResolution resolveBiomeResolution(int x, int y, int z) { + if (engine == null || engine.isClosed()) { + return null; + } + + if (engine.getComplex() == null) { + return null; + } + int blockX = x << 2; int blockZ = z << 2; int blockY = y << 2; @@ -218,7 +290,7 @@ public class CustomBiomeSource extends BiomeSource { irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ); } if (irisBiome == null) { - return getFallbackBiome(); + return null; } RNG noiseRng = new RNG(seed @@ -226,27 +298,7 @@ public class CustomBiomeSource extends BiomeSource { ^ (((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(); + return new BiomeResolution(irisBiome, underground, blockX, blockY, blockZ, noiseRng); } private Holder getFallbackBiome() { @@ -328,4 +380,22 @@ public class CustomBiomeSource extends BiomeSource { return null; } + + private static final class BiomeResolution { + private final IrisBiome irisBiome; + private final boolean underground; + private final int blockX; + private final int blockY; + private final int blockZ; + private final RNG rng; + + private BiomeResolution(IrisBiome irisBiome, boolean underground, int blockX, int blockY, int blockZ, RNG rng) { + this.irisBiome = irisBiome; + this.underground = underground; + this.blockX = blockX; + this.blockY = blockY; + this.blockZ = blockZ; + this.rng = rng; + } + } } 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 index 8f53c2f34..cb432d0c7 100644 --- 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 @@ -50,11 +50,18 @@ public class IrisChunkGenerator extends CustomChunkGenerator { private static final WrappedReturningMethod SET_HEIGHT; private final ChunkGenerator delegate; private final Engine engine; + private final CustomBiomeSource customBiomeSource; + private volatile Set reachableStructureKeysCache; public IrisChunkGenerator(ChunkGenerator delegate, long seed, Engine engine, World world) { - super(((CraftWorld) world).getHandle(), edit(delegate, new CustomBiomeSource(seed, engine, world)), null); + this(delegate, engine, world, new CustomBiomeSource(seed, engine, world)); + } + + private IrisChunkGenerator(ChunkGenerator delegate, Engine engine, World world, CustomBiomeSource customBiomeSource) { + super(((CraftWorld) world).getHandle(), edit(delegate, customBiomeSource), null); this.delegate = delegate; this.engine = engine; + this.customBiomeSource = customBiomeSource; } @Override @@ -91,13 +98,40 @@ public class IrisChunkGenerator extends CustomChunkGenerator { if (!importedControl().active()) { return null; } + HolderSet reachable = filterReachableStructures(level, holders); + if (reachable == null || reachable.size() == 0) { + return null; + } try { - return delegate.findNearestMapStructure(level, holders, pos, radius, findUnexplored); + return delegate.findNearestMapStructure(level, reachable, pos, radius, findUnexplored); } catch (Throwable e) { return null; } } + private HolderSet filterReachableStructures(ServerLevel level, HolderSet holders) { + Set reachable = reachableStructureKeysCache; + if (reachable == null) { + reachable = VanillaStructureBiomes.reachableStructureKeys(level, delegate.getBiomeSource()); + reachableStructureKeysCache = reachable; + } + if (reachable.isEmpty()) { + return holders; + } + Registry registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE); + List> kept = new ArrayList<>(); + for (Holder holder : holders) { + Object id = registry.getKey(holder.value()); + if (id != null && reachable.contains(id.toString())) { + kept.add(holder); + } + } + if (kept.size() == holders.size()) { + return holders; + } + return HolderSet.direct(kept); + } + @Override protected MapCodec codec() { return MapCodec.unit(null); @@ -144,7 +178,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator { @Override public CompletableFuture createBiomes(RandomState randomstate, Blender blender, StructureManager structuremanager, ChunkAccess ichunkaccess) { - return delegate.createBiomes(randomstate, blender, structuremanager, ichunkaccess); + ichunkaccess.fillBiomesFromNoise(customBiomeSource::getVisibleNoiseBiome, randomstate.sampler()); + return CompletableFuture.completedFuture(ichunkaccess); } @Override 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 index 300afe7ac..ce3934b25 100644 --- 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 @@ -44,6 +44,7 @@ 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.BiomeSource; import net.minecraft.world.level.biome.Biomes; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; @@ -89,9 +90,12 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.*; import java.util.List; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; public class NMSBinding implements INMSBinding { private final KMap baseBiomeCache = new KMap<>(); @@ -412,6 +416,43 @@ public class NMSBinding implements INMSBinding { return keys; } + @Override + public KList getReachableStructureKeys(World world) { + KList keys = new KList<>(); + try { + ServerLevel level = ((CraftWorld) world).getHandle(); + BiomeSource source = level.getChunkSource().getGenerator().getBiomeSource(); + keys.addAll(VanillaStructureBiomes.reachableStructureKeys(level, source)); + } catch (Throwable e) { + Iris.reportError(e); + } + return keys; + } + + @Override + public KList getStructureBiomeKeys(String structureKey) { + KList keys = new KList<>(); + try { + keys.addAll(VanillaStructureBiomes.structureBiomeKeys(registry(), structureKey)); + } catch (Throwable e) { + Iris.reportError(e); + } + return keys; + } + + @Override + public KList getPossibleBiomeKeys(World world) { + KList keys = new KList<>(); + try { + ServerLevel level = ((CraftWorld) world).getHandle(); + BiomeSource source = level.getChunkSource().getGenerator().getBiomeSource(); + keys.addAll(VanillaStructureBiomes.possibleBiomeKeys(source)); + } catch (Throwable e) { + Iris.reportError(e); + } + return keys; + } + @Override public boolean isBukkit() { return true; @@ -555,6 +596,42 @@ public class NMSBinding implements INMSBinding { }); } + @Override + public boolean purgeChunk(World world, int x, int z) { + ServerLevel level = ((CraftWorld) world).getHandle(); + try { + CountDownLatch latch = new CountDownLatch(1); + Runnable unload = () -> { + try { + if (world.isChunkLoaded(x, z)) { + world.unloadChunk(x, z, false); + } + } finally { + latch.countDown(); + } + }; + if (!J.runRegion(world, x, z, unload)) { + return false; + } + if (!latch.await(15, TimeUnit.SECONDS)) { + Iris.warn("Chunk purge timed out waiting to unload " + x + "," + z + " in " + world.getName() + "."); + return false; + } + + for (MoonriseRegionFileIO.RegionFileType type : MoonriseRegionFileIO.RegionFileType.values()) { + MoonriseRegionFileIO.scheduleSave(level, x, z, null, type); + } + MoonriseRegionFileIO.flush(level); + return true; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } catch (Throwable e) { + Iris.reportError(e); + return false; + } + } + public ItemStack applyCustomNbt(ItemStack itemStack, KMap customNbt) throws IllegalArgumentException { if (customNbt != null && !customNbt.isEmpty()) { net.minecraft.world.item.ItemStack s = CraftItemStack.asNMSCopy(itemStack); diff --git a/nms/v26_1_R1/src/main/java/art/arcane/iris/core/nms/v26_1_R1/VanillaStructureBiomes.java b/nms/v26_1_R1/src/main/java/art/arcane/iris/core/nms/v26_1_R1/VanillaStructureBiomes.java new file mode 100644 index 000000000..1f9386365 --- /dev/null +++ b/nms/v26_1_R1/src/main/java/art/arcane/iris/core/nms/v26_1_R1/VanillaStructureBiomes.java @@ -0,0 +1,77 @@ +package art.arcane.iris.core.nms.v26_1_R1; + +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.server.level.ServerLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.levelgen.structure.Structure; + +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +final class VanillaStructureBiomes { + private VanillaStructureBiomes() { + } + + static Set possibleBiomeKeys(BiomeSource source) { + Set keys = new LinkedHashSet<>(); + if (source == null) { + return keys; + } + for (Holder holder : source.possibleBiomes()) { + Optional> key = holder.unwrapKey(); + if (key.isPresent()) { + keys.add(key.get().identifier().toString()); + } + } + return keys; + } + + static Set structureBiomeKeys(RegistryAccess access, String structureKey) { + Set keys = new LinkedHashSet<>(); + if (access == null || structureKey == null || structureKey.isEmpty()) { + return keys; + } + Registry registry = access.lookupOrThrow(Registries.STRUCTURE); + Structure structure = registry.getValue(Identifier.parse(structureKey)); + if (structure == null) { + return keys; + } + for (Holder holder : structure.biomes()) { + Optional> key = holder.unwrapKey(); + if (key.isPresent()) { + keys.add(key.get().identifier().toString()); + } + } + return keys; + } + + static Set reachableStructureKeys(ServerLevel level, BiomeSource source) { + Set reachable = new LinkedHashSet<>(); + if (level == null || source == null) { + return reachable; + } + Set possible = possibleBiomeKeys(source); + if (possible.isEmpty()) { + return reachable; + } + Registry registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE); + for (Map.Entry, Structure> entry : registry.entrySet()) { + for (Holder holder : entry.getValue().biomes()) { + Optional> key = holder.unwrapKey(); + if (key.isPresent() && possible.contains(key.get().identifier().toString())) { + reachable.add(entry.getKey().identifier().toString()); + break; + } + } + } + return reachable; + } +}