Good points

This commit is contained in:
Brian Neumann-Fopiano
2026-05-31 07:51:24 -04:00
parent 7eb6e87a81
commit 540a416bc9
30 changed files with 1189 additions and 317 deletions
@@ -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
@@ -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());
@@ -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)
@@ -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<String> 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<IrisStructurePlacement> 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;
}
}
@@ -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(() -> {
@@ -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,
@@ -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;
@@ -206,11 +206,14 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D
}
private List<String> 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();
}
}
@@ -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<String> templateKeys;
try {
@@ -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<File> 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<String> 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<File> 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<String, Object> poolJsonMulti(List<String> pieceNames) {
List<Object> pieces = new ArrayList<>();
for (String name : pieceNames) {
Map<String, Object> entry = new LinkedHashMap<>();
entry.put("piece", name);
entry.put("weight", 1);
pieces.add(entry);
}
Map<String, Object> pool = new LinkedHashMap<>();
pool.put("pieces", pieces);
return pool;
}
private static Map<String, Object> pieceJson(String name) {
Map<String, Object> piece = new LinkedHashMap<>();
piece.put("object", name);
@@ -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);
@@ -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");
}
@@ -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<GridBoundsCache> 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<IrisGenerator> 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);
}
@@ -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!");
@@ -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<Matter> 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<Future<Chunk>> 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()));
}
}
@@ -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<IrisObjectPlacement> placements, String normalizedObjectKey) {
if (placements == null || placements.isEmpty() || normalizedObjectKey.isBlank()) {
return false;
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<IrisData, PlacementIndex> 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<String> 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<int[]> 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<int[]> collectRandomSpreadParams(Engine engine, String key) {
IrisData data = engine.getData();
List<int[]> params = new ArrayList<>();
Set<Long> seen = new LinkedHashSet<>();
KList<IrisStructurePlacement> 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<IrisStructurePlacement> 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<IrisStructurePlacement> 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<String> loadKeys = new LinkedHashSet<>();
Set<String> 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<IrisStructurePlacement> placements, IrisData data, Set<String> loadKeys, Set<String> 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<String> loadKeys;
private final Set<String> vanillaSources;
private PlacementIndex(Set<String> loadKeys, Set<String> vanillaSources) {
this.loadKeys = loadKeys;
this.vanillaSources = vanillaSources;
}
}
}
@@ -77,6 +77,10 @@ public interface Locator<T> {
return (e, c) -> e.getSurfaceBiome((c.getX() << 4) + 8, (c.getZ() << 4) + 8).getLoadKey().equals(loadKey);
}
static Locator<art.arcane.iris.engine.object.IrisStructure> structure(String key) {
return (e, c) -> IrisStructureLocator.startsInChunk(e, key, c.getX(), c.getZ());
}
static Locator<BlockPos> poi(String type) {
return (e, c) -> {
Set<Pair<String, BlockPos>> pos = e.getPOIsAt((c.getX() << 4) + 8, (c.getZ() << 4) + 8);
@@ -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) {
@@ -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<Matter> 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.<BlockData>slice(BlockData.class).set(x & 15, y & 15, z & 15, null);
}
public <T> T getData(int x, int y, int z, Class<T> type) {
int cx = x >> 4;
int cz = z >> 4;
@@ -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();
@@ -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);
@@ -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<PlacedStructurePiece> 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
@@ -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<String, CaveRejectLogState> CAVE_REJECT_LOG_STATE = new ConcurrentHashMap<>();
private static final Set<String> 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<Integer> scanCaveAnchorRange(MantleWriter writer, IrisCaveAnchorMode anchorMode, int step, int x, int z, int height, int maxAnchorY) {
KList<Integer> 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;
}
@@ -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);
@@ -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<Integer> spawnChunks = new CompletableFuture<>();
private final AtomicCache<EngineTarget> targetCache = new AtomicCache<>();
private final AtomicReference<CompletableFuture<Void>> 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<Biome> 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);
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<String> {
@Override
public KList<String> getPossibilities() {
KList<String> 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<String> 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;
}
}
@@ -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));
@@ -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<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel level, HolderSet<Structure> holders, BlockPos pos, int radius, boolean findUnexplored) {
try {
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
BlockPos best = null;
Holder<Structure> bestHolder = null;
long bestDist = Long.MAX_VALUE;
for (Holder<Structure> 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
@@ -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<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel level, HolderSet<Structure> holders, BlockPos pos, int radius, boolean findUnexplored) {
try {
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
BlockPos best = null;
Holder<Structure> bestHolder = null;
long bestDist = Long.MAX_VALUE;
for (Holder<Structure> 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