This commit is contained in:
Brian Neumann-Fopiano
2026-04-23 16:38:32 -04:00
parent 3d128b70a7
commit ce29b70618
32 changed files with 1204 additions and 248 deletions
+1 -1
View File
@@ -1 +1 @@
-1935789196 1435163759
@@ -42,7 +42,14 @@ import org.jetbrains.annotations.Nullable;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicIntegerArray; import java.util.concurrent.atomic.AtomicIntegerArray;
@@ -172,6 +179,70 @@ public class ServerConfigurator {
return fullInstall && verifyDataPacksPost(IrisSettings.get().getAutoConfiguration().isAutoRestartOnCustomBiomeInstall()); return fullInstall && verifyDataPacksPost(IrisSettings.get().getAutoConfiguration().isAutoRestartOnCustomBiomeInstall());
} }
public static boolean installDataPacksIfChanged(boolean fullInstall) {
File packsDir = Iris.instance.getDataFolder("packs");
String current = computePackFingerprint(packsDir);
File cacheFile = new File(Iris.instance.getDataFolder("cache"), "datapack-fingerprint");
String cached = "";
if (cacheFile.exists()) {
try {
cached = Files.readString(cacheFile.toPath(), StandardCharsets.UTF_8).trim();
} catch (IOException e) {
cached = "";
}
}
if (!current.isEmpty() && current.equals(cached)) {
Iris.verbose("Data packs unchanged, skipping install.");
return false;
}
boolean result = installDataPacks(fullInstall);
try {
cacheFile.getParentFile().mkdirs();
Files.writeString(cacheFile.toPath(), current, StandardCharsets.UTF_8);
} catch (IOException e) {
Iris.warn("Failed to write datapack fingerprint cache: " + e.getMessage());
}
return result;
}
public static String computePackFingerprint(File packsDir) {
if (packsDir == null || !packsDir.isDirectory()) {
return "";
}
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
List<String> entries = new ArrayList<>();
collectFingerprintEntries(packsDir, packsDir.getAbsolutePath(), entries);
Collections.sort(entries);
for (String entry : entries) {
digest.update(entry.getBytes(StandardCharsets.UTF_8));
}
byte[] hash = digest.digest();
StringBuilder sb = new StringBuilder(hash.length * 2);
for (byte b : hash) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("SHA-256 not available", e);
}
}
private static void collectFingerprintEntries(File dir, String rootPath, List<String> entries) {
File[] files = dir.listFiles();
if (files == null) {
return;
}
for (File file : files) {
if (file.isDirectory()) {
collectFingerprintEntries(file, rootPath, entries);
} else {
String relative = file.getAbsolutePath().substring(rootPath.length());
entries.add(relative + "|" + file.length() + "|" + file.lastModified());
}
}
}
private static boolean shouldDeferInstallUntilWorldsReady() { private static boolean shouldDeferInstallUntilWorldsReady() {
String forcedMainWorld = IrisSettings.get().getGeneral().forceMainWorld; String forcedMainWorld = IrisSettings.get().getGeneral().forceMainWorld;
if (forcedMainWorld != null && !forcedMainWorld.isBlank()) { if (forcedMainWorld != null && !forcedMainWorld.isBlank()) {
@@ -47,14 +47,14 @@ final class PaperLikeRuntimeBackend implements WorldLifecycleBackend {
if (capabilities.paperLikeFlavor() == CapabilitySnapshot.PaperLikeFlavor.CURRENT_INFO_AND_DATA) { if (capabilities.paperLikeFlavor() == CapabilitySnapshot.PaperLikeFlavor.CURRENT_INFO_AND_DATA) {
Object dimensionKey = WorldLifecycleSupport.createDimensionKey(stemKey); Object dimensionKey = WorldLifecycleSupport.createDimensionKey(stemKey);
Object loadedWorldData = capabilities.paperWorldDataMethod().invoke(null, capabilities.minecraftServer(), dimensionKey, request.worldName()); Object loadedWorldData = capabilities.paperWorldDataMethod().invoke(null, capabilities.minecraftServer(), dimensionKey, request.worldName());
Object worldLoadingInfo = capabilities.worldLoadingInfoConstructor().newInstance(request.environment(), stemKey, dimensionKey, true); Object worldLoadingInfo = capabilities.worldLoadingInfoConstructor().newInstance(request.environment(), stemKey, dimensionKey, !request.studio());
Object worldLoadingInfoAndData = capabilities.worldLoadingInfoAndDataConstructor().newInstance(worldLoadingInfo, loadedWorldData); Object worldLoadingInfoAndData = capabilities.worldLoadingInfoAndDataConstructor().newInstance(worldLoadingInfo, loadedWorldData);
Object worldDataAndGenSettings = WorldLifecycleSupport.createCurrentWorldDataAndSettings(capabilities, request.worldName()); Object worldDataAndGenSettings = WorldLifecycleSupport.createCurrentWorldDataAndSettings(capabilities, request.worldName());
capabilities.createLevelMethod().invoke(capabilities.minecraftServer(), levelStem, worldLoadingInfoAndData, worldDataAndGenSettings); capabilities.createLevelMethod().invoke(capabilities.minecraftServer(), levelStem, worldLoadingInfoAndData, worldDataAndGenSettings);
} else { } else {
legacyStorageAccess = WorldLifecycleSupport.createLegacyStorageAccess(capabilities, request.worldName()); legacyStorageAccess = WorldLifecycleSupport.createLegacyStorageAccess(capabilities, request.worldName());
Object primaryLevelData = WorldLifecycleSupport.createLegacyPrimaryLevelData(capabilities, legacyStorageAccess, request.worldName()); Object primaryLevelData = WorldLifecycleSupport.createLegacyPrimaryLevelData(capabilities, legacyStorageAccess, request.worldName());
Object worldLoadingInfo = capabilities.worldLoadingInfoConstructor().newInstance(0, request.worldName(), request.environment().name().toLowerCase(Locale.ROOT), stemKey, true); Object worldLoadingInfo = capabilities.worldLoadingInfoConstructor().newInstance(0, request.worldName(), request.environment().name().toLowerCase(Locale.ROOT), stemKey, !request.studio());
capabilities.createLevelMethod().invoke(capabilities.minecraftServer(), levelStem, worldLoadingInfo, legacyStorageAccess, primaryLevelData); capabilities.createLevelMethod().invoke(capabilities.minecraftServer(), levelStem, worldLoadingInfo, legacyStorageAccess, primaryLevelData);
} }
@@ -118,14 +118,16 @@ public final class StudioOpenCoordinator {
throw new IllegalStateException("Studio entry anchor could not be resolved."); throw new IllegalStateException("Studio entry anchor could not be resolved.");
} }
long deadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(120L); updateStage(request, "resolve_safe_entry", 0.84D);
updateStage(request, "request_entry_chunk", 0.84D); Location safeEntry;
requestEntryChunk(world, entryAnchor, deadline); try {
safeEntry = WorldRuntimeControlService.get().resolveSafeEntry(world, entryAnchor)
updateStage(request, "resolve_safe_entry", 0.90D); .get(5L, TimeUnit.SECONDS);
Location safeEntry = resolveSafeEntry(world, entryAnchor, deadline); } catch (TimeoutException e) {
throw new IllegalStateException("Studio entry point resolution timed out — region thread may be stalled.");
}
if (safeEntry == null) { if (safeEntry == null) {
throw new IllegalStateException("Studio safe entry resolution timed out."); throw new IllegalStateException("Studio entry point could not be resolved for world \"" + request.worldName() + "\".");
} }
if (request.playerName() != null && !request.playerName().isBlank()) { if (request.playerName() != null && !request.playerName().isBlank()) {
@@ -135,8 +137,7 @@ public final class StudioOpenCoordinator {
throw new IllegalStateException("Player \"" + request.playerName() + "\" is not online."); throw new IllegalStateException("Player \"" + request.playerName() + "\" is not online.");
} }
long remaining = Math.max(1000L, deadline - System.currentTimeMillis()); Boolean teleported = WorldRuntimeControlService.get().teleport(player, safeEntry).get(10L, TimeUnit.SECONDS);
Boolean teleported = WorldRuntimeControlService.get().teleport(player, safeEntry).get(remaining, TimeUnit.MILLISECONDS);
if (!Boolean.TRUE.equals(teleported)) { if (!Boolean.TRUE.equals(teleported)) {
throw new IllegalStateException("Studio teleport did not complete successfully."); throw new IllegalStateException("Studio teleport did not complete successfully.");
} }
@@ -168,18 +169,6 @@ public final class StudioOpenCoordinator {
} }
} }
private void requestEntryChunk(World world, Location entryAnchor, long deadline) throws Exception {
int chunkX = entryAnchor.getBlockX() >> 4;
int chunkZ = entryAnchor.getBlockZ() >> 4;
long remaining = Math.max(1000L, deadline - System.currentTimeMillis());
waitForEntryChunk(world, chunkX, chunkZ, deadline, null).get(remaining, TimeUnit.MILLISECONDS);
}
private Location resolveSafeEntry(World world, Location entryAnchor, long deadline) throws Exception {
long remaining = Math.max(1000L, deadline - System.currentTimeMillis());
return waitForSafeEntry(world, entryAnchor, deadline, null).get(remaining, TimeUnit.MILLISECONDS);
}
private StudioCloseResult closeWorld( private StudioCloseResult closeWorld(
PlatformChunkGenerator provider, PlatformChunkGenerator provider,
String worldName, String worldName,
@@ -361,58 +350,6 @@ public final class StudioOpenCoordinator {
}; };
} }
private CompletableFuture<Void> waitForEntryChunk(World world, int chunkX, int chunkZ, long deadline, Throwable lastFailure) {
long now = System.currentTimeMillis();
if (now >= deadline) {
return CompletableFuture.failedFuture(timeoutFailure("Studio entry chunk request timed out.", lastFailure));
}
long attemptTimeout = Math.min(Math.max(1000L, deadline - now), 3000L);
CompletableFuture<org.bukkit.Chunk> request = withAttemptTimeout(
WorldRuntimeControlService.get().requestChunkAsync(world, chunkX, chunkZ, true),
attemptTimeout,
"Studio entry chunk request attempt timed out."
);
return request.handle((chunk, throwable) -> {
if (throwable == null && world.isChunkLoaded(chunkX, chunkZ)) {
return CompletableFuture.<Void>completedFuture(null);
}
Throwable nextFailure = throwable == null ? lastFailure : unwrapFailure(throwable);
if (System.currentTimeMillis() >= deadline) {
return CompletableFuture.<Void>failedFuture(timeoutFailure("Studio entry chunk request timed out.", nextFailure));
}
return delayFuture(1000L).thenCompose(ignored -> waitForEntryChunk(world, chunkX, chunkZ, deadline, nextFailure));
}).thenCompose(next -> next);
}
private CompletableFuture<Location> waitForSafeEntry(World world, Location entryAnchor, long deadline, Throwable lastFailure) {
long now = System.currentTimeMillis();
if (now >= deadline) {
return CompletableFuture.failedFuture(timeoutFailure("Studio safe-entry resolution timed out.", lastFailure));
}
long attemptTimeout = Math.min(Math.max(1000L, deadline - now), 3000L);
CompletableFuture<Location> resolve = withAttemptTimeout(
WorldRuntimeControlService.get().resolveSafeEntry(world, entryAnchor),
attemptTimeout,
"Studio safe-entry resolution attempt timed out."
);
return resolve.handle((location, throwable) -> {
if (throwable == null && location != null) {
return CompletableFuture.completedFuture(location);
}
Throwable nextFailure = throwable == null ? lastFailure : unwrapFailure(throwable);
if (System.currentTimeMillis() >= deadline) {
return CompletableFuture.<Location>failedFuture(timeoutFailure("Studio safe-entry resolution timed out.", nextFailure));
}
return delayFuture(250L).thenCompose(ignored -> waitForSafeEntry(world, entryAnchor, deadline, nextFailure));
}).thenCompose(next -> next);
}
private CompletableFuture<Void> waitForWorldFamilyUnload(String worldName, long deadline) { private CompletableFuture<Void> waitForWorldFamilyUnload(String worldName, long deadline) {
if (worldName == null || !isWorldFamilyLoaded(worldName) || System.currentTimeMillis() >= deadline) { if (worldName == null || !isWorldFamilyLoaded(worldName) || System.currentTimeMillis() >= deadline) {
return CompletableFuture.completedFuture(null); return CompletableFuture.completedFuture(null);
@@ -444,32 +381,6 @@ public final class StudioOpenCoordinator {
}, CompletableFuture.delayedExecutor(safeDelay, TimeUnit.MILLISECONDS)); }, CompletableFuture.delayedExecutor(safeDelay, TimeUnit.MILLISECONDS));
} }
private <T> CompletableFuture<T> withAttemptTimeout(CompletableFuture<T> source, long timeoutMillis, String message) {
CompletableFuture<T> future = new CompletableFuture<>();
source.whenComplete((value, throwable) -> {
if (throwable != null) {
future.completeExceptionally(unwrapFailure(throwable));
return;
}
future.complete(value);
});
delayFuture(timeoutMillis).whenComplete((ignored, throwable) -> {
if (!future.isDone()) {
future.completeExceptionally(new TimeoutException(message));
}
});
return future;
}
private IllegalStateException timeoutFailure(String message, Throwable lastFailure) {
if (lastFailure == null) {
return new IllegalStateException(message);
}
return new IllegalStateException(message, lastFailure);
}
private Throwable unwrapFailure(Throwable throwable) { private Throwable unwrapFailure(Throwable throwable) {
Throwable cursor = throwable; Throwable cursor = throwable;
while (cursor instanceof CompletionException || cursor instanceof ExecutionException) { while (cursor instanceof CompletionException || cursor instanceof ExecutionException) {
@@ -14,11 +14,9 @@ import io.papermc.lib.PaperLib;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Chunk; import org.bukkit.Chunk;
import org.bukkit.GameRule; import org.bukkit.GameRule;
import org.bukkit.HeightMap;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Tag;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.world.TimeSkipEvent; import org.bukkit.event.world.TimeSkipEvent;
import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.PluginManager;
@@ -227,21 +225,19 @@ public final class WorldRuntimeControlService {
int chunkX = source.getBlockX() >> 4; int chunkX = source.getBlockX() >> 4;
int chunkZ = source.getBlockZ() >> 4; int chunkZ = source.getBlockZ() >> 4;
return requestChunkAsync(world, chunkX, chunkZ, true).thenCompose(chunk -> {
CompletableFuture<Location> future = new CompletableFuture<>(); CompletableFuture<Location> future = new CompletableFuture<>();
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> { boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
try { try {
future.complete(findTopSafeLocation(world, source)); future.complete(findTopSafeLocation(world, source));
} catch (Throwable e) { } catch (Throwable t) {
future.completeExceptionally(e); future.completeExceptionally(t);
} }
}); });
if (!scheduled) { if (!scheduled) {
return CompletableFuture.failedFuture(new IllegalStateException("Failed to schedule safe-entry surface resolve for " + world.getName() + "@" + chunkX + "," + chunkZ + ".")); future.completeExceptionally(new IllegalStateException(
"Failed to schedule safe-entry resolve for " + world.getName() + "@" + chunkX + "," + chunkZ + "."));
} }
return future; return future;
});
} }
public CompletableFuture<Boolean> teleport(Player player, Location location) { public CompletableFuture<Boolean> teleport(Player player, Location location) {
@@ -312,67 +308,15 @@ public final class WorldRuntimeControlService {
int z = source.getBlockZ(); int z = source.getBlockZ();
float yaw = source.getYaw(); float yaw = source.getYaw();
float pitch = source.getPitch(); float pitch = source.getPitch();
for (int y : buildSafeLocationScanOrder(world, source)) {
if (isSafeStandingLocation(world, x, y, z)) {
return new Location(world, x + 0.5D, y, z + 0.5D, yaw, pitch);
}
}
return null;
}
static int[] buildSafeLocationScanOrder(World world, Location source) {
int minY = world.getMinHeight() + 1; int minY = world.getMinHeight() + 1;
int maxY = world.getMaxHeight() - 2; int maxY = world.getMaxHeight() - 2;
int[] scanOrder = new int[maxY - minY + 1]; if (world.isChunkLoaded(x >> 4, z >> 4)) {
int index = 0; int raw = world.getHighestBlockYAt(x, z, HeightMap.MOTION_BLOCKING_NO_LEAVES);
int y = Math.max(minY, Math.min(maxY, raw + 1));
int runtimeSurface = world.getHighestBlockYAt((int) source.getX(), (int) source.getZ()); return new Location(world, x + 0.5D, y, z + 0.5D, yaw, pitch);
int startY = Math.min(maxY, runtimeSurface + 1);
for (int y = startY; y >= minY; y--) {
scanOrder[index++] = y;
} }
int y = Math.max(minY, Math.min(maxY, source.getBlockY()));
for (int y = startY + 1; y <= maxY; y++) { return new Location(world, x + 0.5D, y, z + 0.5D, yaw, pitch);
scanOrder[index++] = y;
}
return scanOrder;
}
private static boolean isSafeStandingLocation(World world, int x, int y, int z) {
if (y <= world.getMinHeight() || y >= world.getMaxHeight() - 1) {
return false;
}
Block below = world.getBlockAt(x, y - 1, z);
Block feet = world.getBlockAt(x, y, z);
Block head = world.getBlockAt(x, y + 1, z);
Material belowType = below.getType();
if (!belowType.isSolid()) {
return false;
}
if (Tag.LEAVES.isTagged(belowType)) {
return false;
}
if (belowType == Material.LAVA
|| belowType == Material.MAGMA_BLOCK
|| belowType == Material.FIRE
|| belowType == Material.SOUL_FIRE
|| belowType == Material.CAMPFIRE
|| belowType == Material.SOUL_CAMPFIRE) {
return false;
}
if (feet.getType().isSolid() || head.getType().isSolid()) {
return false;
}
if (feet.isLiquid() || head.isLiquid()) {
return false;
}
return true;
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@@ -40,7 +40,6 @@ import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.exceptions.IrisException; import art.arcane.volmlib.util.exceptions.IrisException;
import art.arcane.iris.util.common.format.C; import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form; import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.io.IO;
import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J; import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.scheduling.FoliaScheduler; import art.arcane.volmlib.util.scheduling.FoliaScheduler;
@@ -143,16 +142,6 @@ public class IrisCreator {
} }
reportStudioProgress(0.02D, "resolve_dimension"); reportStudioProgress(0.02D, "resolve_dimension");
if (studio()) {
World existing = Bukkit.getWorld(name());
if (existing == null) {
IO.delete(new File(Bukkit.getWorldContainer(), name()));
IO.delete(new File(Bukkit.getWorldContainer(), name() + "_nether"));
IO.delete(new File(Bukkit.getWorldContainer(), name() + "_the_end"));
}
}
reportStudioProgress(0.08D, "resolve_dimension"); reportStudioProgress(0.08D, "resolve_dimension");
IrisDimension d = IrisToolbelt.getDimension(dimension()); IrisDimension d = IrisToolbelt.getDimension(dimension());
@@ -185,7 +174,7 @@ public class IrisCreator {
if (!studio()) { if (!studio()) {
IrisWorlds.get().put(name(), dimension()); IrisWorlds.get().put(name(), dimension());
} }
ServerConfigurator.installDataPacks(!studio()); ServerConfigurator.installDataPacksIfChanged(!studio());
reportStudioProgress(0.40D, "install_datapacks"); reportStudioProgress(0.40D, "install_datapacks");
PlatformChunkGenerator access = (PlatformChunkGenerator) wc.generator(); PlatformChunkGenerator access = (PlatformChunkGenerator) wc.generator();
@@ -129,23 +129,31 @@ public class IrisEngine implements Engine {
wallClock = new AtomicRollingSequence(32); wallClock = new AtomicRollingSequence(32);
lastGPS = new AtomicLong(M.ms()); lastGPS = new AtomicLong(M.ms());
generated = new AtomicInteger(0); generated = new AtomicInteger(0);
long _t0 = M.ms();
mantle = new IrisEngineMantle(this); mantle = new IrisEngineMantle(this);
Iris.info("[IrisEngine timing] new IrisEngineMantle=" + (M.ms() - _t0) + "ms");
context = new IrisContext(this); context = new IrisContext(this);
cleaning = new AtomicBoolean(false); cleaning = new AtomicBoolean(false);
modeFallbackLogged = new AtomicBoolean(false); modeFallbackLogged = new AtomicBoolean(false);
if (studio) { if (studio) {
_t0 = M.ms();
getData().dump(); getData().dump();
getData().clearLists(); getData().clearLists();
getTarget().setDimension(getData().getDimensionLoader().load(getDimension().getLoadKey())); getTarget().setDimension(getData().getDimensionLoader().load(getDimension().getLoadKey()));
Iris.info("[IrisEngine timing] dump+clearLists+reload=" + (M.ms() - _t0) + "ms");
} }
context.touch(); context.touch();
getData().setEngine(this); getData().setEngine(this);
_t0 = M.ms();
getData().loadPrefetch(this); getData().loadPrefetch(this);
Iris.info("[IrisEngine timing] loadPrefetch=" + (M.ms() - _t0) + "ms");
Iris.info("Initializing Engine: " + target.getWorld().name() + "/" + target.getDimension().getLoadKey() + " (" + target.getDimension().getDimensionHeight() + " height) Seed: " + getSeedManager().getSeed()); Iris.info("Initializing Engine: " + target.getWorld().name() + "/" + target.getDimension().getLoadKey() + " (" + target.getDimension().getDimensionHeight() + " height) Seed: " + getSeedManager().getSeed());
failing = false; failing = false;
closed = false; closed = false;
art = J.ar(this::tickRandomPlayer, 0); art = J.ar(this::tickRandomPlayer, 0);
_t0 = M.ms();
setupEngine(); setupEngine();
Iris.info("[IrisEngine timing] setupEngine total=" + (M.ms() - _t0) + "ms");
Iris.debug("Engine Initialized " + getCacheID()); Iris.debug("Engine Initialized " + getCacheID());
} }
@@ -208,15 +216,27 @@ public class IrisEngine implements Engine {
closing.set(false); closing.set(false);
Iris.debug("Setup Engine " + getCacheID()); Iris.debug("Setup Engine " + getCacheID());
cacheId = RNG.r.nextInt(); cacheId = RNG.r.nextInt();
long t0 = M.ms();
complex = ensureComplex(); complex = ensureComplex();
Iris.info("[IrisEngine timing] ensureComplex=" + (M.ms() - t0) + "ms");
t0 = M.ms();
upperContext = buildUpperContext(); upperContext = buildUpperContext();
Iris.info("[IrisEngine timing] buildUpperContext=" + (M.ms() - t0) + "ms");
t0 = M.ms();
effects = new IrisEngineEffects(this); effects = new IrisEngineEffects(this);
Iris.info("[IrisEngine timing] IrisEngineEffects=" + (M.ms() - t0) + "ms");
hash32 = new CompletableFuture<>(); hash32 = new CompletableFuture<>();
t0 = M.ms();
mantle.hotload(); mantle.hotload();
Iris.info("[IrisEngine timing] mantle.hotload=" + (M.ms() - t0) + "ms");
t0 = M.ms();
setupMode(); setupMode();
Iris.info("[IrisEngine timing] setupMode=" + (M.ms() - t0) + "ms");
t0 = M.ms();
IrisWorldManager manager = new IrisWorldManager(this); IrisWorldManager manager = new IrisWorldManager(this);
worldManager = manager; worldManager = manager;
manager.startManager(); manager.startManager();
Iris.info("[IrisEngine timing] IrisWorldManager=" + (M.ms() - t0) + "ms");
J.a(this::computeBiomeMaxes); J.a(this::computeBiomeMaxes);
J.a(() -> { J.a(() -> {
File[] roots = getData().getLoaders() File[] roots = getData().getLoaders()
@@ -31,33 +31,46 @@ import org.jetbrains.annotations.Nullable;
public class IslandObjectPlacer implements IObjectPlacer { public class IslandObjectPlacer implements IObjectPlacer {
private static final int OVERHANG_RADIUS = 2; private static final int OVERHANG_RADIUS = 2;
public enum AnchorFace { TOP, BOTTOM }
private final MantleWriter wrapped; private final MantleWriter wrapped;
private final FloatingIslandSample[] samples; private final FloatingIslandSample[] samples;
private final boolean[] overhangAllowed; private final boolean[] overhangAllowed;
private final int minX; private final int minX;
private final int minZ; private final int minZ;
private final int chunkMaxIslandTopY; private final int chunkMaxIslandTopY;
private final int anchorTopY; private final int chunkMinIslandBottomY;
private final int anchorY;
private final AnchorFace face;
private int writesAttempted; private int writesAttempted;
private int writesDroppedBelow; private int writesDroppedBelow;
private int writesDroppedOverhang; private int writesDroppedOverhang;
private int writesDroppedAboveBottom;
private int writesDroppedBottomOverhang;
public IslandObjectPlacer(MantleWriter wrapped, FloatingIslandSample[] samples, int minX, int minZ, int anchorTopY) { public IslandObjectPlacer(MantleWriter wrapped, FloatingIslandSample[] samples, int minX, int minZ, int anchorTopY) {
this(wrapped, samples, minX, minZ, anchorTopY, AnchorFace.TOP);
}
public IslandObjectPlacer(MantleWriter wrapped, FloatingIslandSample[] samples, int minX, int minZ, int anchorY, AnchorFace face) {
this.wrapped = wrapped; this.wrapped = wrapped;
this.samples = samples; this.samples = samples;
this.minX = minX; this.minX = minX;
this.minZ = minZ; this.minZ = minZ;
this.anchorTopY = anchorTopY; this.anchorY = anchorY;
int maxY = -1; this.face = face;
int maxTopY = -1;
int minBottomY = Integer.MAX_VALUE;
for (FloatingIslandSample s : samples) { for (FloatingIslandSample s : samples) {
if (s != null) { if (s != null) {
int ty = s.topY(); int ty = s.topY();
if (ty > maxY) { if (ty > maxTopY) maxTopY = ty;
maxY = ty; int by = s.bottomY();
if (by >= 0 && by < minBottomY) minBottomY = by;
} }
} }
} this.chunkMaxIslandTopY = maxTopY;
this.chunkMaxIslandTopY = maxY; this.chunkMinIslandBottomY = (minBottomY == Integer.MAX_VALUE) ? -1 : minBottomY;
this.overhangAllowed = buildOverhangMask(samples); this.overhangAllowed = buildOverhangMask(samples);
} }
@@ -104,6 +117,14 @@ public class IslandObjectPlacer implements IObjectPlacer {
return writesDroppedOverhang; return writesDroppedOverhang;
} }
public int getWritesDroppedAboveBottom() {
return writesDroppedAboveBottom;
}
public int getWritesDroppedBottomOverhang() {
return writesDroppedBottomOverhang;
}
private boolean shouldSkipAirColumn(int x, int y, int z) { private boolean shouldSkipAirColumn(int x, int y, int z) {
writesAttempted++; writesAttempted++;
int xf = x - minX; int xf = x - minX;
@@ -111,9 +132,17 @@ public class IslandObjectPlacer implements IObjectPlacer {
if (xf >= 0 && xf < 16 && zf >= 0 && zf < 16) { if (xf >= 0 && xf < 16 && zf >= 0 && zf < 16) {
int idx = (zf << 4) | xf; int idx = (zf << 4) | xf;
if (samples[idx] != null) { if (samples[idx] != null) {
if (face == AnchorFace.TOP) {
return false; return false;
} }
if (y <= anchorTopY) { if (y >= anchorY) {
writesDroppedAboveBottom++;
return true;
}
return false;
}
if (face == AnchorFace.TOP) {
if (y <= anchorY) {
writesDroppedBelow++; writesDroppedBelow++;
return true; return true;
} }
@@ -121,12 +150,29 @@ public class IslandObjectPlacer implements IObjectPlacer {
writesDroppedOverhang++; writesDroppedOverhang++;
return true; return true;
} }
} else {
if (y >= anchorY) {
writesDroppedBottomOverhang++;
return true;
}
if (!overhangAllowed[idx]) {
writesDroppedBottomOverhang++;
return true;
}
}
return false; return false;
} }
if (y <= anchorTopY) { if (face == AnchorFace.TOP) {
if (y <= anchorY) {
writesDroppedBelow++; writesDroppedBelow++;
return true; return true;
} }
} else {
if (y >= anchorY) {
writesDroppedBottomOverhang++;
return true;
}
}
writesDroppedOverhang++; writesDroppedOverhang++;
return true; return true;
} }
@@ -143,19 +189,20 @@ public class IslandObjectPlacer implements IObjectPlacer {
@Override @Override
public int getHighest(int x, int z, IrisData data) { public int getHighest(int x, int z, IrisData data) {
FloatingIslandSample s = sampleAt(x, z); FloatingIslandSample s = sampleAt(x, z);
if (s != null) { if (face == AnchorFace.TOP) {
return s.topY(); if (s != null) return s.topY();
}
return chunkMaxIslandTopY; return chunkMaxIslandTopY;
} }
if (s != null) {
int by = s.bottomY();
return (by >= 0) ? by : chunkMinIslandBottomY;
}
return chunkMinIslandBottomY;
}
@Override @Override
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) { public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
FloatingIslandSample s = sampleAt(x, z); return getHighest(x, z, data);
if (s != null) {
return s.topY();
}
return chunkMaxIslandTopY;
} }
@Override @Override
@@ -57,6 +57,14 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
public static final AtomicLong writesAttemptedTotal = new AtomicLong(); public static final AtomicLong writesAttemptedTotal = new AtomicLong();
public static final AtomicLong writesDroppedBelowTotal = new AtomicLong(); public static final AtomicLong writesDroppedBelowTotal = new AtomicLong();
public static final AtomicLong writesDroppedOverhangTotal = new AtomicLong(); public static final AtomicLong writesDroppedOverhangTotal = new AtomicLong();
public static final AtomicLong objectsInvertedAttempted = new AtomicLong();
public static final AtomicLong objectsInvertedPlaced = new AtomicLong();
public static final AtomicLong objectsInvertedSkippedNoFlat = new AtomicLong();
public static final AtomicLong objectsInvertedFallbackNoInterior = new AtomicLong();
public static final AtomicLong objectsInvertedSkippedShrink = new AtomicLong();
public static final AtomicLong objectsInvertedSkippedNullObj = new AtomicLong();
public static final AtomicLong writesDroppedAboveBottomTotal = new AtomicLong();
public static final AtomicLong writesDroppedBottomOverhangTotal = new AtomicLong();
private static final int TERRAIN_MISMATCH_WARNING_CAP = 200; private static final int TERRAIN_MISMATCH_WARNING_CAP = 200;
private static final AtomicLong heavyClipWarnings = new AtomicLong(); private static final AtomicLong heavyClipWarnings = new AtomicLong();
private static final int HEAVY_CLIP_WARNING_CAP = 30; private static final int HEAVY_CLIP_WARNING_CAP = 30;
@@ -83,6 +91,14 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
writesDroppedOverhangTotal.set(0); writesDroppedOverhangTotal.set(0);
heavyClipWarnings.set(0); heavyClipWarnings.set(0);
anchorYHisto.clear(); anchorYHisto.clear();
objectsInvertedAttempted.set(0);
objectsInvertedPlaced.set(0);
objectsInvertedSkippedNoFlat.set(0);
objectsInvertedFallbackNoInterior.set(0);
objectsInvertedSkippedShrink.set(0);
objectsInvertedSkippedNullObj.set(0);
writesDroppedAboveBottomTotal.set(0);
writesDroppedBottomOverhangTotal.set(0);
} }
private static void recordWriteStats(IrisObject obj, int wx, int wz, int pickTopY, IslandObjectPlacer islandPlacer) { private static void recordWriteStats(IrisObject obj, int wx, int wz, int pickTopY, IslandObjectPlacer islandPlacer) {
@@ -107,6 +123,11 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
} }
} }
private static void recordInvertedWriteStats(IslandObjectPlacer islandPlacer) {
writesDroppedAboveBottomTotal.addAndGet(islandPlacer.getWritesDroppedAboveBottom());
writesDroppedBottomOverhangTotal.addAndGet(islandPlacer.getWritesDroppedBottomOverhang());
}
private static void verifyTerrainBelowObject(IrisObject obj, int wx, int wz, int pickTopY, FloatingIslandSample sample) { private static void verifyTerrainBelowObject(IrisObject obj, int wx, int wz, int pickTopY, FloatingIslandSample sample) {
if (terrainMismatchWarnings.get() >= TERRAIN_MISMATCH_WARNING_CAP) { if (terrainMismatchWarnings.get() >= TERRAIN_MISMATCH_WARNING_CAP) {
return; return;
@@ -189,12 +210,13 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
} }
} }
KList<IrisObjectPlacement> surface = entry.isInheritObjects() && target != null ? target.getSurfaceObjects() : null; KList<IrisObjectPlacement> surface = target != null ? entry.resolveTopObjects(target) : null;
KList<IrisObjectPlacement> extras = entry.getExtraObjects(); KList<IrisObjectPlacement> extras = entry.getExtraObjects();
boolean hasSurface = surface != null && !surface.isEmpty(); boolean hasSurface = surface != null && !surface.isEmpty();
boolean hasExtras = extras != null && !extras.isEmpty(); boolean hasExtras = extras != null && !extras.isEmpty();
KList<Integer> interior = null;
if (hasSurface || hasExtras) { if (hasSurface || hasExtras) {
KList<Integer> interior = interiorColumns(samples, columns); interior = interiorColumns(samples, columns);
if (hasSurface) { if (hasSurface) {
for (IrisObjectPlacement placement : surface) { for (IrisObjectPlacement placement : surface) {
tryPlaceAnchoredChunk(writer, complex, chunkRng, data, placement, samples, columns, interior, minX, minZ, entry); tryPlaceAnchoredChunk(writer, complex, chunkRng, data, placement, samples, columns, interior, minX, minZ, entry);
@@ -206,6 +228,15 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
} }
} }
} }
KList<IrisObjectPlacement> bottom = target != null ? entry.resolveBottomObjects(target) : null;
if (bottom != null && !bottom.isEmpty()) {
if (interior == null) {
interior = interiorColumns(samples, columns);
}
for (IrisObjectPlacement placement : bottom) {
tryPlaceInvertedChunk(writer, complex, chunkRng, data, placement, samples, columns, interior, minX, minZ, entry);
}
}
} }
} }
@@ -352,6 +383,129 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
} }
} }
@ChunkCoordinates
private void tryPlaceInvertedChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, FloatingIslandSample[] samples, KList<Integer> columns, KList<Integer> interior, int minX, int minZ, IrisFloatingChildBiomes entry) {
if (placement == null || columns.isEmpty()) {
return;
}
int density = placement.getDensity(rng, minX, minZ, data);
double perAttempt = placement.getChance();
for (int i = 0; i < density; i++) {
objectsInvertedAttempted.incrementAndGet();
if (!rng.chance(perAttempt + rng.d(-0.005, 0.005))) {
continue;
}
IrisObject raw = placement.getObject(complex, rng);
if (raw == null) {
objectsInvertedSkippedNullObj.incrementAndGet();
continue;
}
IrisObject obj0 = placement.getScale().get(rng, raw);
if (obj0 == null) {
objectsInvertedSkippedShrink.incrementAndGet();
continue;
}
if (entry != null && entry.hasObjectShrink()) {
obj0 = entry.getShrinkScale().get(rng, obj0);
if (obj0 == null) {
objectsInvertedSkippedShrink.incrementAndGet();
continue;
}
}
final IrisObject obj = obj0;
FloatingObjectFootprint fp = FloatingObjectFootprint.compute(obj);
KList<Integer> pool = interior.isEmpty() ? columns : interior;
if (interior.isEmpty()) {
objectsInvertedFallbackNoInterior.incrementAndGet();
}
int pickedKey = pool.get(rng.i(0, pool.size() - 1));
int pickedXf = pickedKey & 15;
int pickedZf = pickedKey >> 4;
FloatingIslandSample pickedSample = samples[(pickedZf << 4) | pickedXf];
if (pickedSample == null) {
objectsInvertedSkippedNoFlat.incrementAndGet();
continue;
}
int pickBottomY = pickedSample.bottomY();
if (pickBottomY < 0) {
objectsInvertedSkippedNoFlat.incrementAndGet();
continue;
}
if (!isFootprintFlatBottom(fp, pickedXf, pickedZf, pickBottomY, samples, 2)) {
if (!isFootprintFlatBottom(fp, pickedXf, pickedZf, pickBottomY, samples, 4)) {
objectsInvertedSkippedNoFlat.incrementAndGet();
continue;
}
}
int wx = minX + pickedXf - fp.getTallestKxBottom();
int wz = minZ + pickedZf - fp.getTallestKzBottom();
IrisObjectPlacement inverted = placement.toPlacement(obj.getLoadKey());
inverted.setMode(translateStiltModeForFloating(inverted.getMode()));
inverted.setTranslate(new IrisObjectTranslate());
inverted.setRotation(IrisObjectRotation.xFlip180());
inverted.setForcePlace(true);
inverted.setBottom(false);
int yv = pickBottomY - 1 + fp.getHighestSolidKeyY();
IslandObjectPlacer islandPlacer = new IslandObjectPlacer(writer, samples, minX, minZ, pickBottomY, IslandObjectPlacer.AnchorFace.BOTTOM);
int id = rng.i(0, Integer.MAX_VALUE);
try {
obj.place(wx, yv, wz, islandPlacer, inverted, rng, (b, bd) -> {
String marker = placementMarker(obj, id);
if (marker != null) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
}
}, null, data);
objectsInvertedPlaced.incrementAndGet();
recordInvertedWriteStats(islandPlacer);
} catch (Throwable e) {
Iris.reportError(e);
}
}
}
private static boolean isFootprintFlatBottom(FloatingObjectFootprint fp, int pickedXf, int pickedZf, int pickBottomY, FloatingIslandSample[] samples, int tolerance) {
int tallestKxBottom = fp.getTallestKxBottom();
int tallestKzBottom = fp.getTallestKzBottom();
int checked = 0;
boolean touchedChunkEdge = false;
long[] cells = fp.footprintXZ();
for (int i = 0, n = cells.length; i < n; i++) {
long encoded = cells[i];
int kx = (int) (encoded >> 32);
int kz = (int) (encoded & 0xFFFFFFFFL);
int colXf = pickedXf + (kx - tallestKxBottom);
int colZf = pickedZf + (kz - tallestKzBottom);
if (colXf < 0 || colXf >= 16 || colZf < 0 || colZf >= 16) {
touchedChunkEdge = true;
continue;
}
FloatingIslandSample s = samples[(colZf << 4) | colXf];
if (s == null) {
return false;
}
int by = s.bottomY();
if (by < 0 || Math.abs(by - pickBottomY) > tolerance) {
return false;
}
checked++;
}
if (checked >= MIN_FOOTPRINT_CELLS_CHECKED) {
return true;
}
return touchedChunkEdge;
}
private static boolean isFootprintFlat(FloatingObjectFootprint fp, int pickedXf, int pickedZf, int pickTopY, FloatingIslandSample[] samples, int tolerance) { private static boolean isFootprintFlat(FloatingObjectFootprint fp, int pickedXf, int pickedZf, int pickTopY, FloatingIslandSample[] samples, int tolerance) {
int tallestKx = fp.getTallestKx(); int tallestKx = fp.getTallestKx();
int tallestKz = fp.getTallestKz(); int tallestKz = fp.getTallestKz();
@@ -449,17 +603,18 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
for (IrisFloatingChildBiomes entry : entries) { for (IrisFloatingChildBiomes entry : entries) {
collectPlacementKeys(entry.getFloatingObjects(), objectKeys); collectPlacementKeys(entry.getFloatingObjects(), objectKeys);
collectPlacementKeys(entry.getExtraObjects(), objectKeys); collectPlacementKeys(entry.getExtraObjects(), objectKeys);
if (entry.isInheritObjects()) { collectPlacementKeys(entry.getTopObjectOverrides(), objectKeys);
collectPlacementKeys(entry.getBottomObjectOverrides(), objectKeys);
try { try {
IrisBiome target = entry.getRealBiome(biome, data); IrisBiome target = entry.getRealBiome(biome, data);
if (target != null) { if (target != null) {
collectPlacementKeys(target.getSurfaceObjects(), objectKeys); collectPlacementKeys(entry.resolveTopObjects(target), objectKeys);
collectPlacementKeys(entry.resolveBottomObjects(target), objectKeys);
} }
} catch (Throwable ignored) { } catch (Throwable ignored) {
} }
} }
} }
}
for (String key : objectKeys) { for (String key : objectKeys) {
try { try {
java.io.File f = data.getObjectLoader().findFile(key); java.io.File f = data.getObjectLoader().findFile(key);
@@ -392,8 +392,8 @@ public class MantleObjectComponent extends IrisMantleComponent {
} }
int xx = rng.i(x, x + 15); int xx = rng.i(x, x + 15);
int zz = rng.i(z, z + 15); int zz = rng.i(z, z + 15);
int surfaceObjectExclusionDepth = resolveSurfaceObjectExclusionDepth(surfaceObjectExclusionBaseDepth, v); int surfaceObjectExclusionDepth = resolveSurfaceObjectExclusionDepth(surfaceObjectExclusionBaseDepth, v, objectPlacement);
int surfaceObjectExclusionRadius = resolveSurfaceObjectExclusionRadius(v); int surfaceObjectExclusionRadius = resolveSurfaceObjectExclusionRadius(v, objectPlacement);
boolean overCave = surfaceObjectExclusionDepth > 0 && hasSurfaceCarveExposure(writer, surfaceHeightLookup, xx, zz, surfaceObjectExclusionDepth, surfaceObjectExclusionRadius); boolean overCave = surfaceObjectExclusionDepth > 0 && hasSurfaceCarveExposure(writer, surfaceHeightLookup, xx, zz, surfaceObjectExclusionDepth, surfaceObjectExclusionRadius);
int id = rng.i(0, Integer.MAX_VALUE); int id = rng.i(0, Integer.MAX_VALUE);
IrisObjectPlacement effectivePlacement = resolveEffectivePlacement(objectPlacement, v); IrisObjectPlacement effectivePlacement = resolveEffectivePlacement(objectPlacement, v);
@@ -1038,23 +1038,30 @@ public class MantleObjectComponent extends IrisMantleComponent {
return Math.max(0, caveProfile.getSurfaceObjectExclusionDepth()); return Math.max(0, caveProfile.getSurfaceObjectExclusionDepth());
} }
private int resolveSurfaceObjectExclusionDepth(int baseDepth, IrisObject object) { private int resolveSurfaceObjectExclusionDepth(int baseDepth, IrisObject object, IrisObjectPlacement placement) {
if (object == null) { if (object == null) {
return baseDepth; return baseDepth;
} }
int horizontalReach = resolveSurfaceObjectExclusionRadius(object) + 2; int horizontalReach = resolveSurfaceObjectExclusionRadius(object, placement) + 2;
int verticalReach = Math.max(4, Math.min(16, Math.floorDiv(Math.max(1, object.getH()), 2))); int verticalReach = Math.max(4, Math.min(16, Math.floorDiv(Math.max(1, object.getH()), 2)));
return Math.max(baseDepth, Math.max(horizontalReach, verticalReach)); return Math.max(baseDepth, Math.max(horizontalReach, verticalReach));
} }
private int resolveSurfaceObjectExclusionRadius(IrisObject object) { static int computeSurfaceExclusionRadius(int maxDimension, int absTranslateX, int absTranslateZ) {
return Math.max(1, Math.floorDiv(Math.max(1, maxDimension), 2) + absTranslateX + absTranslateZ + 1);
}
private int resolveSurfaceObjectExclusionRadius(IrisObject object, IrisObjectPlacement placement) {
if (object == null) { if (object == null) {
return 1; return 1;
} }
int maxDimension = Math.max(object.getW(), object.getD()); int maxDimension = Math.max(object.getW(), object.getD());
return Math.max(1, Math.min(8, Math.floorDiv(Math.max(1, maxDimension), 2))); IrisObjectTranslate t = placement != null ? placement.getTranslate() : null;
int absX = t != null ? Math.abs(t.getX()) : 0;
int absZ = t != null ? Math.abs(t.getZ()) : 0;
return computeSurfaceExclusionRadius(maxDimension, absX, absZ);
} }
private int resolveAnchorSearchAttempts(IrisCaveProfile caveProfile) { private int resolveAnchorSearchAttempts(IrisCaveProfile caveProfile) {
@@ -117,6 +117,8 @@ public class IrisDepositModifier extends EngineAssignedModifier<BlockData> {
if (y > k.getMaxHeight() || y < k.getMinHeight() || y > height - 2) if (y > k.getMaxHeight() || y < k.getMinHeight() || y > height - 2)
continue; continue;
IrisDimension dimension = getDimension();
for (BlockVector j : clump.getBlocks().keys()) { for (BlockVector j : clump.getBlocks().keys()) {
int nx = j.getBlockX() + x; int nx = j.getBlockX() + x;
int ny = j.getBlockY() + y; int ny = j.getBlockY() + y;
@@ -130,9 +132,63 @@ public class IrisDepositModifier extends EngineAssignedModifier<BlockData> {
} }
if (chunk.get(nx, ny, nz, MatterCavern.class) == null) { if (chunk.get(nx, ny, nz, MatterCavern.class) == null) {
data.set(nx, ny, nz, B.toDeepSlateOre(data.get(nx, ny, nz), clump.getBlocks().get(j))); BlockData ore = clump.getBlocks().get(j);
BlockData remapped = resolveDepositVariant(cx, cz, nx, ny, nz, ore, dimension, context);
BlockData finalBlock = remapped != null
? remapped
: B.toDeepSlateOre(data.get(nx, ny, nz), ore);
data.set(nx, ny, nz, finalBlock);
} }
} }
} }
} }
private BlockData resolveDepositVariant(int cx, int cz, int nx, int ny, int nz, BlockData ore, IrisDimension dimension, ChunkContext context) {
int worldX = (cx << 4) + nx;
int worldZ = (cz << 4) + nz;
IrisBiome biome = getEngine().getBiome(worldX, ny, worldZ);
if (biome != null) {
BlockData match = matchDepositVariant(biome.getDepositVariants(), ore, ny);
if (match != null) {
return match;
}
}
IrisRegion region = context.getRegion().get(nx, nz);
if (region != null) {
BlockData match = matchDepositVariant(region.getDepositVariants(), ore, ny);
if (match != null) {
return match;
}
}
if (dimension != null) {
BlockData match = matchDepositVariant(dimension.getDepositVariants(), ore, ny);
if (match != null) {
return match;
}
}
return null;
}
private BlockData matchDepositVariant(java.util.List<IrisDepositVariant> variants, BlockData ore, int y) {
if (variants == null || variants.isEmpty()) {
return null;
}
for (IrisDepositVariant variant : variants) {
if (y < variant.getMinHeight() || y > variant.getMaxHeight()) {
continue;
}
BlockData swapped = variant.remapOrNull(ore, getData());
if (swapped != null) {
return swapped;
}
}
return null;
}
} }
@@ -101,6 +101,12 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Block
+ " writeDropBelow=" + MantleFloatingObjectComponent.writesDroppedBelowTotal.get() + " writeDropBelow=" + MantleFloatingObjectComponent.writesDroppedBelowTotal.get()
+ " writeDropOverhang=" + MantleFloatingObjectComponent.writesDroppedOverhangTotal.get() + " writeDropOverhang=" + MantleFloatingObjectComponent.writesDroppedOverhangTotal.get()
+ " terrainMismatch=" + MantleFloatingObjectComponent.terrainMismatchWarnings.get() + " terrainMismatch=" + MantleFloatingObjectComponent.terrainMismatchWarnings.get()
+ " objInvAttempt=" + MantleFloatingObjectComponent.objectsInvertedAttempted.get()
+ " objInvPlaced=" + MantleFloatingObjectComponent.objectsInvertedPlaced.get()
+ " objInvNoFlat=" + MantleFloatingObjectComponent.objectsInvertedSkippedNoFlat.get()
+ " objInvFallbackNoInterior=" + MantleFloatingObjectComponent.objectsInvertedFallbackNoInterior.get()
+ " writesAboveBottom=" + MantleFloatingObjectComponent.writesDroppedAboveBottomTotal.get()
+ " writesBottomOverhang=" + MantleFloatingObjectComponent.writesDroppedBottomOverhangTotal.get()
+ " anchorY:" + (topAnchorY.length() == 0 ? " <none>" : topAnchorY.toString()) + " anchorY:" + (topAnchorY.length() == 0 ? " <none>" : topAnchorY.toString())
+ " topFloors:" + (topFloors.length() == 0 ? " <none>" : topFloors.toString())); + " topFloors:" + (topFloors.length() == 0 ? " <none>" : topFloors.toString()));
} }
@@ -92,6 +92,7 @@ public final class FloatingIslandSample {
public final int topIdx; public final int topIdx;
public final int solidCount; public final int solidCount;
public final boolean[] solidMask; public final boolean[] solidMask;
private transient int cachedBottomIdx = -2;
private FloatingIslandSample(IrisFloatingChildBiomes entry, int islandBaseY, int thickness, int topIdx, int solidCount, boolean[] solidMask) { private FloatingIslandSample(IrisFloatingChildBiomes entry, int islandBaseY, int thickness, int topIdx, int solidCount, boolean[] solidMask) {
this.entry = entry; this.entry = entry;
@@ -102,10 +103,27 @@ public final class FloatingIslandSample {
this.solidMask = solidMask; this.solidMask = solidMask;
} }
static FloatingIslandSample constructForTest(int islandBaseY, int thickness, int topIdx, int solidCount, boolean[] solidMask) {
return new FloatingIslandSample(null, islandBaseY, thickness, topIdx, solidCount, solidMask);
}
public int topY() { public int topY() {
return islandBaseY + topIdx; return islandBaseY + topIdx;
} }
public int bottomY() {
if (cachedBottomIdx == -2) {
cachedBottomIdx = -1;
for (int i = 0; i < solidMask.length; i++) {
if (solidMask[i]) {
cachedBottomIdx = i;
break;
}
}
}
return cachedBottomIdx == -1 ? -1 : islandBaseY + cachedBottomIdx;
}
public static long columnSeed(long baseSeed, int wx, int wz) { public static long columnSeed(long baseSeed, int wx, int wz) {
return baseSeed ^ ((long) wx * 341873128712L) ^ ((long) wz * 132897987541L); return baseSeed ^ ((long) wx * 341873128712L) ^ ((long) wz * 132897987541L);
} }
@@ -258,6 +276,11 @@ public final class FloatingIslandSample {
} }
} }
if (!useCarve) {
solidCount = solidifyUncarvedInterior(solidMask);
highestSolidIdx = highestSolidIndex(solidMask);
}
if (solidCount == 0 || highestSolidIdx < 0) { if (solidCount == 0 || highestSolidIdx < 0) {
return reject(REJECT_NO_SOLID); return reject(REJECT_NO_SOLID);
} }
@@ -268,6 +291,36 @@ public final class FloatingIslandSample {
return new FloatingIslandSample(entry, botY, thickness, topIdx, solidCount, solidMask); return new FloatingIslandSample(entry, botY, thickness, topIdx, solidCount, solidMask);
} }
static int solidifyUncarvedInterior(boolean[] solidMask) {
int firstSolid = -1;
int lastSolid = -1;
for (int i = 0; i < solidMask.length; i++) {
if (!solidMask[i]) {
continue;
}
if (firstSolid < 0) {
firstSolid = i;
}
lastSolid = i;
}
if (firstSolid < 0) {
return 0;
}
for (int i = firstSolid; i <= lastSolid; i++) {
solidMask[i] = true;
}
return lastSolid - firstSolid + 1;
}
private static int highestSolidIndex(boolean[] solidMask) {
for (int i = solidMask.length - 1; i >= 0; i--) {
if (solidMask[i]) {
return i;
}
}
return -1;
}
private static int computeTopHeight(IrisFloatingChildBiomes entry, IrisBiome target, Engine engine, long baseSeed, int wx, int wz, IrisData data) { private static int computeTopHeight(IrisFloatingChildBiomes entry, IrisBiome target, Engine engine, long baseSeed, int wx, int wz, IrisData data) {
int maxTopHeight = Math.max(0, entry.getMaxTopHeight()); int maxTopHeight = Math.max(0, entry.getMaxTopHeight());
if (maxTopHeight == 0) { if (maxTopHeight == 0) {
@@ -35,20 +35,26 @@ public class FloatingObjectFootprint {
private static final boolean DIAGNOSTIC_LOG = Boolean.parseBoolean(System.getProperty("iris.floating.footprintLog", "true")); private static final boolean DIAGNOSTIC_LOG = Boolean.parseBoolean(System.getProperty("iris.floating.footprintLog", "true"));
private final int lowestSolidKeyY; private final int lowestSolidKeyY;
private final int highestSolidKeyY;
private final int centerX; private final int centerX;
private final int centerY; private final int centerY;
private final int centerZ; private final int centerZ;
private final int tallestKx; private final int tallestKx;
private final int tallestKz; private final int tallestKz;
private final int tallestKxBottom;
private final int tallestKzBottom;
private final long[] footprintXZ; private final long[] footprintXZ;
private FloatingObjectFootprint(int lowestSolidKeyY, int centerX, int centerY, int centerZ, int tallestKx, int tallestKz, long[] footprintXZ) { private FloatingObjectFootprint(int lowestSolidKeyY, int highestSolidKeyY, int centerX, int centerY, int centerZ, int tallestKx, int tallestKz, int tallestKxBottom, int tallestKzBottom, long[] footprintXZ) {
this.lowestSolidKeyY = lowestSolidKeyY; this.lowestSolidKeyY = lowestSolidKeyY;
this.highestSolidKeyY = highestSolidKeyY;
this.centerX = centerX; this.centerX = centerX;
this.centerY = centerY; this.centerY = centerY;
this.centerZ = centerZ; this.centerZ = centerZ;
this.tallestKx = tallestKx; this.tallestKx = tallestKx;
this.tallestKz = tallestKz; this.tallestKz = tallestKz;
this.tallestKxBottom = tallestKxBottom;
this.tallestKzBottom = tallestKzBottom;
this.footprintXZ = footprintXZ; this.footprintXZ = footprintXZ;
} }
@@ -63,6 +69,10 @@ public class FloatingObjectFootprint {
int cz = obj.getCenter().getBlockZ(); int cz = obj.getCenter().getBlockZ();
Map<Long, int[]> columnStats = new HashMap<>(); Map<Long, int[]> columnStats = new HashMap<>();
int[] globalHighestY = {Integer.MIN_VALUE};
int[] globalHighestKx = {0};
int[] globalHighestKz = {0};
obj.getBlocks().forEach((BlockVector key, BlockData bd) -> { obj.getBlocks().forEach((BlockVector key, BlockData bd) -> {
if (!B.isSolid(bd)) { if (!B.isSolid(bd)) {
return; return;
@@ -81,6 +91,11 @@ public class FloatingObjectFootprint {
} }
stats[1]++; stats[1]++;
} }
if (ky > globalHighestY[0]) {
globalHighestY[0] = ky;
globalHighestKx[0] = kx;
globalHighestKz[0] = kz;
}
}); });
long[] footprintArray = new long[columnStats.size()]; long[] footprintArray = new long[columnStats.size()];
@@ -90,15 +105,16 @@ public class FloatingObjectFootprint {
} }
long tallestPacked = resolveTallestColumn(columnStats); long tallestPacked = resolveTallestColumn(columnStats);
int lowestSolidKeyY = columnStats.isEmpty() int lowestSolidKeyY = columnStats.isEmpty() ? cy : columnStats.get(tallestPacked)[0];
? cy int highestSolidKeyY = columnStats.isEmpty() ? cy : globalHighestY[0];
: columnStats.get(tallestPacked)[0];
int tallestKx = columnStats.isEmpty() ? 0 : (int) (tallestPacked >> 32); int tallestKx = columnStats.isEmpty() ? 0 : (int) (tallestPacked >> 32);
int tallestKz = columnStats.isEmpty() ? 0 : (int) (tallestPacked & 0xFFFFFFFFL); int tallestKz = columnStats.isEmpty() ? 0 : (int) (tallestPacked & 0xFFFFFFFFL);
int tallestKxBottom = columnStats.isEmpty() ? 0 : globalHighestKx[0];
int tallestKzBottom = columnStats.isEmpty() ? 0 : globalHighestKz[0];
if (DIAGNOSTIC_LOG) { if (DIAGNOSTIC_LOG) {
logFootprintDiagnostic(cacheKey, obj, cx, cy, cz, lowestSolidKeyY, tallestKx, tallestKz, columnStats); logFootprintDiagnostic(cacheKey, obj, cx, cy, cz, lowestSolidKeyY, tallestKx, tallestKz, columnStats);
} }
return new FloatingObjectFootprint(lowestSolidKeyY, cx, cy, cz, tallestKx, tallestKz, footprintArray); return new FloatingObjectFootprint(lowestSolidKeyY, highestSolidKeyY, cx, cy, cz, tallestKx, tallestKz, tallestKxBottom, tallestKzBottom, footprintArray);
} }
private static void logFootprintDiagnostic(String cacheKey, IrisObject obj, int cx, int cy, int cz, int anchorY, int tallestKx, int tallestKz, Map<Long, int[]> columnStats) { private static void logFootprintDiagnostic(String cacheKey, IrisObject obj, int cx, int cy, int cz, int anchorY, int tallestKx, int tallestKz, Map<Long, int[]> columnStats) {
@@ -232,6 +248,18 @@ public class FloatingObjectFootprint {
return tallestKz; return tallestKz;
} }
public int getHighestSolidKeyY() {
return highestSolidKeyY;
}
public int getTallestKxBottom() {
return tallestKxBottom;
}
public int getTallestKzBottom() {
return tallestKzBottom;
}
public long[] footprintXZ() { public long[] footprintXZ() {
return footprintXZ; return footprintXZ;
} }
@@ -176,6 +176,9 @@ public class IrisBiome extends IrisRegistrant implements IRare {
@ArrayType(min = 1, type = IrisDepositGenerator.class) @ArrayType(min = 1, type = IrisDepositGenerator.class)
@Desc("Define biome deposit generators that add onto the existing regional and global deposit generators") @Desc("Define biome deposit generators that add onto the existing regional and global deposit generators")
private KList<IrisDepositGenerator> deposits = new KList<>(); private KList<IrisDepositGenerator> deposits = new KList<>();
@ArrayType(min = 1, type = IrisDepositVariant.class)
@Desc("Deposit ore remap rules scoped to this biome. Each entry declares a vertical band and a source->replacement block id map. Applied before regional and dimension rules; first matching biome rule wins.")
private KList<IrisDepositVariant> depositVariants = new KList<>();
private transient InferredType inferredType; private transient InferredType inferredType;
@Desc("Collection of ores to be generated") @Desc("Collection of ores to be generated")
@ArrayType(type = IrisOreGenerator.class, min = 1) @ArrayType(type = IrisOreGenerator.class, min = 1)
@@ -0,0 +1,87 @@
/*
* 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.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.MaxNumber;
import art.arcane.iris.engine.object.annotations.MinNumber;
import art.arcane.iris.engine.object.annotations.Required;
import art.arcane.iris.engine.object.annotations.Snippet;
import art.arcane.iris.util.common.data.B;
import art.arcane.volmlib.util.collection.KMap;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.bukkit.Material;
import org.bukkit.block.data.BlockData;
@Snippet("deposit-variant")
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
@Desc("Remaps ore block ids to alternate block ids within a vertical band. Ores declared at dimension, region, and biome scope can be rewritten at placement time (for example, iron_ore -> deepslate_iron_ore inside a deep carving band, or yourmod:iron -> yourmod:moon_iron inside a lunar biome).")
@Data
public class IrisDepositVariant {
private final transient AtomicCache<KMap<Material, BlockData>> resolved = new AtomicCache<>();
@Required
@MinNumber(-2048)
@MaxNumber(8192)
@Desc("Inclusive minimum world Y this variant applies at.")
private int minHeight = 0;
@Required
@MinNumber(-2048)
@MaxNumber(8192)
@Desc("Inclusive maximum world Y this variant applies at.")
private int maxHeight = 0;
@Required
@Desc("Source block id (for example `minecraft:iron_ore`) -> replacement block id (for example `minecraft:deepslate_iron_ore`). Any block id the data loader resolves is accepted, including external/mod blocks. Source match is by material only, so block properties on the source key are ignored.")
private KMap<String, String> remap = new KMap<>();
public BlockData remapOrNull(BlockData ore, IrisData rdata) {
if (ore == null || remap == null || remap.isEmpty()) {
return null;
}
KMap<Material, BlockData> map = resolved.aquire(() -> buildResolved(rdata));
return map.get(ore.getMaterial());
}
private KMap<Material, BlockData> buildResolved(IrisData rdata) {
KMap<Material, BlockData> out = new KMap<>();
for (java.util.Map.Entry<String, String> entry : remap.entrySet()) {
BlockData source = B.getOrNull(entry.getKey(), false);
BlockData target = B.getOrNull(entry.getValue(), true);
if (source == null || target == null) {
continue;
}
out.put(source.getMaterial(), target);
}
return out;
}
}
@@ -236,6 +236,9 @@ public class IrisDimension extends IrisRegistrant {
@ArrayType(min = 1, type = IrisDepositGenerator.class) @ArrayType(min = 1, type = IrisDepositGenerator.class)
@Desc("Define global deposit generators") @Desc("Define global deposit generators")
private KList<IrisDepositGenerator> deposits = new KList<>(); private KList<IrisDepositGenerator> deposits = new KList<>();
@ArrayType(min = 1, type = IrisDepositVariant.class)
@Desc("Dimension-wide deposit ore remap rules. Each entry declares a vertical band and a source->replacement block id map. Applied after biome and region rules; first matching dimension rule wins.")
private KList<IrisDepositVariant> depositVariants = new KList<>();
@ArrayType(min = 1, type = IrisShapedGeneratorStyle.class) @ArrayType(min = 1, type = IrisShapedGeneratorStyle.class)
@Desc("Overlay additional noise on top of the interoplated terrain.") @Desc("Overlay additional noise on top of the interoplated terrain.")
private KList<IrisShapedGeneratorStyle> overlayNoise = new KList<>(); private KList<IrisShapedGeneratorStyle> overlayNoise = new KList<>();
@@ -211,6 +211,45 @@ public class IrisFloatingChildBiomes implements IRare {
@Desc("Visualization color for this floating child in Iris Studio.") @Desc("Visualization color for this floating child in Iris Studio.")
private String color = null; private String color = null;
@Desc("Controls how topObjectOverrides are combined with the inherited surface objects from the target biome. INHERIT_ONLY (default) = behaves identically to before this field was added. MERGE = appends overrides after inherited objects. REPLACE = uses only overrides, ignoring all inherited objects.")
private OverrideMode topObjectMode = OverrideMode.INHERIT_ONLY;
@Desc("Controls how bottomObjectOverrides are combined. INHERIT_ONLY (default) = no bottom objects placed (there is no inherited bottom set). MERGE = same as REPLACE for bottom (no inherited source). REPLACE = uses bottomObjectOverrides list only.")
private OverrideMode bottomObjectMode = OverrideMode.INHERIT_ONLY;
@ArrayType(min = 1, type = IrisObjectPlacement.class)
@Desc("Object placements that override or supplement the inherited surface objects on the island TOP. Behaviour depends on topObjectMode. INHERIT_ONLY = this list is ignored. MERGE = appended after inherited. REPLACE = used instead of inherited.")
private KList<IrisObjectPlacement> topObjectOverrides = new KList<>();
@ArrayType(min = 1, type = IrisObjectPlacement.class)
@Desc("Object placements anchored to the island BOTTOM face. Each entry is auto-inverted 180 degrees around the X axis and placed flush against the lowest solid face of the island, so objects appear to hang upside-down from the underside. WARNING: directional blocks (stairs, doors, slabs) will not render correctly when flipped — use non-directional content (logs, leaves, stone, mycelium, ice, glass) for bottom placements.")
private KList<IrisObjectPlacement> bottomObjectOverrides = new KList<>();
public KList<IrisObjectPlacement> resolveTopObjects(IrisBiome target) {
KList<IrisObjectPlacement> surfaceObjects = (inheritObjects && target != null) ? target.getSurfaceObjects() : new KList<>();
return resolveTopObjectsFromSurface(surfaceObjects);
}
KList<IrisObjectPlacement> resolveTopObjectsFromSurface(KList<IrisObjectPlacement> surfaceObjects) {
return switch (topObjectMode) {
case REPLACE -> new KList<>(topObjectOverrides);
case MERGE -> {
KList<IrisObjectPlacement> merged = new KList<>();
merged.addAll(surfaceObjects);
merged.addAll(topObjectOverrides);
yield merged;
}
case INHERIT_ONLY -> surfaceObjects;
};
}
public KList<IrisObjectPlacement> resolveBottomObjects(IrisBiome target) {
return switch (bottomObjectMode) {
case INHERIT_ONLY -> new KList<>();
case MERGE, REPLACE -> bottomObjectOverrides;
};
}
public boolean hasObjectShrink() { public boolean hasObjectShrink() {
return objectShrinkFactor > 0 && objectShrinkFactor < 1.0; return objectShrinkFactor > 0 && objectShrinkFactor < 1.0;
} }
@@ -171,7 +171,7 @@ public class IrisGeneratorStyle {
} }
if (cng == null) { if (cng == null) {
cng = style.create(rng).bake(); cng = (style != null ? style : NoiseStyle.FLAT).create(rng).bake();
} }
cng = cng.scale(1D / zoom).pow(exponent).bake(); cng = cng.scale(1D / zoom).pow(exponent).bake();
@@ -205,7 +205,7 @@ public class IrisGeneratorStyle {
@SuppressWarnings("BooleanMethodIsAlwaysInverted") @SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean isFlat() { public boolean isFlat() {
return style.equals(NoiseStyle.FLAT); return style == null || style.equals(NoiseStyle.FLAT);
} }
public double getMaxFractureDistance() { public double getMaxFractureDistance() {
@@ -59,6 +59,22 @@ public class IrisObjectRotation {
@Desc("The z axis rotation") @Desc("The z axis rotation")
private IrisAxisRotationClamp zAxis = new IrisAxisRotationClamp(); private IrisAxisRotationClamp zAxis = new IrisAxisRotationClamp();
public static IrisObjectRotation xFlip180() {
IrisObjectRotation rt = new IrisObjectRotation();
IrisAxisRotationClamp rtx = new IrisAxisRotationClamp();
IrisAxisRotationClamp rty = new IrisAxisRotationClamp();
IrisAxisRotationClamp rtz = new IrisAxisRotationClamp();
rt.setEnabled(true);
rt.setXAxis(rtx);
rt.setYAxis(rty);
rt.setZAxis(rtz);
rtx.setEnabled(true);
rtx.minMax(180);
rty.setEnabled(false);
rtz.setEnabled(false);
return rt;
}
public static IrisObjectRotation of(double x, double y, double z) { public static IrisObjectRotation of(double x, double y, double z) {
IrisObjectRotation rt = new IrisObjectRotation(); IrisObjectRotation rt = new IrisObjectRotation();
IrisAxisRotationClamp rtx = new IrisAxisRotationClamp(); IrisAxisRotationClamp rtx = new IrisAxisRotationClamp();
@@ -141,6 +141,9 @@ public class IrisRegion extends IrisRegistrant implements IRare {
@ArrayType(min = 1, type = IrisDepositGenerator.class) @ArrayType(min = 1, type = IrisDepositGenerator.class)
@Desc("Define regional deposit generators that add onto the global deposit generators") @Desc("Define regional deposit generators that add onto the global deposit generators")
private KList<IrisDepositGenerator> deposits = new KList<>(); private KList<IrisDepositGenerator> deposits = new KList<>();
@ArrayType(min = 1, type = IrisDepositVariant.class)
@Desc("Deposit ore remap rules scoped to this region. Each entry declares a vertical band and a source->replacement block id map. Applied after biome rules but before dimension rules; first matching region rule wins.")
private KList<IrisDepositVariant> depositVariants = new KList<>();
@Desc("The style of rivers") @Desc("The style of rivers")
private IrisGeneratorStyle riverStyle = NoiseStyle.VASCULAR_THIN.style().zoomed(7.77); private IrisGeneratorStyle riverStyle = NoiseStyle.VASCULAR_THIN.style().zoomed(7.77);
@Desc("The style of lakes") @Desc("The style of lakes")
@@ -0,0 +1,33 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
@Desc("Controls how an override list is combined with the inherited object set from the target biome.")
public enum OverrideMode {
@Desc("Ignore the override list entirely. Use only the inherited objects from the target biome (subject to inheritObjects).")
INHERIT_ONLY,
@Desc("Append override list entries after the inherited objects from the target biome. Both sets are placed.")
MERGE,
@Desc("Use only the override list. The inherited objects from the target biome are discarded regardless of inheritObjects.")
REPLACE
}
@@ -149,7 +149,9 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
try { try {
INMS.get().inject(world.getSeed(), engine, world); INMS.get().inject(world.getSeed(), engine, world);
Iris.info("Injected Iris Biome Source into " + world.getName()); Iris.info("Injected Iris Biome Source into " + world.getName());
if (!studio) {
J.s(() -> updateSpawnLocation(world), 1); J.s(() -> updateSpawnLocation(world), 1);
}
} catch (Throwable e) { } catch (Throwable e) {
Iris.reportError(e); Iris.reportError(e);
Iris.error("Failed to inject biome source into " + world.getName()); Iris.error("Failed to inject biome source into " + world.getName());
@@ -0,0 +1,74 @@
package art.arcane.iris.core;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.lang.reflect.Method;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
public class ServerConfiguratorDatapackFingerprintTest {
@Rule
public TemporaryFolder tmp = new TemporaryFolder();
private Method fingerprintMethod() throws Exception {
try {
return ServerConfigurator.class.getMethod("computePackFingerprint", File.class);
} catch (NoSuchMethodException e) {
fail("ServerConfigurator.computePackFingerprint(File) does not exist yet — implement it in Task 2");
throw e;
}
}
@Test
public void computePackFingerprintReturnsSameHashForUnchangedFiles() throws Exception {
Method method = fingerprintMethod();
File packsDir = tmp.newFolder("packs");
File dimFile = new File(packsDir, "testpack/dimensions/overworld.json");
dimFile.getParentFile().mkdirs();
dimFile.createNewFile();
String fp1 = (String) method.invoke(null, packsDir);
String fp2 = (String) method.invoke(null, packsDir);
assertNotNull("Fingerprint must not be null", fp1);
assertEquals("Same unchanged files must produce identical fingerprint", fp1, fp2);
}
@Test
public void computePackFingerprintChangesWhenFileIsModified() throws Exception {
Method method = fingerprintMethod();
File packsDir = tmp.newFolder("packs");
File dimFile = new File(packsDir, "testpack/dimensions/overworld.json");
dimFile.getParentFile().mkdirs();
dimFile.createNewFile();
String fp1 = (String) method.invoke(null, packsDir);
dimFile.setLastModified(dimFile.lastModified() + 2000L);
String fp2 = (String) method.invoke(null, packsDir);
assertNotEquals("A modified file must produce a different fingerprint", fp1, fp2);
}
@Test
public void computePackFingerprintChangesWhenFileIsAdded() throws Exception {
Method method = fingerprintMethod();
File packsDir = tmp.newFolder("packs");
File dimDir = new File(packsDir, "testpack/dimensions");
dimDir.mkdirs();
File dimFile = new File(dimDir, "overworld.json");
dimFile.createNewFile();
String fp1 = (String) method.invoke(null, packsDir);
File extraFile = new File(dimDir, "nether.json");
extraFile.createNewFile();
String fp2 = (String) method.invoke(null, packsDir);
assertNotEquals("Adding a file must produce a different fingerprint", fp1, fp2);
}
}
@@ -0,0 +1,30 @@
package art.arcane.iris.core.runtime;
import org.junit.Test;
import java.util.Arrays;
import static org.junit.Assert.assertFalse;
public class StudioOpenCoordinatorSpawnStuckRegressionTest {
@Test
public void waitForSafeEntryRetryLoopIsRemoved() {
boolean found = Arrays.stream(StudioOpenCoordinator.class.getDeclaredMethods())
.anyMatch(m -> m.getName().equals("waitForSafeEntry"));
assertFalse("waitForSafeEntry retry loop must be removed — it burns up to 120s on ocean columns", found);
}
@Test
public void requestEntryChunkRedundantLoopIsRemoved() {
boolean found = Arrays.stream(StudioOpenCoordinator.class.getDeclaredMethods())
.anyMatch(m -> m.getName().equals("requestEntryChunk"));
assertFalse("requestEntryChunk must be removed — createLevel already loads (0,0)", found);
}
@Test
public void waitForEntryChunkRedundantLoopIsRemoved() {
boolean found = Arrays.stream(StudioOpenCoordinator.class.getDeclaredMethods())
.anyMatch(m -> m.getName().equals("waitForEntryChunk"));
assertFalse("waitForEntryChunk retry loop must be removed", found);
}
}
@@ -1,11 +1,16 @@
package art.arcane.iris.core.runtime; package art.arcane.iris.core.runtime;
import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import org.bukkit.HeightMap;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.block.Block;
import org.junit.Test; import org.junit.Test;
import org.mockito.Mockito;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
@@ -41,19 +46,21 @@ public class WorldRuntimeControlServiceSafeEntryTest {
} }
@Test @Test
public void scansFromRuntimeSurfaceBeforeFallingThroughRemainingWorldHeight() { public void resolvesSafeEntryImmediatelyWhenColumnIsAllWater() {
World world = mock(World.class); World world = mock(World.class);
Block stub = mock(Block.class, Mockito.RETURNS_DEEP_STUBS);
doReturn(-64).when(world).getMinHeight();
doReturn(320).when(world).getMaxHeight();
doReturn(true).when(world).isChunkLoaded(0, 0);
doReturn(62).when(world).getHighestBlockYAt(0, 0);
doReturn(62).when(world).getHighestBlockYAt(0, 0, HeightMap.MOTION_BLOCKING_NO_LEAVES);
doReturn(stub).when(world).getBlockAt(anyInt(), anyInt(), anyInt());
doReturn(0).when(world).getMinHeight(); Location source = new Location(world, 0.5D, 62D, 0.5D);
doReturn(256).when(world).getMaxHeight(); Location result = WorldRuntimeControlService.findTopSafeLocation(world, source);
doReturn(179).when(world).getHighestBlockYAt(0, 0);
int[] scanOrder = WorldRuntimeControlService.buildSafeLocationScanOrder(world, new Location(world, 0.5D, 96D, 0.5D)); assertNotNull("Safe entry must resolve to a non-null location even for water-only columns", result);
assertEquals(63, result.getBlockY());
assertEquals(180, scanOrder[0]);
assertEquals(179, scanOrder[1]);
assertEquals(1, scanOrder[179]);
assertEquals(181, scanOrder[180]);
assertEquals(254, scanOrder[scanOrder.length - 1]);
} }
} }
@@ -0,0 +1,42 @@
package art.arcane.iris.engine.mantle.components;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class MantleFloatingObjectComponentInvertedCountersTest {
@Test
public void resetObjectCounters_resetsAllInvertedCountersToZero() {
MantleFloatingObjectComponent.objectsInvertedAttempted.set(5);
MantleFloatingObjectComponent.objectsInvertedPlaced.set(3);
MantleFloatingObjectComponent.objectsInvertedSkippedNoFlat.set(2);
MantleFloatingObjectComponent.objectsInvertedFallbackNoInterior.set(1);
MantleFloatingObjectComponent.objectsInvertedSkippedShrink.set(4);
MantleFloatingObjectComponent.objectsInvertedSkippedNullObj.set(7);
MantleFloatingObjectComponent.writesDroppedAboveBottomTotal.set(11);
MantleFloatingObjectComponent.writesDroppedBottomOverhangTotal.set(9);
MantleFloatingObjectComponent.resetObjectCounters();
assertEquals(0, MantleFloatingObjectComponent.objectsInvertedAttempted.get());
assertEquals(0, MantleFloatingObjectComponent.objectsInvertedPlaced.get());
assertEquals(0, MantleFloatingObjectComponent.objectsInvertedSkippedNoFlat.get());
assertEquals(0, MantleFloatingObjectComponent.objectsInvertedFallbackNoInterior.get());
assertEquals(0, MantleFloatingObjectComponent.objectsInvertedSkippedShrink.get());
assertEquals(0, MantleFloatingObjectComponent.objectsInvertedSkippedNullObj.get());
assertEquals(0, MantleFloatingObjectComponent.writesDroppedAboveBottomTotal.get());
assertEquals(0, MantleFloatingObjectComponent.writesDroppedBottomOverhangTotal.get());
}
@Test
public void resetObjectCounters_alsoResetsExistingCounters_noRegression() {
MantleFloatingObjectComponent.objectsAttempted.set(99);
MantleFloatingObjectComponent.objectsPlaced.set(88);
MantleFloatingObjectComponent.resetObjectCounters();
assertEquals(0, MantleFloatingObjectComponent.objectsAttempted.get());
assertEquals(0, MantleFloatingObjectComponent.objectsPlaced.get());
}
}
@@ -0,0 +1,33 @@
package art.arcane.iris.engine.mantle.components;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
public class MantleObjectComponentCaveExposureTest {
@Test
public void test_resolveSurfaceObjectExclusionRadius_largeObject_coversFullFootprint() {
int radius = MantleObjectComponent.computeSurfaceExclusionRadius(24, 0, 0);
assertTrue("Expected radius >= 12 for a 24x24 object but got " + radius, radius >= 12);
}
@Test
public void test_resolveSurfaceObjectExclusionRadius_withTranslateOffset_includesOffset() {
int radius = MantleObjectComponent.computeSurfaceExclusionRadius(16, 5, 3);
int expected = 8 + 5 + 3 + 1;
assertTrue("Expected radius >= " + expected + " for 16x16 object with translate offsets 5+3 but got " + radius, radius >= expected);
}
@Test
public void test_resolveSurfaceObjectExclusionRadius_smallObject_atLeastOne() {
int radius = MantleObjectComponent.computeSurfaceExclusionRadius(2, 0, 0);
assertTrue("Expected radius >= 1 for a 2x2 object but got " + radius, radius >= 1);
}
@Test
public void test_resolveSurfaceObjectExclusionRadius_nullLike_returnsOne() {
int radius = MantleObjectComponent.computeSurfaceExclusionRadius(0, 0, 0);
assertTrue("Expected radius >= 1 for zero-dimension object but got " + radius, radius >= 1);
}
}
@@ -0,0 +1,75 @@
package art.arcane.iris.engine.object;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class FloatingIslandSampleBottomYTest {
private FloatingIslandSample buildSample(int islandBaseY, boolean[] solidMask) {
int topIdx = 0;
int solidCount = 0;
for (int i = solidMask.length - 1; i >= 0; i--) {
if (solidMask[i]) {
topIdx = i;
break;
}
}
for (boolean b : solidMask) {
if (b) solidCount++;
}
return FloatingIslandSample.constructForTest(islandBaseY, solidMask.length, topIdx, solidCount, solidMask);
}
@Test
public void bottomY_firstMaskTrue_returnsIslandBaseY() {
boolean[] mask = {true, true, false};
FloatingIslandSample sample = buildSample(100, mask);
assertEquals(100, sample.bottomY());
}
@Test
public void bottomY_lowestSolidAtOffset_returnsIslandBaseYPlusOffset() {
boolean[] mask = {false, false, true, true};
FloatingIslandSample sample = buildSample(50, mask);
assertEquals(52, sample.bottomY());
}
@Test
public void bottomY_allFalseMask_returnsNegativeOne() {
boolean[] mask = {false, false, false};
FloatingIslandSample sample = buildSample(100, mask);
assertEquals(-1, sample.bottomY());
}
@Test
public void bottomY_isCached_sameSampleReturnsSameValue() {
boolean[] mask = {false, true, true};
FloatingIslandSample sample = buildSample(200, mask);
int first = sample.bottomY();
int second = sample.bottomY();
assertEquals(first, second);
}
@Test
public void solidifyUncarvedInterior_fillsGapsBetweenSolids() {
boolean[] mask = {false, true, false, false, true, false};
int count = FloatingIslandSample.solidifyUncarvedInterior(mask);
assertEquals(4, count);
assertEquals(false, mask[0]);
assertEquals(true, mask[1]);
assertEquals(true, mask[2]);
assertEquals(true, mask[3]);
assertEquals(true, mask[4]);
assertEquals(false, mask[5]);
}
@Test
public void solidifyUncarvedInterior_emptyMaskStaysEmpty() {
boolean[] mask = {false, false, false};
int count = FloatingIslandSample.solidifyUncarvedInterior(mask);
assertEquals(0, count);
assertEquals(false, mask[0]);
assertEquals(false, mask[1]);
assertEquals(false, mask[2]);
}
}
@@ -0,0 +1,112 @@
package art.arcane.iris.engine.object;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class IrisFloatingChildBiomesResolverTest {
@Test
public void resolveTopObjects_inheritOnly_withInheritTrue_returnsSurfaceObjects() {
IrisObjectPlacement placement = new IrisObjectPlacement();
KList<IrisObjectPlacement> surface = new KList<>();
surface.add(placement);
IrisFloatingChildBiomes entry = new IrisFloatingChildBiomes();
entry.setInheritObjects(true);
entry.setTopObjectMode(OverrideMode.INHERIT_ONLY);
KList<IrisObjectPlacement> result = entry.resolveTopObjectsFromSurface(surface);
assertEquals(surface, result);
}
@Test
public void resolveTopObjects_replace_returnsTopOverridesIgnoringSurface() {
IrisObjectPlacement surfacePlacement = new IrisObjectPlacement();
IrisObjectPlacement overridePlacement = new IrisObjectPlacement();
KList<IrisObjectPlacement> surface = new KList<>();
surface.add(surfacePlacement);
KList<IrisObjectPlacement> overrides = new KList<>();
overrides.add(overridePlacement);
IrisFloatingChildBiomes entry = new IrisFloatingChildBiomes();
entry.setInheritObjects(true);
entry.setTopObjectMode(OverrideMode.REPLACE);
entry.setTopObjectOverrides(overrides);
KList<IrisObjectPlacement> result = entry.resolveTopObjectsFromSurface(surface);
assertEquals(1, result.size());
assertEquals(overridePlacement, result.get(0));
}
@Test
public void resolveTopObjects_merge_returnsCombinedSurfacePlusOverrides() {
IrisObjectPlacement surfacePlacement = new IrisObjectPlacement();
IrisObjectPlacement overridePlacement = new IrisObjectPlacement();
KList<IrisObjectPlacement> surface = new KList<>();
surface.add(surfacePlacement);
KList<IrisObjectPlacement> overrides = new KList<>();
overrides.add(overridePlacement);
IrisFloatingChildBiomes entry = new IrisFloatingChildBiomes();
entry.setInheritObjects(true);
entry.setTopObjectMode(OverrideMode.MERGE);
entry.setTopObjectOverrides(overrides);
KList<IrisObjectPlacement> result = entry.resolveTopObjectsFromSurface(surface);
assertEquals(2, result.size());
assertTrue(result.contains(surfacePlacement));
assertTrue(result.contains(overridePlacement));
}
@Test
public void resolveTopObjects_inheritOnly_emptySurface_returnsEmpty() {
IrisFloatingChildBiomes entry = new IrisFloatingChildBiomes();
entry.setTopObjectMode(OverrideMode.INHERIT_ONLY);
KList<IrisObjectPlacement> result = entry.resolveTopObjectsFromSurface(new KList<>());
assertTrue(result.isEmpty());
}
@Test
public void resolveBottomObjects_inheritOnly_returnsEmptyList() {
IrisFloatingChildBiomes entry = new IrisFloatingChildBiomes();
entry.setBottomObjectMode(OverrideMode.INHERIT_ONLY);
KList<IrisObjectPlacement> result = entry.resolveBottomObjects(null);
assertTrue(result.isEmpty());
}
@Test
public void resolveBottomObjects_replace_returnsBottomOverrides() {
IrisObjectPlacement overridePlacement = new IrisObjectPlacement();
KList<IrisObjectPlacement> overrides = new KList<>();
overrides.add(overridePlacement);
IrisFloatingChildBiomes entry = new IrisFloatingChildBiomes();
entry.setBottomObjectMode(OverrideMode.REPLACE);
entry.setBottomObjectOverrides(overrides);
KList<IrisObjectPlacement> result = entry.resolveBottomObjects(null);
assertEquals(1, result.size());
assertEquals(overridePlacement, result.get(0));
}
@Test
public void resolveTopObjects_inheritTrue_nullTargetProduces_emptySurface() {
IrisFloatingChildBiomes entry = new IrisFloatingChildBiomes();
entry.setInheritObjects(true);
entry.setTopObjectMode(OverrideMode.INHERIT_ONLY);
KList<IrisObjectPlacement> result = entry.resolveTopObjects(null);
assertTrue(result.isEmpty());
}
}
@@ -0,0 +1,41 @@
package art.arcane.iris.engine.object;
import org.bukkit.util.BlockVector;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class IrisObjectRotationFlipTest {
@Test
public void xFlip180_canRotateX_returnsTrue() {
IrisObjectRotation rot = IrisObjectRotation.xFlip180();
assertTrue(rot.canRotateX());
}
@Test
public void xFlip180_canRotateY_returnsFalse() {
IrisObjectRotation rot = IrisObjectRotation.xFlip180();
assertTrue(!rot.canRotateY());
}
@Test
public void xFlip180_rotateVector_negatesYandZ() {
IrisObjectRotation rot = IrisObjectRotation.xFlip180();
BlockVector v = new BlockVector(1, 2, 3);
BlockVector result = rot.rotate(v, 0, 0, 0);
assertEquals(1, result.getBlockX());
assertEquals(-2, result.getBlockY());
assertEquals(-3, result.getBlockZ());
}
@Test
public void xFlip180_rotateNegativeVector_negatesYandZ() {
IrisObjectRotation rot = IrisObjectRotation.xFlip180();
BlockVector v = new BlockVector(-3, -5, -7);
BlockVector result = rot.rotate(v, 0, 0, 0);
assertEquals(-3, result.getBlockX());
assertEquals(5, result.getBlockY());
assertEquals(7, result.getBlockZ());
}
}
@@ -0,0 +1,69 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.mantle.components.IslandObjectPlacer;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class IslandObjectPlacerAnchorFaceTest {
private FloatingIslandSample sampleWithBottomAt(int baseY, int bottomOffset) {
boolean[] mask = new boolean[10];
mask[bottomOffset] = true;
mask[9] = true;
return FloatingIslandSample.constructForTest(baseY, 10, 9, 2, mask);
}
@Test
public void bottomFace_getHighest_inFootprint_returnsSampleBottomY() {
FloatingIslandSample[] samples = new FloatingIslandSample[256];
samples[0] = sampleWithBottomAt(100, 0); // bottomY = 100
IslandObjectPlacer placer = new IslandObjectPlacer(null, samples, 0, 0, 100, IslandObjectPlacer.AnchorFace.BOTTOM);
int result = placer.getHighest(0, 0, null);
assertEquals(100, result);
}
@Test
public void bottomFace_getHighest_offFootprint_returnsChunkMinBottomY() {
FloatingIslandSample[] samples = new FloatingIslandSample[256];
samples[0] = sampleWithBottomAt(100, 0); // bottomY = 100, only sample
IslandObjectPlacer placer = new IslandObjectPlacer(null, samples, 0, 0, 100, IslandObjectPlacer.AnchorFace.BOTTOM);
// No sample at (15, 15) falls back to chunkMinIslandBottomY = 100
int result = placer.getHighest(15, 15, null);
assertEquals(100, result);
}
@Test
public void bottomFace_set_aboveAnchor_dropsWrite_andIncrementsDroppedAboveBottom() {
FloatingIslandSample[] samples = new FloatingIslandSample[256];
samples[0] = sampleWithBottomAt(100, 0); // bottomY = 100
IslandObjectPlacer placer = new IslandObjectPlacer(null, samples, 0, 0, 100, IslandObjectPlacer.AnchorFace.BOTTOM);
// y=101 >= anchorBottomY=100 in-footprint but above/at anchor dropped
placer.set(0, 101, 0, null);
assertEquals(1, placer.getWritesAttempted());
assertEquals(1, placer.getWritesDroppedAboveBottom());
}
@Test
public void topFace_existingConstructor_dropsBelowAnchor_noRegression() {
FloatingIslandSample[] samples = new FloatingIslandSample[256];
samples[0] = sampleWithBottomAt(100, 0);
// No sample at x=1, z=0 (idx=1)
// Existing single-face constructor defaults to TOP
IslandObjectPlacer placer = new IslandObjectPlacer(null, samples, 0, 0, 105);
// Off-footprint column, y=104 <= anchorTopY=105 dropped below
placer.set(1, 104, 0, null);
assertEquals(1, placer.getWritesAttempted());
assertEquals(1, placer.getWritesDroppedBelow());
}
}