From 540a416bc9c272df842a87c7d61d0f85691fc35b Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Sun, 31 May 2026 07:51:24 -0400 Subject: [PATCH] Good points --- .../art/arcane/iris/core/IrisSettings.java | 52 +-- .../arcane/iris/core/ServerConfigurator.java | 3 + .../iris/core/commands/CommandFind.java | 62 ++++ .../iris/core/commands/CommandStructure.java | 159 +++++---- .../methods/AsyncPregenMethod.java | 35 +- .../core/runtime/StudioOpenCoordinator.java | 18 + .../arcane/iris/core/service/BoardSVC.java | 43 +++ .../arcane/iris/core/service/CommandSVC.java | 3 + .../core/structure/BulkStructureImporter.java | 39 ++ .../core/structure/StructureImporter.java | 142 +++++++- .../arcane/iris/core/tools/IrisCreator.java | 4 + .../arcane/iris/core/tools/IrisToolbelt.java | 16 +- .../art/arcane/iris/engine/IrisComplex.java | 3 +- .../art/arcane/iris/engine/IrisEngine.java | 2 +- .../arcane/iris/engine/IrisWorldManager.java | 62 +--- .../arcane/iris/engine/framework/Engine.java | 4 + .../framework/IrisStructureLocator.java | 333 ++++++++++++++++++ .../arcane/iris/engine/framework/Locator.java | 4 + .../framework/StructurePlacementGrid.java | 15 +- .../iris/engine/mantle/MantleWriter.java | 19 + .../iris/engine/mantle/MatterGenerator.java | 2 +- .../mantle/components/IrisCaveCarver3D.java | 42 ++- .../components/IrisStructureComponent.java | 193 ++++++---- .../components/MantleObjectComponent.java | 34 +- .../arcane/iris/engine/object/IrisObject.java | 22 +- .../engine/platform/BukkitChunkGenerator.java | 25 +- .../specialhandlers/StructureHandler.java | 89 +++++ .../AsyncPregenMethodConcurrencyCapTest.java | 9 - .../core/nms/v1_21_R7/IrisChunkGenerator.java | 36 +- .../core/nms/v26_1_R1/IrisChunkGenerator.java | 36 +- 30 files changed, 1189 insertions(+), 317 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/engine/framework/IrisStructureLocator.java create mode 100644 core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/StructureHandler.java 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 5145c3153..a009a6c97 100644 --- a/core/src/main/java/art/arcane/iris/core/IrisSettings.java +++ b/core/src/main/java/art/arcane/iris/core/IrisSettings.java @@ -110,16 +110,8 @@ public class IrisSettings { public boolean autoRestartOnCustomBiomeInstall = true; } - @Data - public static class IrisAsyncTeleport { - public boolean enabled = false; - public int loadViewDistance = 2; - public boolean urgent = false; - } - @Data public static class IrisSettingsWorld { - public IrisAsyncTeleport asyncTeleport = new IrisAsyncTeleport(); public boolean postLoadBlockUpdates = true; public boolean forcePersistEntities = true; public boolean anbientEntitySpawningSystem = true; @@ -133,33 +125,27 @@ public class IrisSettings { @Data public static class IrisSettingsConcurrency { - public int parallelism = -1; - public int ioParallelism = -2; - public int worldGenParallelism = -1; + public int getParallelism() { + return Math.max(2, Runtime.getRuntime().availableProcessors()); + } + + public int getIoParallelism() { + return Math.max(2, Runtime.getRuntime().availableProcessors() / 2); + } public int getWorldGenThreads() { - return getThreadCount(worldGenParallelism); + return Math.max(2, Runtime.getRuntime().availableProcessors()); } } @Data public static class IrisSettingsPregen { - public boolean useCacheByDefault = true; - public boolean useHighPriority = false; - public boolean useVirtualThreads = false; public boolean useTicketQueue = true; public IrisRuntimeSchedulerMode runtimeSchedulerMode = IrisRuntimeSchedulerMode.AUTO; public IrisPaperLikeBackendMode paperLikeBackendMode = IrisPaperLikeBackendMode.AUTO; - public int maxConcurrency = 256; - public int paperLikeMaxConcurrency = 96; - public int foliaMaxConcurrency = 32; public int chunkLoadTimeoutSeconds = 15; public int timeoutWarnIntervalMs = 500; public int saveIntervalMs = 30_000; - public boolean enablePregenPerformanceProfile = true; - public int pregenProfileNoiseCacheSize = 4_096; - public boolean pregenProfileEnableFastCache = true; - public boolean pregenProfileLogJvmHints = true; public int getChunkLoadTimeoutSeconds() { return Math.max(5, Math.min(chunkLoadTimeoutSeconds, 120)); @@ -169,14 +155,6 @@ public class IrisSettings { return Math.max(timeoutWarnIntervalMs, 250); } - public int getPaperLikeMaxConcurrency() { - return Math.max(1, paperLikeMaxConcurrency); - } - - public int getFoliaMaxConcurrency() { - return Math.max(1, foliaMaxConcurrency); - } - public IrisPaperLikeBackendMode getPaperLikeBackendMode() { if (paperLikeBackendMode == null) { return IrisPaperLikeBackendMode.AUTO; @@ -200,16 +178,6 @@ public class IrisSettings { public int objectLoaderCacheSize = 4_096; public int tectonicPlateSize = -1; public int mantleCleanupDelay = 200; - public int heightBoundsInterpolationGrid = 4; - - public int getHeightBoundsInterpolationGrid() { - int grid = heightBoundsInterpolationGrid; - if (grid <= 1) { - return 1; - } - - return Math.min(16, Integer.highestOneBit(grid)); - } public int getTectonicPlateSize() { if (tectonicPlateSize > 0) @@ -234,7 +202,6 @@ public class IrisSettings { public int spinh = -20; public int spins = 7; public int spinb = 8; - public String cartographerMessage = "Iris does not allow cartographers in its world due to crashes."; @SuppressWarnings("BooleanMethodIsAlwaysInverted") @@ -262,9 +229,6 @@ public class IrisSettings { public String defaultWorldType = "overworld"; public int maxBiomeChildDepth = 4; public boolean preventLeafDecay = true; - public boolean useMulticore = false; - public boolean useMulticoreMantle = false; - public boolean earlyCustomBlocks = false; } @Data diff --git a/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java b/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java index a37f33188..6ebf8229b 100644 --- a/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java +++ b/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java @@ -235,6 +235,9 @@ public class ServerConfigurator { } for (File file : files) { if (file.isDirectory()) { + if (file.getName().startsWith(".")) { + continue; + } collectFingerprintEntries(file, rootPath, entries); } else { String relative = file.getAbsolutePath().substring(rootPath.length()); 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 5c06e4489..ba7ecbf75 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 @@ -21,15 +21,23 @@ package art.arcane.iris.core.commands; 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.object.IrisBiome; import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.util.common.director.DirectorExecutor; 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.director.DirectorOrigin; import art.arcane.volmlib.util.director.annotations.Director; import art.arcane.volmlib.util.director.annotations.Param; +import io.papermc.lib.PaperLib; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; import org.bukkit.entity.Player; +import org.bukkit.util.StructureSearchResult; @Director(name = "find", origin = DirectorOrigin.PLAYER, description = "Iris Find commands", aliases = "goto") public class CommandFind implements DirectorExecutor { @@ -83,6 +91,60 @@ public class CommandFind implements DirectorExecutor { e.gotoPOI(type, player(), teleport); } + @Director(description = "Find a structure (a vanilla key like minecraft:village_plains or minecraft:stronghold, or an imported iris structure key)") + public void structure( + @Param(description = "The structure to look for (e.g. minecraft:village_plains, minecraft:stronghold, minecraft_ancient_city)", customHandler = StructureHandler.class) + String structure + ) { + Engine e = engine(); + + if (e == null) { + sender().sendMessage(C.GOLD + "Not in an Iris World!"); + return; + } + + if (IrisStructureLocator.isPlaced(e, structure)) { + e.gotoStructure(structure, player(), true); + return; + } + + Player target = player(); + if (target == null) { + sender().sendMessage(C.GOLD + "Run this in-game to teleport to a structure."); + return; + } + + sender().sendMessage(C.GRAY + "Locating " + structure + "..."); + J.s(() -> { + try { + org.bukkit.generator.structure.Structure match = null; + for (org.bukkit.generator.structure.Structure s : Registry.STRUCTURE) { + NamespacedKey key = s.getKey(); + if (key != null && key.toString().equalsIgnoreCase(structure)) { + match = s; + break; + } + } + if (match == null) { + sender().sendMessage(C.RED + "Unknown structure: " + structure); + 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."); + return; + } + Location at = result.getLocation(); + int y = target.getWorld().getHighestBlockYAt(at.getBlockX(), at.getBlockZ()) + 2; + Location dest = new Location(target.getWorld(), at.getBlockX() + 0.5, y, at.getBlockZ() + 0.5); + PaperLib.teleportAsync(target, dest); + sender().sendMessage(C.GREEN + "Teleported to " + structure + " @ " + at.getBlockX() + ", " + at.getBlockZ()); + } catch (Throwable t) { + sender().sendMessage(C.RED + "Could not locate " + structure + ": " + t.getClass().getSimpleName()); + } + }); + } + @Director(description = "Find an object") public void object( @Param(description = "The object to look for", customHandler = ObjectHandler.class) 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 17e0f4c8e..709a8e907 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 @@ -23,33 +23,31 @@ import art.arcane.iris.core.structure.BulkStructureImporter; import art.arcane.iris.core.structure.StructureImporter; import art.arcane.iris.core.structure.StructureIndexService; import art.arcane.iris.core.structure.VillageImporter; -import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.PlacedStructurePiece; import art.arcane.iris.engine.framework.StructureAssembler; -import art.arcane.iris.engine.framework.StructurePlacementGrid; import art.arcane.iris.engine.object.IObjectPlacer; -import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisDimension; -import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.engine.object.IrisObjectPlacement; import art.arcane.iris.engine.object.IrisStructure; -import art.arcane.iris.engine.object.IrisStructurePlacement; import art.arcane.iris.engine.object.ObjectPlaceMode; -import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.volmlib.util.collection.KList; import art.arcane.iris.util.common.director.DirectorExecutor; import art.arcane.iris.util.common.director.DirectorHelp; import art.arcane.iris.util.common.format.C; -import art.arcane.iris.engine.data.cache.Cache; import art.arcane.volmlib.util.director.DirectorOrigin; import art.arcane.volmlib.util.director.annotations.Director; import art.arcane.volmlib.util.director.annotations.Param; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.volmlib.util.math.RNG; +import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.NamespacedKey; +import org.bukkit.Registry; import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.block.data.BlockData; +import org.bukkit.util.StructureSearchResult; import java.io.File; import java.util.HashMap; @@ -173,6 +171,93 @@ public class CommandStructure implements DirectorExecutor { } } + @Director(description = "Re-ingest EVERY vanilla structure and jigsaw template from scratch (overwrite), regenerating all .iob objects so they pick up the latest jigsaw/structure-block conversion. Use this after updating Iris 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(); + if (data == null) { + 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)..."); + 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."); + } + + @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) + public void verify( + @Param(description = "The dimension to verify", aliases = "dim") + IrisDimension dimension, + @Param(description = "Search radius in chunks around the origin (larger is much slower)", defaultValue = "48") + int radius + ) { + World world = resolveIrisWorld(dimension); + if (world == null) { + sender().sendMessage(C.RED + "No loaded Iris world found for " + dimension.getLoadKey() + ". Join or create one first (the search runs against a live world)."); + return; + } + boolean senderIsPlayer = sender() != null && sender().isPlayer(); + Location center = (senderIsPlayer && player().getWorld() == world) ? player().getLocation() : world.getSpawnLocation(); + int searchRadius = Math.max(1, Math.min(radius, 1000)); + + sender().sendMessage(C.GREEN + "Verifying structures in " + C.WHITE + world.getName() + C.GREEN + " from " + center.getBlockX() + "," + center.getBlockZ() + " within " + searchRadius + " chunks..."); + + int found = 0; + int missing = 0; + KList notFound = new KList<>(); + for (org.bukkit.generator.structure.Structure structure : Registry.STRUCTURE) { + NamespacedKey key = structure.getKey(); + String keyName = key == null ? structure.toString() : key.toString(); + 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()); + } else { + missing++; + notFound.add(keyName); + } + } catch (Throwable e) { + missing++; + notFound.add(keyName + " (error: " + e.getClass().getSimpleName() + ")"); + } + } + + sender().sendMessage(C.GREEN + "Structure verify: " + C.WHITE + found + C.GREEN + " generate, " + C.WHITE + missing + C.GREEN + " not found within " + searchRadius + " chunks."); + if (!notFound.isEmpty()) { + sender().sendMessage(C.YELLOW + "Not found (rarer than radius, disabled, or non-generating): " + C.GRAY + String.join(", ", notFound)); + } + } + + private World resolveIrisWorld(IrisDimension dimension) { + if (sender() != null && sender().isPlayer() && IrisToolbelt.isIrisWorld(player().getWorld())) { + return player().getWorld(); + } + World fallback = null; + for (World w : Bukkit.getWorlds()) { + if (!IrisToolbelt.isIrisWorld(w)) { + continue; + } + if (fallback == null) { + fallback = w; + } + PlatformChunkGenerator gen = IrisToolbelt.access(w); + if (gen != null && gen.getEngine() != null && gen.getEngine().getDimension() != null + && dimension.getLoadKey().equals(gen.getEngine().getDimension().getLoadKey())) { + return w; + } + } + return fallback; + } + @Director(description = "Resolve an iris structure's jigsaw graph and report piece count & bounds", origin = DirectorOrigin.BOTH) public void info( @Param(description = "The dimension whose pack holds the structure", aliases = "dim") @@ -249,64 +334,4 @@ public class CommandStructure implements DirectorExecutor { sender().sendMessage(C.GREEN + "Placed '" + structure + "' (" + pieces.size() + " pieces) at your location."); } - @Director(description = "Find the nearest placement of an iris structure around you", aliases = {"l"}, origin = DirectorOrigin.PLAYER) - public void locate( - @Param(description = "The iris structure key to find") - String structure, - @Param(description = "Search radius in chunks", defaultValue = "96") - int radius - ) { - World world = player().getWorld(); - if (!IrisToolbelt.isIrisWorld(world)) { - sender().sendMessage(C.RED + "You must be in an Iris world"); - return; - } - Engine engine = IrisToolbelt.access(world).getEngine(); - IrisData data = engine.getData(); - long seed = engine.getSeedManager().getMantle(); - int pcx = player().getLocation().getBlockX() >> 4; - int pcz = player().getLocation().getBlockZ() >> 4; - int max = Math.max(1, Math.min(radius, 512)); - for (int r = 0; r <= max; r++) { - for (int dx = -r; dx <= r; dx++) { - for (int dz = -r; dz <= r; dz++) { - if (Math.max(Math.abs(dx), Math.abs(dz)) != r) { - continue; - } - int cx = pcx + dx; - int cz = pcz + dz; - if (chunkStarts(engine, seed, structure, cx, cz)) { - sender().sendMessage(C.GREEN + "Nearest '" + structure + "' starts near chunk " + cx + ", " + cz + " (block " + (cx << 4) + ", " + (cz << 4) + ")"); - return; - } - } - } - } - sender().sendMessage(C.YELLOW + "No '" + structure + "' placement found within " + max + " chunks."); - } - - private boolean chunkStarts(Engine engine, long seed, String structure, int cx, int cz) { - int bx = 8 + (cx << 4); - int bz = 8 + (cz << 4); - IrisBiome biome = engine.getComplex().getTrueBiomeStream().get(bx, bz); - IrisRegion region = engine.getComplex().getRegionStream().get(bx, bz); - KList placements = new KList<>(); - if (biome != null) { - placements.addAll(biome.getStructures()); - } - if (region != null) { - placements.addAll(region.getStructures()); - } - placements.addAll(engine.getDimension().getStructures()); - RNG rng = new RNG(Cache.key(cx, cz) + seed); - for (IrisStructurePlacement placement : placements) { - if (!placement.getStructures().contains(structure)) { - continue; - } - if (StructurePlacementGrid.startsInChunk(placement, cx, cz, seed, rng)) { - return true; - } - } - return false; - } } diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java index 407637bbb..81ff4da55 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 @@ -42,7 +42,6 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -64,7 +63,6 @@ public class AsyncPregenMethod implements PregeneratorMethod { private final int runtimeCpuThreads; private final int effectiveWorkerThreads; private final int recommendedRuntimeConcurrencyCap; - private final int configuredMaxConcurrency; private final Method directChunkAtAsyncUrgentMethod; private final Method directChunkAtAsyncMethod; private final String chunkAccessMode; @@ -124,26 +122,18 @@ public class AsyncPregenMethod implements PregeneratorMethod { this.backendMode = "paper-ticket"; } } - int runtimeMaxConcurrency = foliaRuntime - ? pregen.getFoliaMaxConcurrency() - : pregen.getPaperLikeMaxConcurrency(); - int configuredThreads = applyRuntimeConcurrencyCap( - runtimeMaxConcurrency, - foliaRuntime, - workerThreadsForCap - ); - this.configuredMaxConcurrency = Math.max(1, pregen.getMaxConcurrency()); + int configuredThreads = foliaRuntime + ? computeFoliaRecommendedCap(workerThreadsForCap) + : computePaperLikeRecommendedCap(workerThreadsForCap); this.threads = Math.max(1, configuredThreads); this.workerPoolThreads = detectedWorkerPoolThreads; this.runtimeCpuThreads = detectedCpuThreads; this.effectiveWorkerThreads = workerThreadsForCap; - this.recommendedRuntimeConcurrencyCap = foliaRuntime - ? computeFoliaRecommendedCap(workerThreadsForCap) - : computePaperLikeRecommendedCap(workerThreadsForCap); + this.recommendedRuntimeConcurrencyCap = configuredThreads; this.semaphore = new Semaphore(this.threads, true); this.timeoutSeconds = pregen.getChunkLoadTimeoutSeconds(); this.timeoutWarnIntervalMs = pregen.getTimeoutWarnIntervalMs(); - this.urgent = IrisSettings.get().getPregen().useHighPriority; + this.urgent = false; this.lastUse = new ConcurrentHashMap<>(); this.adaptiveInFlightLimit = new AtomicInteger(this.threads); this.adaptiveMinInFlightLimit = Math.max(4, Math.min(16, Math.max(1, this.threads / 4))); @@ -155,7 +145,7 @@ public class AsyncPregenMethod implements PregeneratorMethod { return configuredMode; } - return pregen.isUseVirtualThreads() ? IrisPaperLikeBackendMode.SERVICE : IrisPaperLikeBackendMode.TICKET; + return IrisPaperLikeBackendMode.TICKET; } private int resolveWorkerPoolThreads() { @@ -394,14 +384,6 @@ public class AsyncPregenMethod implements PregeneratorMethod { return Math.max(detectedCpuThreads, Math.max(configuredWorldGenThreads, Math.max(1, detectedWorkerPoolThreads))); } - static int applyRuntimeConcurrencyCap(int maxConcurrency, boolean foliaRuntime, int workerThreads) { - int normalizedMaxConcurrency = Math.max(1, maxConcurrency); - int recommendedCap = foliaRuntime - ? computeFoliaRecommendedCap(workerThreads) - : computePaperLikeRecommendedCap(workerThreads); - return Math.min(normalizedMaxConcurrency, recommendedCap); - } - private String metricsSnapshot() { long stalledFor = Math.max(0L, M.ms() - lastProgressAt.get()); return "world=" + world.getName() @@ -509,7 +491,6 @@ public class AsyncPregenMethod implements PregeneratorMethod { + ", workerPoolThreads=" + workerPoolThreads + ", cpuThreads=" + runtimeCpuThreads + ", effectiveWorkerThreads=" + effectiveWorkerThreads - + ", maxConcurrency=" + configuredMaxConcurrency + ", recommendedCap=" + recommendedRuntimeConcurrencyCap + ", urgent=" + urgent + ", timeout=" + timeoutSeconds + "s"); @@ -779,9 +760,7 @@ public class AsyncPregenMethod implements PregeneratorMethod { } private class ServiceExecutor implements Executor { - private final ExecutorService service = IrisSettings.get().getPregen().isUseVirtualThreads() ? - Executors.newVirtualThreadPerTaskExecutor() : - new MultiBurst("Iris Async Pregen"); + private final ExecutorService service = new MultiBurst("Iris Async Pregen"); public void generate(int x, int z, PregenListener listener) { service.submit(() -> { diff --git a/core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java b/core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java index 8fcbb80e2..2a63b91b6 100644 --- a/core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java +++ b/core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java @@ -85,6 +85,9 @@ public final class StudioOpenCoordinator { World world = null; PlatformChunkGenerator provider = null; try { + long openStart = System.currentTimeMillis(); + long t = openStart; + Iris.info("[Studio timing] ===== studio open START: " + request.worldName() + " ====="); updateStage(request, "resolve_dimension", 0.04D); if (IrisToolbelt.getDimension(request.dimensionKey()) == null) { throw new IrisException("Dimension cannot be found for id " + request.dimensionKey() + "."); @@ -92,6 +95,7 @@ public final class StudioOpenCoordinator { updateStage(request, "prepare_world_pack", 0.10D); cleanupStaleTransientWorlds(request.worldName()); + t = logStudioPhase("resolveDimension + cleanupStaleWorlds", t, openStart); updateStage(request, "install_datapacks", 0.18D); IrisCreator creator = IrisToolbelt.createWorld() @@ -102,6 +106,7 @@ public final class StudioOpenCoordinator { .dimension(request.dimensionKey()) .studioProgressConsumer((progress, stage) -> updateStage(request, mapCreatorStage(stage), progress)); world = creator.create(); + t = logStudioPhase("createWorld (datapacks + bukkit world + engine setup)", t, openStart); provider = IrisToolbelt.access(world); if (provider == null) { throw new IllegalStateException("Studio runtime provider is unavailable for world \"" + request.worldName() + "\"."); @@ -109,14 +114,17 @@ public final class StudioOpenCoordinator { updateStage(request, "apply_world_rules", 0.72D); WorldRuntimeControlService.get().applyStudioWorldRules(world); + t = logStudioPhase("applyStudioWorldRules", t, openStart); updateStage(request, "prepare_generator", 0.78D); WorldRuntimeControlService.get().prepareGenerator(world); + t = logStudioPhase("prepareGenerator", t, openStart); Location entryAnchor = WorldRuntimeControlService.get().resolveEntryAnchor(world); if (entryAnchor == null) { throw new IllegalStateException("Studio entry anchor could not be resolved."); } + t = logStudioPhase("resolveEntryAnchor", t, openStart); updateStage(request, "resolve_safe_entry", 0.84D); Location safeEntry; @@ -129,6 +137,7 @@ public final class StudioOpenCoordinator { if (safeEntry == null) { throw new IllegalStateException("Studio entry point could not be resolved for world \"" + request.worldName() + "\"."); } + t = logStudioPhase("resolveSafeEntry (generates/loads spawn chunk to FULL)", t, openStart); if (request.playerName() != null && !request.playerName().isBlank()) { updateStage(request, "teleport_player", 0.96D); @@ -141,6 +150,7 @@ public final class StudioOpenCoordinator { if (!Boolean.TRUE.equals(teleported)) { throw new IllegalStateException("Studio teleport did not complete successfully."); } + t = logStudioPhase("teleportPlayer", t, openStart); } updateStage(request, "finalize_open", 1.00D); @@ -153,7 +163,9 @@ public final class StudioOpenCoordinator { if (request.onDone() != null) { request.onDone().accept(world); } + t = logStudioPhase("finalize + openVSCode", t, openStart); + Iris.info("[Studio timing] ===== server-side open PREP done = " + (System.currentTimeMillis() - openStart) + "ms (world " + world.getName() + ") — player-view terrain gen follows; the REAL wait is the [Studio timing] player-view lines below ====="); future.complete(new StudioOpenResult(world, safeEntry)); } catch (Throwable e) { Iris.reportError("Studio open failed for world \"" + request.worldName() + "\".", e); @@ -169,6 +181,12 @@ public final class StudioOpenCoordinator { } } + private long logStudioPhase(String phase, long t, long openStart) { + long now = System.currentTimeMillis(); + Iris.info("[Studio timing] " + phase + " = " + (now - t) + "ms (cumulative " + (now - openStart) + "ms)"); + return now; + } + private StudioCloseResult closeWorld( PlatformChunkGenerator provider, String worldName, diff --git a/core/src/main/java/art/arcane/iris/core/service/BoardSVC.java b/core/src/main/java/art/arcane/iris/core/service/BoardSVC.java index b37c43c13..774c8e4d7 100644 --- a/core/src/main/java/art/arcane/iris/core/service/BoardSVC.java +++ b/core/src/main/java/art/arcane/iris/core/service/BoardSVC.java @@ -34,6 +34,7 @@ import art.arcane.iris.util.common.plugin.IrisService; import art.arcane.iris.util.common.scheduling.J; import art.arcane.volmlib.util.matter.MatterCavern; import lombok.Data; +import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.World; import org.bukkit.entity.Player; @@ -41,6 +42,10 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.player.PlayerChangedWorldEvent; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.scoreboard.DisplaySlot; +import org.bukkit.scoreboard.Objective; +import org.bukkit.scoreboard.Scoreboard; +import org.bukkit.scoreboard.Team; import java.util.ArrayList; import java.util.List; @@ -58,11 +63,42 @@ public class BoardSVC implements IrisService, BoardProvider { .scoreDirection(ScoreDirection.DOWN) .build(); + cleanupLeakedMainScoreboard(); + for (Player player : Iris.instance.getServer().getOnlinePlayers()) { J.runEntity(player, () -> updatePlayer(player)); } } + private void cleanupLeakedMainScoreboard() { + try { + if (Bukkit.getScoreboardManager() == null) { + return; + } + Scoreboard main = Bukkit.getScoreboardManager().getMainScoreboard(); + if (main == null) { + return; + } + + Objective named = main.getObjective("board"); + if (named != null) { + named.unregister(); + } + + Objective sidebar = main.getObjective(DisplaySlot.SIDEBAR); + if (sidebar != null && "board".equals(sidebar.getName())) { + sidebar.unregister(); + } + + Team team = main.getTeam("board"); + if (team != null) { + team.unregister(); + } + } catch (Throwable e) { + Iris.reportError(e); + } + } + @Override public void onDisable() { boardEnabled = false; @@ -159,6 +195,13 @@ public class BoardSVC implements IrisService, BoardProvider { public PlayerBoard(Player player) { this.player = player; + try { + if (Bukkit.getScoreboardManager() != null + && player.getScoreboard().equals(Bukkit.getScoreboardManager().getMainScoreboard())) { + player.setScoreboard(Bukkit.getScoreboardManager().getNewScoreboard()); + } + } catch (Throwable ignored) { + } this.board = new Board(player, settings); this.lines = new ArrayList<>(); this.cancelled = false; diff --git a/core/src/main/java/art/arcane/iris/core/service/CommandSVC.java b/core/src/main/java/art/arcane/iris/core/service/CommandSVC.java index 740db7ff3..07b41c8bf 100644 --- a/core/src/main/java/art/arcane/iris/core/service/CommandSVC.java +++ b/core/src/main/java/art/arcane/iris/core/service/CommandSVC.java @@ -206,11 +206,14 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D } private List runDirectorTab(CommandSender sender, String alias, String[] args) { + DirectorContext.touch(new VolmitSender(sender)); try { return getDirector().tabComplete(new DirectorInvocation(new BukkitDirectorSender(sender), alias, Arrays.asList(args))); } catch (Throwable e) { Iris.warn("Director tab completion failed: " + e.getClass().getSimpleName() + " " + e.getMessage()); return List.of(); + } finally { + DirectorContext.remove(); } } 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 d2b3583e4..1e03bb364 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 @@ -87,6 +87,9 @@ public final class BulkStructureImporter { 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++; + sender.sendMessage(C.YELLOW + "[skip] " + keyString + ": no single-template NBT - vanilla builds this in code or from separate piece templates (imported via the templates pass); nothing to import as one structure."); } else { failed++; sender.sendMessage(C.RED + "[fail] " + keyString + ": " + single.message()); @@ -113,6 +116,42 @@ public final class BulkStructureImporter { return new Report(total, imported, skipped, failed); } + public static Report importTemplateGroups(IrisData data, StructureImporter.Mode mode, VolmitSender sender) { + String[][] groups = { + {"shipwreck", "minecraft:shipwreck", "shipwreck"}, + {"ruined_portal", "minecraft:ruined_portal", "ruined_portal"}, + {"ocean_ruin", "minecraft:ocean_ruin_cold", "underwater_ruin"}, + {"nether_fossil", "minecraft:nether_fossil", "nether_fossils"}, + }; + + int imported = 0; + int skipped = 0; + int failed = 0; + + sender.sendMessage(C.GREEN + "Building single-template structures from imported pieces (one variant placed per generation)..."); + for (String[] g : groups) { + try { + StructureImporter.Result result = StructureImporter.importTemplateGroup(data, g[0], g[1], g[2], mode); + if (result.success()) { + imported++; + sender.sendMessage(C.GRAY + "[group] " + g[1] + " -> " + g[0] + " (" + result.blocks() + " variants)"); + } else if (result.message() != null && result.message().startsWith("Skipped")) { + skipped++; + } else { + skipped++; + sender.sendMessage(C.YELLOW + "[skip] " + g[0] + ": " + result.message()); + } + } catch (Throwable e) { + failed++; + sender.sendMessage(C.RED + "[fail] " + g[0] + ": " + e.getMessage()); + } + } + + StructureIndexService.write(data); + sender.sendMessage(C.GREEN + "Single-template structures: " + C.WHITE + imported + C.GREEN + " built, " + C.WHITE + skipped + C.GREEN + " skipped, " + C.WHITE + failed + C.GREEN + " failed."); + return new Report(groups.length, imported, skipped, failed); + } + public static Report importAllTemplates(IrisData data, StructureImporter.Mode mode, VolmitSender sender) { List templateKeys; try { 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 daf436713..d9a5cbcd4 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 @@ -24,13 +24,20 @@ import art.arcane.iris.engine.object.LegacyTileData; import com.google.gson.GsonBuilder; import org.bukkit.Bukkit; import org.bukkit.Location; +import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.block.BlockState; +import org.bukkit.block.data.BlockData; import org.bukkit.structure.Palette; import org.bukkit.structure.Structure; import org.bukkit.util.BlockVector; +import java.util.Optional; + +import java.io.BufferedInputStream; +import java.io.DataInputStream; import java.io.File; +import java.io.FileInputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.ArrayList; @@ -108,12 +115,29 @@ public final class StructureImporter { if (x < 0 || y < 0 || z < 0 || x >= w || y >= h || z >= d) { continue; } - object.setUnsigned(x, y, z, block.getBlockData()); + BlockData blockData = block.getBlockData(); + Material mat = blockData.getMaterial(); + if (mat == Material.STRUCTURE_VOID) { + continue; + } + boolean structural = mat == Material.JIGSAW || mat == Material.STRUCTURE_BLOCK; + if (mat == Material.JIGSAW) { + BlockData resolved = readJigsawFinalState(block); + if (resolved == null || isAir(resolved)) { + continue; + } + blockData = resolved; + } else if (mat == Material.STRUCTURE_BLOCK) { + continue; + } + object.setUnsigned(x, y, z, blockData); count++; - LegacyTileData tile = captureTile(block); - if (tile != null) { - object.setUnsignedTile(x, y, z, tile); - tiles++; + if (!structural) { + LegacyTileData tile = captureTile(block); + if (tile != null) { + object.setUnsignedTile(x, y, z, tile); + tiles++; + } } } @@ -130,6 +154,33 @@ public final class StructureImporter { return new Result(true, "Imported " + key + " as '" + name + "' (" + count + " blocks, " + tiles + " tiles, " + w + "x" + h + "x" + d + ")", count); } + private static boolean isAir(BlockData data) { + Material m = data.getMaterial(); + return m == Material.AIR || m == Material.CAVE_AIR || m == Material.VOID_AIR; + } + + private static BlockData readJigsawFinalState(BlockState block) { + try { + Object nbt = block.getClass().getMethod("getSnapshotNBT").invoke(block); + if (nbt == null) { + return null; + } + Object res = nbt.getClass().getMethod("getString", String.class).invoke(nbt, "final_state"); + String finalState = null; + if (res instanceof String s) { + finalState = s; + } else if (res instanceof Optional o && o.isPresent()) { + finalState = String.valueOf(o.get()); + } + if (finalState == null || finalState.isBlank()) { + return null; + } + return Bukkit.createBlockData(finalState); + } catch (Throwable e) { + return null; + } + } + private static LegacyTileData captureTile(BlockState block) { try { return LegacyTileData.fromBukkit(block); @@ -138,6 +189,87 @@ public final class StructureImporter { } } + public static Result importTemplateGroup(IrisData data, String groupName, String vanillaSource, String prefix, Mode mode) { + File objectsRoot = new File(data.getDataFolder(), "objects"); + File prefixDir = new File(objectsRoot, prefix); + if (!prefixDir.isDirectory()) { + return new Result(false, "No imported templates under objects/" + prefix + " for " + groupName, 0); + } + + List iobs = new ArrayList<>(); + collectIob(prefixDir, iobs); + if (iobs.isEmpty()) { + return new Result(false, "No .iob templates under objects/" + prefix + " for " + groupName, 0); + } + + File structureFile = new File(data.getDataFolder(), "structures/" + groupName + ".json"); + if (mode == Mode.ADD_ONLY && structureFile.exists()) { + return new Result(false, "Skipped (add-only): structure '" + groupName + "' already exists", 0); + } + + String rootPath = objectsRoot.getAbsolutePath() + File.separator; + List pieceNames = new ArrayList<>(); + int maxSpan = 1; + for (File iob : iobs) { + String rel = iob.getAbsolutePath().substring(rootPath.length()).replace(File.separatorChar, '/'); + if (rel.endsWith(".iob")) { + rel = rel.substring(0, rel.length() - ".iob".length()); + } + pieceNames.add(rel); + maxSpan = Math.max(maxSpan, readObjectSpan(iob)); + } + + try { + for (String piece : pieceNames) { + writeJson(new File(data.getDataFolder(), "jigsaw-pieces/" + piece + ".json"), pieceJson(piece)); + } + writeJson(new File(data.getDataFolder(), "jigsaw-pools/" + groupName + ".json"), poolJsonMulti(pieceNames)); + writeJson(structureFile, structureJson(groupName, vanillaSource, maxSpan)); + } catch (Throwable e) { + return new Result(false, "Failed writing group structure '" + groupName + "': " + e.getMessage(), 0); + } + + return new Result(true, "Built structure '" + groupName + "' from " + pieceNames.size() + " template variants", pieceNames.size()); + } + + private static void collectIob(File dir, List out) { + File[] files = dir.listFiles(); + if (files == null) { + return; + } + for (File f : files) { + if (f.isDirectory()) { + collectIob(f, out); + } else if (f.getName().endsWith(".iob")) { + out.add(f); + } + } + } + + private static int readObjectSpan(File iob) { + try (DataInputStream din = new DataInputStream(new BufferedInputStream(new FileInputStream(iob)))) { + int w = din.readInt(); + din.readInt(); + int d = din.readInt(); + return Math.max(1, Math.max(w, d)); + } catch (Throwable e) { + return 1; + } + } + + private static Map poolJsonMulti(List pieceNames) { + List pieces = new ArrayList<>(); + for (String name : pieceNames) { + Map entry = new LinkedHashMap<>(); + entry.put("piece", name); + entry.put("weight", 1); + pieces.add(entry); + } + Map pool = new LinkedHashMap<>(); + pool.put("pieces", pieces); + return pool; + } + private static Map pieceJson(String name) { Map piece = new LinkedHashMap<>(); piece.put("object", name); diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java b/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java index 522d63b91..9d3ed65ab 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java @@ -164,6 +164,7 @@ public class IrisCreator { throw new IrisException("You cannot invoke create() on the main thread."); } + long createStart = System.currentTimeMillis(); reportStudioProgress(0.02D, "resolve_dimension"); reportStudioProgress(0.08D, "resolve_dimension"); IrisDimension d = IrisToolbelt.getDimension(dimension()); @@ -198,6 +199,7 @@ public class IrisCreator { IrisWorlds.get().put(name(), dimension()); } ServerConfigurator.installDataPacksIfChanged(!studio()); + Iris.info("[Studio timing] create.packPrep + datapacks = " + (System.currentTimeMillis() - createStart) + "ms (cumulative in create)"); reportStudioProgress(0.40D, "install_datapacks"); PlatformChunkGenerator access = (PlatformChunkGenerator) wc.generator(); @@ -207,12 +209,14 @@ public class IrisCreator { World world; reportStudioProgress(0.46D, "create_world"); + long nmsStart = System.currentTimeMillis(); try { WorldLifecycleCaller callerKind = benchmark ? WorldLifecycleCaller.BENCHMARK : studio() ? WorldLifecycleCaller.STUDIO : WorldLifecycleCaller.CREATE; WorldLifecycleRequest request = WorldLifecycleRequest.fromCreator(wc, studio(), benchmark, callerKind); world = J.sfut(() -> INMS.get().createWorldAsync(wc, request)) .thenCompose(Function.identity()) .get(); + Iris.info("[Studio timing] create.createWorldAsync (NMS bukkit world load + spawn prep) = " + (System.currentTimeMillis() - nmsStart) + "ms"); } catch (Throwable e) { done.set(true); cancelRepeatingTask(createProgressTask); diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java b/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java index 1bec3c2e6..892d3bcf4 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java @@ -222,7 +222,7 @@ public class IrisToolbelt { * @return the pregenerator job (already started) */ public static PregeneratorJob pregenerate(PregenTask task, PregeneratorMethod method, Engine engine) { - return pregenerate(task, method, engine, IrisSettings.get().getPregen().useCacheByDefault); + return pregenerate(task, method, engine, true); } /** @@ -243,14 +243,9 @@ public class IrisToolbelt { } public static boolean applyPregenPerformanceProfile() { - IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen(); - if (!pregen.isEnablePregenPerformanceProfile()) { - return false; - } - IrisSettings.IrisSettingsPerformance performance = IrisSettings.get().getPerformance(); int previousNoiseCacheSize = performance.getNoiseCacheSize(); - int targetNoiseCacheSize = Math.max(previousNoiseCacheSize, Math.max(1, pregen.getPregenProfileNoiseCacheSize())); + int targetNoiseCacheSize = Math.max(previousNoiseCacheSize, 4_096); boolean fastCacheEnabledBefore = Boolean.getBoolean("iris.cache.fast"); boolean changed = false; @@ -259,15 +254,12 @@ public class IrisToolbelt { changed = true; } - if (pregen.isPregenProfileEnableFastCache() && !fastCacheEnabledBefore) { + if (!fastCacheEnabledBefore) { System.setProperty("iris.cache.fast", "true"); changed = true; } - if (pregen.isPregenProfileLogJvmHints() - && pregen.isPregenProfileEnableFastCache() - && PREGEN_PROFILE_JVM_HINT_LOGGED.compareAndSet(false, true) - && !fastCacheEnabledBefore) { + if (PREGEN_PROFILE_JVM_HINT_LOGGED.compareAndSet(false, true) && !fastCacheEnabledBefore) { Iris.info("For startup-wide cache-fast coverage, set JVM argument: -Diris.cache.fast=true"); } diff --git a/core/src/main/java/art/arcane/iris/engine/IrisComplex.java b/core/src/main/java/art/arcane/iris/engine/IrisComplex.java index 67a990c33..b2c63c158 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisComplex.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisComplex.java @@ -50,6 +50,7 @@ public class IrisComplex implements DataProvider { private static final BlockData AIR = Material.AIR.createBlockData(); private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D); private static final int GRID_BOUNDS_CACHE_SIZE = 8192; + private static final int HEIGHT_BOUNDS_GRID = 4; private static final ThreadLocal GRID_BOUNDS_CACHE = ThreadLocal.withInitial(GridBoundsCache::new); private RNG rng; private double fluidHeight; @@ -337,7 +338,7 @@ public class IrisComplex implements DataProvider { } private NoiseBounds gridSampleBounds(Engine engine, IrisInterpolator interpolator, int interpolatorIndex, Set generators, double x, double z) { - int grid = IrisSettings.get().getPerformance().getHeightBoundsInterpolationGrid(); + int grid = HEIGHT_BOUNDS_GRID; if (grid <= 1) { return sampleBoundsRaw(engine, interpolator, generators, x, z); } diff --git a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java index c52d276ec..0192355f9 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java @@ -253,7 +253,7 @@ public class IrisEngine implements Engine { .filter(File::exists) .filter(File::isDirectory) .toArray(File[]::new); - hash32.complete(IO.hashRecursive(roots)); + hash32.complete(IO.hashRecursiveMeta(roots)); }); } catch (Throwable e) { Iris.error("FAILED TO SETUP ENGINE!"); diff --git a/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java b/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java index 8442895e0..8def8b4df 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java @@ -21,9 +21,7 @@ package art.arcane.iris.engine; import art.arcane.iris.Iris; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.gui.PregeneratorJob; -import art.arcane.iris.core.link.Identifier; import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.core.service.ExternalDataSVC; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.data.cache.Cache; import art.arcane.iris.engine.framework.Engine; @@ -699,48 +697,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager { cleanup.remove(key); getEngine().cleanupMantleChunk(cX, cZ); }, Math.max(IrisSettings.get().getPerformance().mantleCleanupDelay * 50L, 0), TimeUnit.MILLISECONDS)); - - if (generated) { - //INMS.get().injectBiomesFromMantle(e, getMantle()); - - if (!IrisSettings.get().getGenerator().earlyCustomBlocks) return; - if (isPregenActiveForThisWorld()) return; - - World world = e.getWorld(); - int chunkX = e.getX(); - int chunkZ = e.getZ(); - int minY = getTarget().getWorld().minHeight(); - int delay = RNG.r.i(20, 60); - Iris.tickets.addTicket(e); - - Runnable applyCustomBlocks = () -> { - if (J.isFolia() && (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ))) { - Iris.tickets.removeTicket(e); - return; - } - - Chunk chunkRef = world.getChunkAt(chunkX, chunkZ); - MantleChunk mantleChunk = getMantle().getChunk(chunkRef).use(); - try { - mantleChunk.raiseFlagUnchecked(MantleFlag.CUSTOM, () -> { - mantleChunk.iterate(Identifier.class, (x, y, z, v) -> { - Iris.service(ExternalDataSVC.class).processUpdate(getEngine(), chunkRef.getBlock(x & 15, y + minY, z & 15), v); - }); - }); - } finally { - mantleChunk.release(); - Iris.tickets.removeTicket(e); - } - }; - - if (J.isFolia()) { - if (!J.runRegion(world, chunkX, chunkZ, applyCustomBlocks, delay)) { - Iris.tickets.removeTicket(e); - } - } else { - J.s(applyCustomBlocks, delay); - } - } } @Override @@ -777,19 +733,17 @@ public class IrisWorldManager extends EngineAssignedWorldManager { @Override public void teleportAsync(PlayerTeleportEvent e) { - if (IrisSettings.get().getWorld().getAsyncTeleport().isEnabled()) { - e.setCancelled(true); - warmupAreaAsync(e.getPlayer(), e.getTo(), () -> J.runEntity(e.getPlayer(), () -> { - ignoreTP.set(true); - e.getPlayer().teleport(e.getTo(), e.getCause()); - ignoreTP.set(false); - })); - } + e.setCancelled(true); + warmupAreaAsync(e.getPlayer(), e.getTo(), () -> J.runEntity(e.getPlayer(), () -> { + ignoreTP.set(true); + e.getPlayer().teleport(e.getTo(), e.getCause()); + ignoreTP.set(false); + })); } private void warmupAreaAsync(Player player, Location to, Runnable r) { J.a(() -> { - int viewDistance = IrisSettings.get().getWorld().getAsyncTeleport().getLoadViewDistance(); + int viewDistance = 2; KList> futures = new KList<>(); for (int i = -viewDistance; i <= viewDistance; i++) { for (int j = -viewDistance; j <= viewDistance; j++) { @@ -805,7 +759,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager { -> PaperLib.getChunkAtAsync(to.getWorld(), (to.getBlockX() >> 4) + finalI, (to.getBlockZ() >> 4) + finalJ, - true, IrisSettings.get().getWorld().getAsyncTeleport().isUrgent()).get())); + true, false).get())); } } diff --git a/core/src/main/java/art/arcane/iris/engine/framework/Engine.java b/core/src/main/java/art/arcane/iris/engine/framework/Engine.java index ab2dcacf5..28f9e064d 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/Engine.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/Engine.java @@ -1012,6 +1012,10 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat Locator.poi(type).find(p, teleport, "POI " + type); } + default void gotoStructure(String key, Player player, boolean teleport) { + Locator.structure(key).find(player, teleport, "Structure " + key); + } + private static boolean containsObjectPlacement(KList placements, String normalizedObjectKey) { if (placements == null || placements.isEmpty() || normalizedObjectKey.isBlank()) { return false; diff --git a/core/src/main/java/art/arcane/iris/engine/framework/IrisStructureLocator.java b/core/src/main/java/art/arcane/iris/engine/framework/IrisStructureLocator.java new file mode 100644 index 000000000..5b42f3fbc --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/IrisStructureLocator.java @@ -0,0 +1,333 @@ +/* + * 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.engine.data.cache.Cache; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.object.IrisStructure; +import art.arcane.iris.engine.object.IrisStructurePlacement; +import art.arcane.iris.engine.object.StructureDistribution; +import art.arcane.iris.engine.object.StructurePlacementRoute; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; + +/** + * Finds where IRIS_PLACED structures generate. A structure key matches either the iris + * structure's own load key or its {@code vanillaSource} (so a vanilla key like + * {@code minecraft:ancient_city} resolves to the imported {@code minecraft_ancient_city}). + * + * A per-pack index of placed keys is cached so {@code /locate} and {@code /iris goto} skip + * the grid scan entirely for keys that are not placed (keeping them as fast as vanilla). + * + * For RANDOM_SPREAD placements (the common, vanilla-style spaced grid) the locator jumps + * straight to the single candidate chunk in each spacing-sized grid cell rather than testing + * every chunk -- roughly a {@code spacing^2} reduction in placement checks, matching vanilla + * locate performance. Non-grid distributions (DENSITY / CONCENTRIC_RINGS) fall back to the + * exhaustive per-chunk ring scan. + */ +public final class IrisStructureLocator { + private static final Map INDEX_CACHE = Collections.synchronizedMap(new WeakHashMap<>()); + + private IrisStructureLocator() { + } + + /** Iris structure load keys that are referenced by any IRIS_PLACED placement (for autocomplete). */ + public static Set placedKeys(Engine engine) { + if (engine == null) { + return Collections.emptySet(); + } + return index(engine).loadKeys; + } + + public static boolean isPlaced(Engine engine, String key) { + if (engine == null || key == null || key.isEmpty()) { + return false; + } + PlacementIndex idx = index(engine); + return idx.loadKeys.contains(key) || idx.vanillaSources.contains(key); + } + + public static boolean startsInChunk(Engine engine, String key, int cx, int cz) { + if (!isPlaced(engine, key)) { + return false; + } + long seed = engine.getSeedManager().getMantle(); + RNG rng = new RNG(Cache.key(cx, cz) + seed); + for (IrisStructurePlacement placement : placementsAt(engine, cx, cz)) { + if (placement.getRoute() == StructurePlacementRoute.NATIVE_AT_POINT) { + continue; + } + if (!matches(placement, key, engine.getData())) { + continue; + } + if (StructurePlacementGrid.startsInChunk(placement, cx, cz, seed, rng)) { + return true; + } + } + return false; + } + + public static int[] locate(Engine engine, String key, int fromBlockX, int fromBlockZ, int maxRadiusChunks) { + if (!isPlaced(engine, key)) { + return null; + } + int max = Math.max(1, Math.min(maxRadiusChunks, 2048)); + + List gridParams = collectRandomSpreadParams(engine, key); + if (gridParams == null || gridParams.isEmpty()) { + // A non-grid distribution (DENSITY / CONCENTRIC_RINGS) matches this key, or no grid + // params could be resolved; fall back to the exhaustive per-chunk scan. + return locateByChunkScan(engine, key, fromBlockX, fromBlockZ, max); + } + + long seed = engine.getSeedManager().getMantle(); + int pcx = fromBlockX >> 4; + int pcz = fromBlockZ >> 4; + long maxDistSq = (long) max * (long) max; + long bestDistSq = Long.MAX_VALUE; + int[] best = null; + + for (int[] params : gridParams) { + int spacing = Math.max(1, params[0]); + int separation = params[1]; + int salt = params[2]; + int centerCellX = Math.floorDiv(pcx, spacing); + int centerCellZ = Math.floorDiv(pcz, spacing); + int cellRadius = (max / spacing) + 2; + int firstHitRing = -1; + + for (int r = 0; r <= cellRadius; r++) { + if (firstHitRing >= 0 && r > firstHitRing + 1) { + break; + } + for (int dx = -r; dx <= r; dx++) { + for (int dz = -r; dz <= r; dz++) { + if (Math.max(Math.abs(dx), Math.abs(dz)) != r) { + continue; + } + int[] candidate = StructurePlacementGrid.randomSpreadCellChunk( + centerCellX + dx, centerCellZ + dz, spacing, separation, salt, seed); + int cx = candidate[0]; + int cz = candidate[1]; + long ddx = (long) cx - pcx; + long ddz = (long) cz - pcz; + long distSq = ddx * ddx + ddz * ddz; + if (distSq > maxDistSq || distSq >= bestDistSq) { + continue; + } + if (startsInChunk(engine, key, cx, cz)) { + bestDistSq = distSq; + best = new int[]{cx << 4, structureY(engine, key, cx, cz), cz << 4}; + if (firstHitRing < 0) { + firstHitRing = r; + } + } + } + } + } + } + + return best; + } + + private static int[] locateByChunkScan(Engine engine, String key, int fromBlockX, int fromBlockZ, int max) { + int pcx = fromBlockX >> 4; + int pcz = fromBlockZ >> 4; + for (int r = 0; r <= max; r++) { + for (int dx = -r; dx <= r; dx++) { + for (int dz = -r; dz <= r; dz++) { + if (Math.max(Math.abs(dx), Math.abs(dz)) != r) { + continue; + } + int cx = pcx + dx; + int cz = pcz + dz; + if (startsInChunk(engine, key, cx, cz)) { + return new int[]{cx << 4, structureY(engine, key, cx, cz), cz << 4}; + } + } + } + } + return null; + } + + /** + * Collects the distinct {spacing, separation, salt} tuples of every RANDOM_SPREAD placement that + * matches the key across the dimension, its regions and biomes. Returns {@code null} if any + * matching placement uses a non-grid distribution (DENSITY / CONCENTRIC_RINGS), signalling the + * caller to fall back to the exhaustive per-chunk scan. + */ + private static List collectRandomSpreadParams(Engine engine, String key) { + IrisData data = engine.getData(); + List params = new ArrayList<>(); + Set seen = new LinkedHashSet<>(); + + KList all = new KList<>(); + all.addAll(engine.getDimension().getStructures()); + for (IrisRegion region : engine.getDimension().getAllRegions(engine)) { + all.addAll(region.getStructures()); + } + for (IrisBiome biome : engine.getDimension().getAllBiomes(engine)) { + all.addAll(biome.getStructures()); + } + + for (IrisStructurePlacement placement : all) { + if (placement == null || placement.getRoute() == StructurePlacementRoute.NATIVE_AT_POINT) { + continue; + } + if (!matches(placement, key, data)) { + continue; + } + if (placement.getDistribution() != StructureDistribution.RANDOM_SPREAD) { + return null; + } + int spacing = Math.max(1, placement.getSpacing()); + int separation = placement.getSeparation(); + int salt = placement.getSalt(); + long packed = ((long) spacing << 42) ^ ((long) (separation & 0x1FFFFF) << 21) ^ (salt & 0x1FFFFFL); + if (seen.add(packed)) { + params.add(new int[]{spacing, separation, salt}); + } + } + + return params; + } + + private static KList placementsAt(Engine engine, int cx, int cz) { + int bx = 8 + (cx << 4); + int bz = 8 + (cz << 4); + IrisBiome biome = engine.getComplex().getTrueBiomeStream().get(bx, bz); + IrisRegion region = engine.getComplex().getRegionStream().get(bx, bz); + KList placements = new KList<>(); + if (biome != null) { + placements.addAll(biome.getStructures()); + } + if (region != null) { + placements.addAll(region.getStructures()); + } + placements.addAll(engine.getDimension().getStructures()); + return placements; + } + + private static boolean matches(IrisStructurePlacement placement, String key, IrisData data) { + for (String structureKey : placement.getStructures()) { + if (structureKey == null) { + continue; + } + if (structureKey.equalsIgnoreCase(key)) { + return true; + } + IrisStructure structure = IrisData.loadAnyStructure(structureKey, data); + if (structure != null) { + String vanillaSource = structure.getVanillaSource(); + if (vanillaSource != null && !vanillaSource.isEmpty() && vanillaSource.equalsIgnoreCase(key)) { + return true; + } + } + } + return false; + } + + private static int structureY(Engine engine, String key, int cx, int cz) { + IrisData data = engine.getData(); + int bx = 8 + (cx << 4); + int bz = 8 + (cz << 4); + for (IrisStructurePlacement placement : placementsAt(engine, cx, cz)) { + if (placement.getRoute() == StructurePlacementRoute.NATIVE_AT_POINT) { + continue; + } + if (!matches(placement, key, data)) { + continue; + } + if (placement.isUnderground()) { + int lo = Math.max(engine.getMinHeight() + 1, Math.min(placement.getMinHeight(), placement.getMaxHeight())); + int hi = Math.min(engine.getMinHeight() + engine.getHeight() - 1, Math.max(placement.getMinHeight(), placement.getMaxHeight())); + return (lo + hi) / 2; + } + } + return engine.getHeight(bx, bz) + engine.getMinHeight(); + } + + private static PlacementIndex index(Engine engine) { + IrisData data = engine.getData(); + PlacementIndex cached = INDEX_CACHE.get(data); + if (cached != null) { + return cached; + } + PlacementIndex built = build(engine, data); + INDEX_CACHE.put(data, built); + return built; + } + + private static PlacementIndex build(Engine engine, IrisData data) { + Set loadKeys = new LinkedHashSet<>(); + Set vanillaSources = new LinkedHashSet<>(); + collect(engine.getDimension().getStructures(), data, loadKeys, vanillaSources); + for (IrisRegion region : engine.getDimension().getAllRegions(engine)) { + collect(region.getStructures(), data, loadKeys, vanillaSources); + } + for (IrisBiome biome : engine.getDimension().getAllBiomes(engine)) { + collect(biome.getStructures(), data, loadKeys, vanillaSources); + } + return new PlacementIndex(loadKeys, vanillaSources); + } + + private static void collect(KList placements, IrisData data, Set loadKeys, Set vanillaSources) { + if (placements == null) { + return; + } + for (IrisStructurePlacement placement : placements) { + if (placement == null || placement.getRoute() == StructurePlacementRoute.NATIVE_AT_POINT) { + continue; + } + for (String structureKey : placement.getStructures()) { + if (structureKey == null || structureKey.isEmpty()) { + continue; + } + loadKeys.add(structureKey); + IrisStructure structure = IrisData.loadAnyStructure(structureKey, data); + if (structure != null) { + String vanillaSource = structure.getVanillaSource(); + if (vanillaSource != null && !vanillaSource.isEmpty()) { + vanillaSources.add(vanillaSource); + } + } + } + } + } + + private static final class PlacementIndex { + private final Set loadKeys; + private final Set vanillaSources; + + private PlacementIndex(Set loadKeys, Set vanillaSources) { + this.loadKeys = loadKeys; + this.vanillaSources = vanillaSources; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/Locator.java b/core/src/main/java/art/arcane/iris/engine/framework/Locator.java index d2b849f6f..9b6227ab8 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/Locator.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/Locator.java @@ -77,6 +77,10 @@ public interface Locator { return (e, c) -> e.getSurfaceBiome((c.getX() << 4) + 8, (c.getZ() << 4) + 8).getLoadKey().equals(loadKey); } + static Locator structure(String key) { + return (e, c) -> IrisStructureLocator.startsInChunk(e, key, c.getX(), c.getZ()); + } + static Locator poi(String type) { return (e, c) -> { Set> pos = e.getPOIsAt((c.getX() << 4) + 8, (c.getZ() << 4) + 8); diff --git a/core/src/main/java/art/arcane/iris/engine/framework/StructurePlacementGrid.java b/core/src/main/java/art/arcane/iris/engine/framework/StructurePlacementGrid.java index b78520637..38e40b8f7 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/StructurePlacementGrid.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/StructurePlacementGrid.java @@ -35,14 +35,25 @@ public final class StructurePlacementGrid { public static boolean randomSpreadStart(int cx, int cz, int spacing, int separation, int salt, long seed) { int sp = Math.max(1, spacing); - int sep = Math.max(0, Math.min(separation, sp - 1)); int cellX = Math.floorDiv(cx, sp); int cellZ = Math.floorDiv(cz, sp); + int[] start = randomSpreadCellChunk(cellX, cellZ, spacing, separation, salt, seed); + return start[0] == cx && start[1] == cz; + } + + /** + * Returns the single {chunkX, chunkZ} that a RANDOM_SPREAD placement occupies within the given + * grid cell. This is the inverse of {@link #randomSpreadStart} and uses the identical formula, so + * a locator can jump straight to the one candidate per cell instead of testing every chunk. + */ + public static int[] randomSpreadCellChunk(int cellX, int cellZ, int spacing, int separation, int salt, long seed) { + int sp = Math.max(1, spacing); + int sep = Math.max(0, Math.min(separation, sp - 1)); RNG r = new RNG(mix(seed, cellX, cellZ, salt)); int range = Math.max(1, sp - sep); int startCx = cellX * sp + r.i(0, range - 1); int startCz = cellZ * sp + r.i(0, range - 1); - return startCx == cx && startCz == cz; + return new int[]{startCx, startCz}; } private static boolean concentricRingsStart(int cx, int cz, IrisStructurePlacement placement, long seed) { diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java b/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java index 47608f25e..03a3e81d8 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java @@ -201,6 +201,25 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable { return true; } + public void clearBlock(int x, int y, int z) { + if (y < 0 || y >= mantle.getWorldHeight()) { + return; + } + MantleChunk chunk = acquireChunk(x >> 4, z >> 4); + if (chunk == null) { + return; + } + int section = y >> 4; + if (!chunk.exists(section)) { + return; + } + Matter matter = chunk.get(section); + if (matter == null) { + return; + } + matter.slice(BlockData.class).set(x & 15, y & 15, z & 15, null); + } + public T getData(int x, int y, int z, Class type) { int cx = x >> 4; int cz = z >> 4; 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 b53c40e4d..e0602ece4 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 @@ -50,7 +50,7 @@ public interface MatterGenerator { return; } - boolean useMulticore = multicore || IrisSettings.get().getGenerator().isUseMulticoreMantle(); + boolean useMulticore = multicore; String threadName = Thread.currentThread().getName(); boolean regenThread = threadName.startsWith("Iris-Regen-"); boolean traceRegen = regenThread && IrisSettings.get().getGeneral().isDebug(); diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java index 11413ad17..be087e859 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java @@ -69,6 +69,7 @@ public class IrisCaveCarver3D { private final double warpStrength; private final boolean hasWarp; private final boolean hasModules; + private final int warpResolution; public IrisCaveCarver3D(Engine engine, IrisCaveProfile profile) { this.engine = engine; @@ -89,6 +90,7 @@ public class IrisCaveCarver3D { this.detailWeight = profile.getDetailWeight(); this.warpStrength = profile.getWarpStrength(); this.hasWarp = this.warpStrength > 0D; + this.warpResolution = 2; double weight = Math.abs(baseWeight) + Math.abs(detailWeight); int index = 0; @@ -1589,9 +1591,17 @@ public class IrisCaveCarver3D { return density <= thresholdLimit; } + private int snapWarp(int c) { + int g = warpResolution; + return g <= 1 ? c : Math.floorDiv(c, g) * g; + } + private boolean classifyDensityPointWarpOnly(int x, int y, int z, double thresholdLimit) { - double warpA = warpDensity.noiseFastSigned3D(x, y, z); - double warpB = warpDensity.noiseFastSigned3D(x + 31.37D, y - 17.21D, z + 23.91D); + int sx = snapWarp(x); + int sy = snapWarp(y); + int sz = snapWarp(z); + double warpA = warpDensity.noiseFastSigned3D(sx, sy, sz); + double warpB = warpDensity.noiseFastSigned3D(sx + 31.37D, sy - 17.21D, sz + 23.91D); double warpedX = x + (warpA * warpStrength); double warpedY = y + (warpB * warpStrength); double warpedZ = z + ((warpA - warpB) * 0.5D * warpStrength); @@ -1621,8 +1631,11 @@ public class IrisCaveCarver3D { return classifyDensityPointWarpOnly(x, y, z, thresholdLimit); } - double warpA = warpDensity.noiseFastSigned3D(x, y, z); - double warpB = warpDensity.noiseFastSigned3D(x + 31.37D, y - 17.21D, z + 23.91D); + int sx = snapWarp(x); + int sy = snapWarp(y); + int sz = snapWarp(z); + double warpA = warpDensity.noiseFastSigned3D(sx, sy, sz); + double warpB = warpDensity.noiseFastSigned3D(sx + 31.37D, sy - 17.21D, sz + 23.91D); double warpedX = x + (warpA * warpStrength); double warpedY = y + (warpB * warpStrength); double warpedZ = z + ((warpA - warpB) * 0.5D * warpStrength); @@ -1716,8 +1729,11 @@ public class IrisCaveCarver3D { return true; } - double warpA = warpDensity.noiseFastSigned3D(x, y, z); - double warpB = warpDensity.noiseFastSigned3D(x + 31.37D, y - 17.21D, z + 23.91D); + int sx = snapWarp(x); + int sy = snapWarp(y); + int sz = snapWarp(z); + double warpA = warpDensity.noiseFastSigned3D(sx, sy, sz); + double warpB = warpDensity.noiseFastSigned3D(sx + 31.37D, sy - 17.21D, sz + 23.91D); double warpedX = x + (warpA * warpStrength); double warpedY = y + (warpB * warpStrength); double warpedZ = z + ((warpA - warpB) * 0.5D * warpStrength); @@ -1844,8 +1860,11 @@ public class IrisCaveCarver3D { } private double sampleDensityWarpOnly(int x, int y, int z) { - double warpA = warpDensity.noiseFastSigned3D(x, y, z); - double warpB = warpDensity.noiseFastSigned3D(x + 31.37D, y - 17.21D, z + 23.91D); + int sx = snapWarp(x); + int sy = snapWarp(y); + int sz = snapWarp(z); + double warpA = warpDensity.noiseFastSigned3D(sx, sy, sz); + double warpB = warpDensity.noiseFastSigned3D(sx + 31.37D, sy - 17.21D, sz + 23.91D); double warpedX = x + (warpA * warpStrength); double warpedY = y + (warpB * warpStrength); double warpedZ = z + ((warpA - warpB) * 0.5D * warpStrength); @@ -1866,8 +1885,11 @@ public class IrisCaveCarver3D { } private double sampleDensityWarpModules(int x, int y, int z, ModuleState[] localModules, int activeModuleCount) { - double warpA = warpDensity.noiseFastSigned3D(x, y, z); - double warpB = warpDensity.noiseFastSigned3D(x + 31.37D, y - 17.21D, z + 23.91D); + int sx = snapWarp(x); + int sy = snapWarp(y); + int sz = snapWarp(z); + double warpA = warpDensity.noiseFastSigned3D(sx, sy, sz); + double warpB = warpDensity.noiseFastSigned3D(sx + 31.37D, sy - 17.21D, sz + 23.91D); double warpedX = x + (warpA * warpStrength); double warpedY = y + (warpB * warpStrength); double warpedZ = z + ((warpA - warpB) * 0.5D * warpStrength); diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java index be1447de0..f9707e8cf 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java @@ -38,23 +38,22 @@ import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.engine.object.IrisStructure; import art.arcane.iris.engine.object.IrisStructurePlacement; import art.arcane.iris.engine.object.StructurePlacementRoute; -import art.arcane.iris.util.common.data.B; import art.arcane.iris.util.project.noise.CNG; import art.arcane.volmlib.util.collection.KList; import art.arcane.iris.util.project.context.ChunkContext; import art.arcane.volmlib.util.documentation.ChunkCoordinates; +import art.arcane.volmlib.util.matter.MatterCavern; import art.arcane.volmlib.util.mantle.flag.ReservedFlag; import art.arcane.volmlib.util.math.RNG; -import org.bukkit.block.data.BlockData; @ComponentFlag(ReservedFlag.JIGSAW) public class IrisStructureComponent extends IrisMantleComponent { private static final long MAX_BORE_VOLUME = 6_000_000L; private static final long MAX_OVERBORE_VOLUME = 48_000_000L; - private static final BlockData CARVE_AIR = B.get("CAVE_AIR"); + private static final MatterCavern CARVE_CAVERN = new MatterCavern(true, "", (byte) 0); public IrisStructureComponent(EngineMantle engineMantle) { - super(engineMantle, ReservedFlag.JIGSAW, 1); + super(engineMantle, ReservedFlag.JIGSAW, 3); } @Override @@ -93,6 +92,12 @@ public class IrisStructureComponent extends IrisMantleComponent { return; } + boolean trace = IrisSettings.get().getGeneral().isDebug(); + if (trace) { + Iris.info("[StructTrace] ORIGIN chunk=" + cx + "," + cz + " structures=" + placement.getStructures() + + " underground=" + placement.isUnderground() + " band=" + placement.getMinHeight() + ".." + placement.getMaxHeight()); + } + int sx = (cx << 4) + rng.i(0, 15); int sz = (cz << 4) + rng.i(0, 15); int baseY; @@ -102,6 +107,10 @@ public class IrisStructureComponent extends IrisMantleComponent { int bandMin = Math.max(worldMinY, Math.min(placement.getMinHeight(), placement.getMaxHeight())); int bandMax = Math.min(worldMaxY, Math.max(placement.getMinHeight(), placement.getMaxHeight())); if (bandMin > bandMax) { + if (trace) { + Iris.info("[StructTrace] BAIL band-inverted chunk=" + cx + "," + cz + " bandMin=" + bandMin + " bandMax=" + bandMax + + " worldMinY=" + worldMinY + " worldMaxY=" + worldMaxY); + } return; } baseY = bandMin == bandMax ? bandMin : rng.i(bandMin, bandMax); @@ -116,14 +125,24 @@ public class IrisStructureComponent extends IrisMantleComponent { String key = placement.getStructures().get(rng.i(0, placement.getStructures().size() - 1)); IrisStructure structure = art.arcane.iris.core.loader.IrisData.loadAnyStructure(key, getData()); if (structure == null) { + if (trace) { + Iris.info("[StructTrace] BAIL structure-load-null chunk=" + cx + "," + cz + " key=" + key); + } return; } StructureAssembler assembler = new StructureAssembler(getData(), structure, sx, baseY, sz); KList pieces = assembler.assemble(rng); if (pieces == null || pieces.isEmpty()) { + if (trace) { + Iris.info("[StructTrace] BAIL no-pieces chunk=" + cx + "," + cz + " key=" + key + " baseY=" + baseY + + " pieces=" + (pieces == null ? "null" : "empty")); + } return; } + if (trace) { + Iris.info("[StructTrace] ASSEMBLED chunk=" + cx + "," + cz + " key=" + key + " baseY=" + baseY + " pieces=" + pieces.size()); + } if (placement.isOverbore()) { overboreStructure(writer, pieces, placement.getOverboreRadius(), placement.getOverboreHeight(), placement.getOverboreFloor()); @@ -132,7 +151,13 @@ public class IrisStructureComponent extends IrisMantleComponent { } ObjectPlaceMode mode = structure.getPlaceMode(); - if (placement.isUnderground() || mode == ObjectPlaceMode.STRUCTURE_PIECE || mode == ObjectPlaceMode.FLOATING) { + if (placement.isUnderground()) { + ObjectPlaceMode undergroundMode = (mode == ObjectPlaceMode.ORGANIC_STILT || mode == ObjectPlaceMode.CEILING_HANG) + ? mode : ObjectPlaceMode.STRUCTURE_PIECE; + for (PlacedStructurePiece p : pieces) { + placeObject(writer, structure, p, undergroundMode, p.getY(), rng); + } + } else if (mode == ObjectPlaceMode.STRUCTURE_PIECE || mode == ObjectPlaceMode.FLOATING) { for (PlacedStructurePiece p : pieces) { placeObject(writer, structure, p, ObjectPlaceMode.STRUCTURE_PIECE, p.getY(), rng); } @@ -174,10 +199,11 @@ public class IrisStructureComponent extends IrisMantleComponent { Iris.warn("Skipping structure bore of " + volume + " blocks (cap " + MAX_BORE_VOLUME + "); use a smaller structure or larger spacing."); return; } + int mantleOffset = getEngineMantle().getEngine().getMinHeight(); for (int bx = minX; bx <= maxX; bx++) { for (int by = minY; by <= maxY; by++) { for (int bz = minZ; bz <= maxZ; bz++) { - writer.set(bx, by, bz, CARVE_AIR); + writer.setDataIfAbsent(bx, by - mantleOffset, bz, CARVE_CAVERN); } } } @@ -188,82 +214,99 @@ public class IrisStructureComponent extends IrisMantleComponent { if (bounds == null) { return; } - int r = Math.max(0, radius); - int boxMinX = bounds[0]; - int boxMinZ = bounds[2]; - int boxMaxX = bounds[3]; - int boxMaxZ = bounds[5]; + int margin = Math.max(1, radius); + int head = Math.max(0, ceiling); + int floorCut = Math.max(0, floorDepth); + int mantleOffset = getEngineMantle().getEngine().getMinHeight(); int worldMin = getEngineMantle().getEngine().getMinHeight() + 1; int worldMax = getEngineMantle().getEngine().getMinHeight() + getEngineMantle().getEngine().getHeight() - 1; - int floorY = Math.max(worldMin, bounds[1] - Math.max(0, floorDepth)); - int apexY = Math.min(worldMax, bounds[4] + Math.max(0, ceiling)); - if (apexY < floorY) { - return; - } - int expMinX = boxMinX - r; - int expMaxX = boxMaxX + r; - int expMinZ = boxMinZ - r; - int expMaxZ = boxMaxZ + r; - long volume = (long) (expMaxX - expMinX + 1) * (long) (apexY - floorY + 1) * (long) (expMaxZ - expMinZ + 1); - if (volume > MAX_OVERBORE_VOLUME) { - Iris.warn("Skipping structure overbore of " + volume + " blocks (cap " + MAX_OVERBORE_VOLUME + "); reduce overboreRadius or use larger spacing."); - return; - } - int span = apexY - floorY; - double rr = r <= 0 ? 1.0 : (double) r; - RNG noiseRng = new RNG(seed() + Cache.key(boxMinX, boxMinZ)); - CNG outlineNoise = CNG.signature(noiseRng); - CNG ceilNoise = CNG.signature(noiseRng.nextParallelRNG(0x51E10)); - CNG floorNoise = CNG.signature(noiseRng.nextParallelRNG(0xF1009)); - double outlineFreq = 0.045; - double ceilFreq = 0.055; - double floorFreq = 0.07; - int floorAmp = Math.min(5, Math.max(1, span / 8)); - double ceilLump = Math.min(4.0, span * 0.15); + double freq = 0.07; + double rollFreq = 0.03; + double clearMax = 1.45; + double reachSide = margin; + double reachUp = head < 1 ? 1.0 : head; + double reachDown = floorCut < 1 ? 1.0 : floorCut; + double upReachMin = 0.4; + double upReachSpan = 1.4; + int sideExt = (int) Math.ceil(reachSide * clearMax); + int upExt = (int) Math.ceil(reachUp * (upReachMin + upReachSpan) * clearMax); + + long work = 0L; + for (PlacedStructurePiece p : pieces) { + long wx = (long) (p.getMaxX() - p.getMinX() + 1) + 2L * sideExt; + long wz = (long) (p.getMaxZ() - p.getMinZ() + 1) + 2L * sideExt; + long wy = (long) (p.getMaxY() - p.getMinY() + 1) + upExt + floorCut; + work += wx * wy * wz; + } + if (work > MAX_OVERBORE_VOLUME) { + Iris.warn("Skipping structure overbore of " + work + " blocks (cap " + MAX_OVERBORE_VOLUME + "); reduce overboreRadius/overboreHeight or use larger spacing."); + return; + } + + RNG noiseRng = new RNG(seed() + Cache.key(bounds[0], bounds[2])); + CNG blob = CNG.signature(noiseRng); + CNG roll = CNG.signature(noiseRng.nextParallelRNG(0x2A17)); if (IrisSettings.get().getGeneral().isDebug()) { - Iris.info("Overbore carving cavern: box=[" + boxMinX + "," + floorY + "," + boxMinZ + " -> " + boxMaxX + "," + apexY + "," + boxMaxZ + "] radius=" + r + " volume=" + volume); + Iris.info("Overbore carving organic cavern: pieces=" + pieces.size() + " margin=" + margin + " head=" + head + " floorCut=" + floorCut + " work=" + work); } - BlockData air = CARVE_AIR; - for (int bx = expMinX; bx <= expMaxX; bx++) { - int dx = bx < boxMinX ? boxMinX - bx : bx > boxMaxX ? bx - boxMaxX : 0; - for (int bz = expMinZ; bz <= expMaxZ; bz++) { - int dz = bz < boxMinZ ? boxMinZ - bz : bz > boxMaxZ ? bz - boxMaxZ : 0; - int floorYcol = floorY + (int) Math.round(floorNoise.fitDouble(-1.0, 1.0, bx * floorFreq, bz * floorFreq) * floorAmp); - if (floorYcol < worldMin) { - floorYcol = worldMin; - } - int columnCeil; - if (dx == 0 && dz == 0) { - int bump = (int) Math.round(ceilNoise.fitDouble(0.0, 1.0, bx * ceilFreq, bz * ceilFreq) * ceilLump); - columnCeil = Math.min(worldMax, apexY - Math.max(0, bump)); - } else { - double dist = Math.sqrt((double) dx * dx + (double) dz * dz); - double outline = outlineNoise.fitDouble(-1.0, 1.0, bx * outlineFreq, bz * outlineFreq); - double rEff = rr * (0.80 + 0.35 * outline); - if (rEff < 1.0) { - rEff = 1.0; + for (PlacedStructurePiece p : pieces) { + int pMinX = p.getMinX(); + int pMinY = p.getMinY(); + int pMinZ = p.getMinZ(); + int pMaxX = p.getMaxX(); + int pMaxY = p.getMaxY(); + int pMaxZ = p.getMaxZ(); + int exMinX = pMinX - sideExt; + int exMaxX = pMaxX + sideExt; + int exMinZ = pMinZ - sideExt; + int exMaxZ = pMaxZ + sideExt; + int exMinY = Math.max(worldMin, pMinY - floorCut); + int exMaxY = Math.min(worldMax, pMaxY + upExt); + for (int bx = exMinX; bx <= exMaxX; bx++) { + double dx = bx < pMinX ? pMinX - bx : bx > pMaxX ? bx - pMaxX : 0; + double nx = dx / reachSide; + for (int bz = exMinZ; bz <= exMaxZ; bz++) { + double dz = bz < pMinZ ? pMinZ - bz : bz > pMaxZ ? bz - pMaxZ : 0; + double nz = dz / reachSide; + double nxz = nx * nx + nz * nz; + double w = roll.fitDouble(0.0, 1.0, bx * rollFreq, bz * rollFreq) * 0.7 + + roll.fitDouble(0.0, 1.0, bx * rollFreq * 3.0, bz * rollFreq * 3.0) * 0.3; + double contrast = (w - 0.5) * 2.6 + 0.5; + if (contrast < 0.0) { + contrast = 0.0; + } else if (contrast > 1.0) { + contrast = 1.0; } - if (dist > rEff) { - continue; + double upReach = reachUp * (upReachMin + upReachSpan * contrast); + if (upReach < 1.0) { + upReach = 1.0; } - double t = dist / rEff; - double dome = Math.sqrt(Math.max(0.0, 1.0 - t * t)); - double lump = ceilNoise.fitDouble(-1.0, 1.0, bx * ceilFreq, bz * ceilFreq); - double mix = dome * (0.80 + 0.40 * lump); - if (mix < 0.0) { - mix = 0.0; + for (int by = exMinY; by <= exMaxY; by++) { + double ny; + if (by > pMaxY) { + ny = (by - pMaxY) / upReach; + } else if (by < pMinY) { + ny = (pMinY - by) / reachDown; + } else { + ny = 0.0; + } + double nd = Math.sqrt(nxz + ny * ny); + if (nd > clearMax) { + continue; + } + boolean carve = nd <= 0.45; + if (!carve) { + double n = blob.fitDouble(0.0, 1.0, bx * freq, by * freq, bz * freq); + carve = nd <= 0.5 + 0.5 * n; + } + writer.clearBlock(bx, by - mantleOffset, bz); + if (carve) { + writer.setDataIfAbsent(bx, by - mantleOffset, bz, CARVE_CAVERN); + } } - columnCeil = floorYcol + (int) Math.round(span * mix); - if (columnCeil <= floorYcol) { - continue; - } - columnCeil = Math.min(worldMax, columnCeil); - } - for (int by = floorYcol; by <= columnCeil; by++) { - writer.set(bx, by, bz, air); } } } @@ -299,7 +342,11 @@ public class IrisStructureComponent extends IrisMantleComponent { if (!structure.getEdit().isEmpty()) { config.setEdit(structure.getEdit()); } - object.place(p.getX(), y, p.getZ(), writer, config, rng, null, null, getData()); + if (mode != ObjectPlaceMode.STRUCTURE_PIECE && mode != ObjectPlaceMode.FLOATING) { + config.setForcePlace(true); + } + int placeY = (y == -1) ? -1 : y - getEngineMantle().getEngine().getMinHeight(); + object.place(p.getX(), placeY, p.getZ(), writer, config, rng, null, null, getData()); } @Override diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java index 7b8097361..5abe33e7a 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java @@ -62,6 +62,7 @@ import java.util.concurrent.atomic.AtomicLong; @ComponentFlag(ReservedFlag.OBJECT) public class MantleObjectComponent extends IrisMantleComponent { private static final long CAVE_REJECT_LOG_THROTTLE_MS = 5000L; + private static final int BEDROCK_CLEARANCE = 6; private static final int SURFACE_HEIGHT_CHUNK_FILL_THRESHOLD = 128; private static final Map CAVE_REJECT_LOG_STATE = new ConcurrentHashMap<>(); private static final Set MISSING_LOAD_KEY_WARNED = ConcurrentHashMap.newKeySet(); @@ -258,7 +259,7 @@ public class MantleObjectComponent extends IrisMantleComponent { if (chance) { biomeCaveTriggered++; try { - ObjectPlacementResult result = placeCaveObject(writer, rng, x, z, i, biomeCaveProfile, complex, traceRegen, x, z, "biome-cave"); + ObjectPlacementResult result = placeCaveObject(writer, rng, x, z, i, biomeCaveProfile, complex, traceRegen, x, z, "biome-cave", caveBiome.getLoadKey()); attempts += result.attempts(); placed += result.placed(); rejected += result.rejected(); @@ -323,7 +324,7 @@ public class MantleObjectComponent extends IrisMantleComponent { if (chance) { regionCaveTriggered++; try { - ObjectPlacementResult result = placeCaveObject(writer, rng, x, z, i, regionCaveProfile, complex, traceRegen, x, z, "region-cave"); + ObjectPlacementResult result = placeCaveObject(writer, rng, x, z, i, regionCaveProfile, complex, traceRegen, x, z, "region-cave", null); attempts += result.attempts(); placed += result.placed(); rejected += result.rejected(); @@ -484,6 +485,26 @@ public class MantleObjectComponent extends IrisMantleComponent { } @ChunkCoordinates + private boolean caveAnchorBiomeConflicts(int x, int y, int z, String expectedCaveBiomeKey) { + if (expectedCaveBiomeKey == null) { + return false; + } + Engine engine = getEngineMantle().getEngine(); + IrisBiome at = engine.getCaveBiome(x, y, z); + if (at == null) { + return false; + } + String atKey = at.getLoadKey(); + if (atKey == null || atKey.equals(expectedCaveBiomeKey)) { + return false; + } + IrisBiome surface = engine.getSurfaceBiome(x, z); + if (surface != null && atKey.equals(surface.getLoadKey())) { + return false; + } + return true; + } + private ObjectPlacementResult placeCaveObject( MantleWriter writer, RNG rng, @@ -495,7 +516,8 @@ public class MantleObjectComponent extends IrisMantleComponent { boolean traceRegen, int metricChunkX, int metricChunkZ, - String scope + String scope, + String expectedCaveBiomeKey ) { int attempts = 0; int placed = 0; @@ -556,6 +578,10 @@ public class MantleObjectComponent extends IrisMantleComponent { continue; } + if (caveAnchorBiomeConflicts(candidateX, candidateY, candidateZ, expectedCaveBiomeKey)) { + continue; + } + x = candidateX; z = candidateZ; y = candidateY; @@ -1053,7 +1079,7 @@ public class MantleObjectComponent extends IrisMantleComponent { private KList scanCaveAnchorRange(MantleWriter writer, IrisCaveAnchorMode anchorMode, int step, int x, int z, int height, int maxAnchorY) { KList anchors = new KList<>(); - for (int y = 1; y < maxAnchorY; y += step) { + for (int y = BEDROCK_CLEARANCE; y < maxAnchorY; y += step) { if (!writer.isCarved(x, y, z)) { continue; } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java index 119a0862d..afcbab6d9 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java @@ -321,7 +321,12 @@ public class IrisObject extends IrisRegistrant { int s = din.readInt(); for (int i = 0; i < s; i++) { - blocks.put(new Vector3i(din.readShort(), din.readShort(), din.readShort()), B.get(din.readUTF())); + Vector3i pos = new Vector3i(din.readShort(), din.readShort(), din.readShort()); + BlockData data = B.get(din.readUTF()); + if (isStructureMarker(data)) { + continue; + } + blocks.put(pos, data); } if (din.available() == 0) @@ -358,7 +363,12 @@ public class IrisObject extends IrisRegistrant { s = din.readInt(); for (i = 0; i < s; i++) { - blocks.put(new Vector3i(din.readShort(), din.readShort(), din.readShort()), B.get(palette.get(din.readShort()))); + Vector3i pos = new Vector3i(din.readShort(), din.readShort(), din.readShort()); + BlockData data = B.get(palette.get(din.readShort())); + if (isStructureMarker(data)) { + continue; + } + blocks.put(pos, data); } s = din.readInt(); @@ -368,6 +378,14 @@ public class IrisObject extends IrisRegistrant { } } + private static boolean isStructureMarker(BlockData data) { + if (data == null) { + return false; + } + Material m = data.getMaterial(); + return m == Material.JIGSAW || m == Material.STRUCTURE_BLOCK || m == Material.STRUCTURE_VOID; + } + public void write(OutputStream o) throws IOException { DataOutputStream dos = new DataOutputStream(o); dos.writeInt(w); 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 e73b8e3f3..e6e0b774a 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 @@ -96,6 +96,10 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun private final AtomicBoolean setup; private final boolean studio; private final AtomicInteger a = new AtomicInteger(0); + private final AtomicBoolean firstChunkLogged = new AtomicBoolean(false); + private volatile long firstChunkTime = 0L; + private final AtomicInteger chunkGenCount = new AtomicInteger(0); + private volatile long lastChunkGenTime = 0L; private final CompletableFuture spawnChunks = new CompletableFuture<>(); private final AtomicCache targetCache = new AtomicCache<>(); private final AtomicReference> closeFuture = new AtomicReference<>(); @@ -308,7 +312,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun setChunkReplacementPhase(phaseRef, effectiveListener, "generate", x, z); long generateStart = System.currentTimeMillis(); - boolean useMulticore = IrisSettings.get().getGenerator().useMulticore && !J.isFolia(); + boolean useMulticore = false; AtomicBoolean generateDone = new AtomicBoolean(false); AtomicLong generationWatchdogStart = new AtomicLong(System.currentTimeMillis()); Thread generateThread = Thread.currentThread(); @@ -699,6 +703,18 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun try { Engine engine = getEngine(world); + lastChunkGenTime = System.currentTimeMillis(); + if (firstChunkLogged.compareAndSet(false, true)) { + firstChunkTime = System.currentTimeMillis(); + Iris.info("[Studio timing] FIRST chunk generating in " + world.getName() + " at chunk " + x + "," + z + " (player-view terrain starts here — server-side open prep already done)"); + } + if (studio) { + int gen = chunkGenCount.incrementAndGet(); + if (gen % 64 == 0) { + long el = Math.max(1L, System.currentTimeMillis() - firstChunkTime); + Iris.info("[Studio timing] player-view terrain: " + gen + " chunks in " + el + "ms (" + (gen * 1000L / el) + " chunks/sec) — THIS is the wait you actually see"); + } + } computeStudioGenerator(); TerrainChunk tc = TerrainChunk.create(d); this.world.bind(world); @@ -707,7 +723,8 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun } else { ChunkDataHunkHolder blocks = new ChunkDataHunkHolder(d); Hunk biomes = Hunk.viewBiomes(tc); - engine.generate(x << 4, z << 4, blocks, biomes, IrisSettings.get().getGenerator().useMulticore); + boolean useMulticore = studio && !J.isFolia(); + engine.generate(x << 4, z << 4, blocks, biomes, useMulticore); blocks.apply(); } @@ -763,6 +780,10 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun return true; } + if (System.currentTimeMillis() - lastChunkGenTime < 2000L) { + return true; + } + World realWorld = this.world.realWorld(); PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance(); return realWorld != null && pregeneratorJob != null && pregeneratorJob.targetsWorld(realWorld); diff --git a/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/StructureHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/StructureHandler.java new file mode 100644 index 000000000..bf302109b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/StructureHandler.java @@ -0,0 +1,89 @@ +/* + * 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.specialhandlers; + +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.IrisStructureLocator; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.iris.util.common.director.DirectorParameterHandler; +import art.arcane.volmlib.util.director.exceptions.DirectorParsingException; + +import java.util.stream.Collectors; + +public class StructureHandler implements DirectorParameterHandler { + @Override + public KList getPossibilities() { + KList keys = new KList<>(); + + try { + for (String k : INMS.get().getStructureKeys()) { + if (k != null && !k.isEmpty()) { + keys.addIfMissing(k); + } + } + } catch (Throwable ignored) { + } + + try { + Engine e = engine(); + if (e != null) { + for (String k : IrisStructureLocator.placedKeys(e)) { + if (k != null && !k.isEmpty()) { + keys.addIfMissing(k); + } + } + } + } catch (Throwable ignored) { + } + + return keys; + } + + @Override + public String toString(String structure) { + return structure; + } + + @Override + public String parse(String in, boolean force) throws DirectorParsingException { + KList options = getPossibilities(in); + + if (options.isEmpty()) { + return in; + } + try { + return options.stream().filter((i) -> toString(i).equalsIgnoreCase(in)).collect(Collectors.toList()).get(0); + } catch (Throwable e) { + return in; + } + } + + @Override + public boolean supports(Class type) { + return type.equals(String.class); + } + + @Override + public String getRandomDefault() { + String f = getPossibilities().getRandom(); + + return f == null ? "minecraft_ancient_city" : f; + } +} diff --git a/core/src/test/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethodConcurrencyCapTest.java b/core/src/test/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethodConcurrencyCapTest.java index 64c1dde72..bc331216c 100644 --- a/core/src/test/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethodConcurrencyCapTest.java +++ b/core/src/test/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethodConcurrencyCapTest.java @@ -21,15 +21,6 @@ public class AsyncPregenMethodConcurrencyCapTest { assertEquals(192, AsyncPregenMethod.computeFoliaRecommendedCap(80)); } - @Test - public void runtimeCapUsesGlobalCeilingAndWorkerRecommendation() { - assertEquals(80, AsyncPregenMethod.applyRuntimeConcurrencyCap(256, true, 20)); - assertEquals(12, AsyncPregenMethod.applyRuntimeConcurrencyCap(12, true, 20)); - assertEquals(64, AsyncPregenMethod.applyRuntimeConcurrencyCap(256, true, 8)); - assertEquals(16, AsyncPregenMethod.applyRuntimeConcurrencyCap(256, false, 8)); - assertEquals(20, AsyncPregenMethod.applyRuntimeConcurrencyCap(20, false, 40)); - } - @Test public void paperLikeConcurrencyProvisionsForWorldGenThreadBump() { assertEquals(32, AsyncPregenMethod.resolvePaperLikeConcurrencyWorkerThreads(4, 16, 32)); 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 8f9c0bf93..9eb4e793a 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 @@ -31,6 +31,7 @@ import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; import net.minecraft.core.registries.Registries; import java.util.stream.Collectors; +import art.arcane.iris.engine.framework.IrisStructureLocator; import art.arcane.iris.engine.framework.StructurePlacementGrid; import art.arcane.iris.engine.object.IrisStructurePlacement; import art.arcane.iris.engine.object.StructurePlacementRoute; @@ -66,10 +67,43 @@ public class IrisChunkGenerator extends CustomChunkGenerator { @Override public @Nullable Pair> findNearestMapStructure(ServerLevel level, HolderSet holders, BlockPos pos, int radius, boolean findUnexplored) { + try { + Registry registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE); + BlockPos best = null; + Holder bestHolder = null; + long bestDist = Long.MAX_VALUE; + for (Holder holder : holders) { + Object id = registry.getKey(holder.value()); + if (id == null) { + continue; + } + int[] at = IrisStructureLocator.locate(engine, id.toString(), pos.getX(), pos.getZ(), Math.max(1, radius)); + if (at == null) { + continue; + } + long dx = (long) at[0] - pos.getX(); + long dz = (long) at[2] - pos.getZ(); + long d = dx * dx + dz * dz; + if (d < bestDist) { + bestDist = d; + best = new BlockPos(at[0], at[1], at[2]); + bestHolder = holder; + } + } + if (best != null) { + return Pair.of(best, bestHolder); + } + } catch (Throwable e) { + Iris.reportError(e); + } if (!vanillaControl().active()) { return null; } - return delegate.findNearestMapStructure(level, holders, pos, radius, findUnexplored); + try { + return delegate.findNearestMapStructure(level, holders, pos, radius, findUnexplored); + } catch (Throwable e) { + return null; + } } @Override 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 b22f47e18..2e63a70fd 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 @@ -31,6 +31,7 @@ import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; import net.minecraft.core.registries.Registries; import java.util.stream.Collectors; +import art.arcane.iris.engine.framework.IrisStructureLocator; import art.arcane.iris.engine.framework.StructurePlacementGrid; import art.arcane.iris.engine.object.IrisStructurePlacement; import art.arcane.iris.engine.object.StructurePlacementRoute; @@ -66,10 +67,43 @@ public class IrisChunkGenerator extends CustomChunkGenerator { @Override public @Nullable Pair> findNearestMapStructure(ServerLevel level, HolderSet holders, BlockPos pos, int radius, boolean findUnexplored) { + try { + Registry registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE); + BlockPos best = null; + Holder bestHolder = null; + long bestDist = Long.MAX_VALUE; + for (Holder holder : holders) { + Object id = registry.getKey(holder.value()); + if (id == null) { + continue; + } + int[] at = IrisStructureLocator.locate(engine, id.toString(), pos.getX(), pos.getZ(), Math.max(1, radius)); + if (at == null) { + continue; + } + long dx = (long) at[0] - pos.getX(); + long dz = (long) at[2] - pos.getZ(); + long d = dx * dx + dz * dz; + if (d < bestDist) { + bestDist = d; + best = new BlockPos(at[0], at[1], at[2]); + bestHolder = holder; + } + } + if (best != null) { + return Pair.of(best, bestHolder); + } + } catch (Throwable e) { + Iris.reportError(e); + } if (!vanillaControl().active()) { return null; } - return delegate.findNearestMapStructure(level, holders, pos, radius, findUnexplored); + try { + return delegate.findNearestMapStructure(level, holders, pos, radius, findUnexplored); + } catch (Throwable e) { + return null; + } } @Override