mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-05-19 16:10:42 +00:00
Content
This commit is contained in:
Vendored
+1
-1
@@ -1 +1 @@
|
||||
-1935789196
|
||||
1435163759
|
||||
@@ -42,7 +42,14 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
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.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicIntegerArray;
|
||||
@@ -172,6 +179,70 @@ public class ServerConfigurator {
|
||||
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() {
|
||||
String forcedMainWorld = IrisSettings.get().getGeneral().forceMainWorld;
|
||||
if (forcedMainWorld != null && !forcedMainWorld.isBlank()) {
|
||||
|
||||
@@ -47,14 +47,14 @@ final class PaperLikeRuntimeBackend implements WorldLifecycleBackend {
|
||||
if (capabilities.paperLikeFlavor() == CapabilitySnapshot.PaperLikeFlavor.CURRENT_INFO_AND_DATA) {
|
||||
Object dimensionKey = WorldLifecycleSupport.createDimensionKey(stemKey);
|
||||
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 worldDataAndGenSettings = WorldLifecycleSupport.createCurrentWorldDataAndSettings(capabilities, request.worldName());
|
||||
capabilities.createLevelMethod().invoke(capabilities.minecraftServer(), levelStem, worldLoadingInfoAndData, worldDataAndGenSettings);
|
||||
} else {
|
||||
legacyStorageAccess = WorldLifecycleSupport.createLegacyStorageAccess(capabilities, 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -118,14 +118,16 @@ public final class StudioOpenCoordinator {
|
||||
throw new IllegalStateException("Studio entry anchor could not be resolved.");
|
||||
}
|
||||
|
||||
long deadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(120L);
|
||||
updateStage(request, "request_entry_chunk", 0.84D);
|
||||
requestEntryChunk(world, entryAnchor, deadline);
|
||||
|
||||
updateStage(request, "resolve_safe_entry", 0.90D);
|
||||
Location safeEntry = resolveSafeEntry(world, entryAnchor, deadline);
|
||||
updateStage(request, "resolve_safe_entry", 0.84D);
|
||||
Location safeEntry;
|
||||
try {
|
||||
safeEntry = WorldRuntimeControlService.get().resolveSafeEntry(world, entryAnchor)
|
||||
.get(5L, TimeUnit.SECONDS);
|
||||
} catch (TimeoutException e) {
|
||||
throw new IllegalStateException("Studio entry point resolution timed out — region thread may be stalled.");
|
||||
}
|
||||
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()) {
|
||||
@@ -135,8 +137,7 @@ public final class StudioOpenCoordinator {
|
||||
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(remaining, TimeUnit.MILLISECONDS);
|
||||
Boolean teleported = WorldRuntimeControlService.get().teleport(player, safeEntry).get(10L, TimeUnit.SECONDS);
|
||||
if (!Boolean.TRUE.equals(teleported)) {
|
||||
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(
|
||||
PlatformChunkGenerator provider,
|
||||
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) {
|
||||
if (worldName == null || !isWorldFamilyLoaded(worldName) || System.currentTimeMillis() >= deadline) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
@@ -444,32 +381,6 @@ public final class StudioOpenCoordinator {
|
||||
}, 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) {
|
||||
Throwable cursor = throwable;
|
||||
while (cursor instanceof CompletionException || cursor instanceof ExecutionException) {
|
||||
|
||||
@@ -14,11 +14,9 @@ import io.papermc.lib.PaperLib;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.GameRule;
|
||||
import org.bukkit.HeightMap;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Tag;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.world.TimeSkipEvent;
|
||||
import org.bukkit.plugin.PluginManager;
|
||||
@@ -227,21 +225,19 @@ public final class WorldRuntimeControlService {
|
||||
|
||||
int chunkX = source.getBlockX() >> 4;
|
||||
int chunkZ = source.getBlockZ() >> 4;
|
||||
return requestChunkAsync(world, chunkX, chunkZ, true).thenCompose(chunk -> {
|
||||
CompletableFuture<Location> future = new CompletableFuture<>();
|
||||
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
|
||||
try {
|
||||
future.complete(findTopSafeLocation(world, source));
|
||||
} catch (Throwable e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
if (!scheduled) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException("Failed to schedule safe-entry surface resolve for " + world.getName() + "@" + chunkX + "," + chunkZ + "."));
|
||||
CompletableFuture<Location> future = new CompletableFuture<>();
|
||||
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
|
||||
try {
|
||||
future.complete(findTopSafeLocation(world, source));
|
||||
} catch (Throwable t) {
|
||||
future.completeExceptionally(t);
|
||||
}
|
||||
|
||||
return future;
|
||||
});
|
||||
if (!scheduled) {
|
||||
future.completeExceptionally(new IllegalStateException(
|
||||
"Failed to schedule safe-entry resolve for " + world.getName() + "@" + chunkX + "," + chunkZ + "."));
|
||||
}
|
||||
return future;
|
||||
}
|
||||
|
||||
public CompletableFuture<Boolean> teleport(Player player, Location location) {
|
||||
@@ -312,67 +308,15 @@ public final class WorldRuntimeControlService {
|
||||
int z = source.getBlockZ();
|
||||
float yaw = source.getYaw();
|
||||
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 maxY = world.getMaxHeight() - 2;
|
||||
int[] scanOrder = new int[maxY - minY + 1];
|
||||
int index = 0;
|
||||
|
||||
int runtimeSurface = world.getHighestBlockYAt((int) source.getX(), (int) source.getZ());
|
||||
int startY = Math.min(maxY, runtimeSurface + 1);
|
||||
|
||||
for (int y = startY; y >= minY; y--) {
|
||||
scanOrder[index++] = y;
|
||||
if (world.isChunkLoaded(x >> 4, z >> 4)) {
|
||||
int raw = world.getHighestBlockYAt(x, z, HeightMap.MOTION_BLOCKING_NO_LEAVES);
|
||||
int y = Math.max(minY, Math.min(maxY, raw + 1));
|
||||
return new Location(world, x + 0.5D, y, z + 0.5D, yaw, pitch);
|
||||
}
|
||||
|
||||
for (int y = startY + 1; y <= maxY; y++) {
|
||||
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;
|
||||
int y = Math.max(minY, Math.min(maxY, source.getBlockY()));
|
||||
return new Location(world, x + 0.5D, y, z + 0.5D, yaw, pitch);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
||||
@@ -40,7 +40,6 @@ import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.exceptions.IrisException;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
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.scheduling.J;
|
||||
import art.arcane.volmlib.util.scheduling.FoliaScheduler;
|
||||
@@ -143,16 +142,6 @@ public class IrisCreator {
|
||||
}
|
||||
|
||||
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");
|
||||
IrisDimension d = IrisToolbelt.getDimension(dimension());
|
||||
|
||||
@@ -185,7 +174,7 @@ public class IrisCreator {
|
||||
if (!studio()) {
|
||||
IrisWorlds.get().put(name(), dimension());
|
||||
}
|
||||
ServerConfigurator.installDataPacks(!studio());
|
||||
ServerConfigurator.installDataPacksIfChanged(!studio());
|
||||
reportStudioProgress(0.40D, "install_datapacks");
|
||||
|
||||
PlatformChunkGenerator access = (PlatformChunkGenerator) wc.generator();
|
||||
|
||||
@@ -129,23 +129,31 @@ public class IrisEngine implements Engine {
|
||||
wallClock = new AtomicRollingSequence(32);
|
||||
lastGPS = new AtomicLong(M.ms());
|
||||
generated = new AtomicInteger(0);
|
||||
long _t0 = M.ms();
|
||||
mantle = new IrisEngineMantle(this);
|
||||
Iris.info("[IrisEngine timing] new IrisEngineMantle=" + (M.ms() - _t0) + "ms");
|
||||
context = new IrisContext(this);
|
||||
cleaning = new AtomicBoolean(false);
|
||||
modeFallbackLogged = new AtomicBoolean(false);
|
||||
if (studio) {
|
||||
_t0 = M.ms();
|
||||
getData().dump();
|
||||
getData().clearLists();
|
||||
getTarget().setDimension(getData().getDimensionLoader().load(getDimension().getLoadKey()));
|
||||
Iris.info("[IrisEngine timing] dump+clearLists+reload=" + (M.ms() - _t0) + "ms");
|
||||
}
|
||||
context.touch();
|
||||
getData().setEngine(this);
|
||||
_t0 = M.ms();
|
||||
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());
|
||||
failing = false;
|
||||
closed = false;
|
||||
art = J.ar(this::tickRandomPlayer, 0);
|
||||
_t0 = M.ms();
|
||||
setupEngine();
|
||||
Iris.info("[IrisEngine timing] setupEngine total=" + (M.ms() - _t0) + "ms");
|
||||
Iris.debug("Engine Initialized " + getCacheID());
|
||||
}
|
||||
|
||||
@@ -208,15 +216,27 @@ public class IrisEngine implements Engine {
|
||||
closing.set(false);
|
||||
Iris.debug("Setup Engine " + getCacheID());
|
||||
cacheId = RNG.r.nextInt();
|
||||
long t0 = M.ms();
|
||||
complex = ensureComplex();
|
||||
Iris.info("[IrisEngine timing] ensureComplex=" + (M.ms() - t0) + "ms");
|
||||
t0 = M.ms();
|
||||
upperContext = buildUpperContext();
|
||||
Iris.info("[IrisEngine timing] buildUpperContext=" + (M.ms() - t0) + "ms");
|
||||
t0 = M.ms();
|
||||
effects = new IrisEngineEffects(this);
|
||||
Iris.info("[IrisEngine timing] IrisEngineEffects=" + (M.ms() - t0) + "ms");
|
||||
hash32 = new CompletableFuture<>();
|
||||
t0 = M.ms();
|
||||
mantle.hotload();
|
||||
Iris.info("[IrisEngine timing] mantle.hotload=" + (M.ms() - t0) + "ms");
|
||||
t0 = M.ms();
|
||||
setupMode();
|
||||
Iris.info("[IrisEngine timing] setupMode=" + (M.ms() - t0) + "ms");
|
||||
t0 = M.ms();
|
||||
IrisWorldManager manager = new IrisWorldManager(this);
|
||||
worldManager = manager;
|
||||
manager.startManager();
|
||||
Iris.info("[IrisEngine timing] IrisWorldManager=" + (M.ms() - t0) + "ms");
|
||||
J.a(this::computeBiomeMaxes);
|
||||
J.a(() -> {
|
||||
File[] roots = getData().getLoaders()
|
||||
|
||||
+72
-25
@@ -31,33 +31,46 @@ import org.jetbrains.annotations.Nullable;
|
||||
public class IslandObjectPlacer implements IObjectPlacer {
|
||||
private static final int OVERHANG_RADIUS = 2;
|
||||
|
||||
public enum AnchorFace { TOP, BOTTOM }
|
||||
|
||||
private final MantleWriter wrapped;
|
||||
private final FloatingIslandSample[] samples;
|
||||
private final boolean[] overhangAllowed;
|
||||
private final int minX;
|
||||
private final int minZ;
|
||||
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 writesDroppedBelow;
|
||||
private int writesDroppedOverhang;
|
||||
private int writesDroppedAboveBottom;
|
||||
private int writesDroppedBottomOverhang;
|
||||
|
||||
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.samples = samples;
|
||||
this.minX = minX;
|
||||
this.minZ = minZ;
|
||||
this.anchorTopY = anchorTopY;
|
||||
int maxY = -1;
|
||||
this.anchorY = anchorY;
|
||||
this.face = face;
|
||||
int maxTopY = -1;
|
||||
int minBottomY = Integer.MAX_VALUE;
|
||||
for (FloatingIslandSample s : samples) {
|
||||
if (s != null) {
|
||||
int ty = s.topY();
|
||||
if (ty > maxY) {
|
||||
maxY = ty;
|
||||
}
|
||||
if (ty > maxTopY) maxTopY = ty;
|
||||
int by = s.bottomY();
|
||||
if (by >= 0 && by < minBottomY) minBottomY = by;
|
||||
}
|
||||
}
|
||||
this.chunkMaxIslandTopY = maxY;
|
||||
this.chunkMaxIslandTopY = maxTopY;
|
||||
this.chunkMinIslandBottomY = (minBottomY == Integer.MAX_VALUE) ? -1 : minBottomY;
|
||||
this.overhangAllowed = buildOverhangMask(samples);
|
||||
}
|
||||
|
||||
@@ -104,6 +117,14 @@ public class IslandObjectPlacer implements IObjectPlacer {
|
||||
return writesDroppedOverhang;
|
||||
}
|
||||
|
||||
public int getWritesDroppedAboveBottom() {
|
||||
return writesDroppedAboveBottom;
|
||||
}
|
||||
|
||||
public int getWritesDroppedBottomOverhang() {
|
||||
return writesDroppedBottomOverhang;
|
||||
}
|
||||
|
||||
private boolean shouldSkipAirColumn(int x, int y, int z) {
|
||||
writesAttempted++;
|
||||
int xf = x - minX;
|
||||
@@ -111,21 +132,46 @@ public class IslandObjectPlacer implements IObjectPlacer {
|
||||
if (xf >= 0 && xf < 16 && zf >= 0 && zf < 16) {
|
||||
int idx = (zf << 4) | xf;
|
||||
if (samples[idx] != null) {
|
||||
if (face == AnchorFace.TOP) {
|
||||
return false;
|
||||
}
|
||||
if (y >= anchorY) {
|
||||
writesDroppedAboveBottom++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (y <= anchorTopY) {
|
||||
writesDroppedBelow++;
|
||||
return true;
|
||||
}
|
||||
if (!overhangAllowed[idx]) {
|
||||
writesDroppedOverhang++;
|
||||
return true;
|
||||
if (face == AnchorFace.TOP) {
|
||||
if (y <= anchorY) {
|
||||
writesDroppedBelow++;
|
||||
return true;
|
||||
}
|
||||
if (!overhangAllowed[idx]) {
|
||||
writesDroppedOverhang++;
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (y >= anchorY) {
|
||||
writesDroppedBottomOverhang++;
|
||||
return true;
|
||||
}
|
||||
if (!overhangAllowed[idx]) {
|
||||
writesDroppedBottomOverhang++;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (y <= anchorTopY) {
|
||||
writesDroppedBelow++;
|
||||
return true;
|
||||
if (face == AnchorFace.TOP) {
|
||||
if (y <= anchorY) {
|
||||
writesDroppedBelow++;
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (y >= anchorY) {
|
||||
writesDroppedBottomOverhang++;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
writesDroppedOverhang++;
|
||||
return true;
|
||||
@@ -143,19 +189,20 @@ public class IslandObjectPlacer implements IObjectPlacer {
|
||||
@Override
|
||||
public int getHighest(int x, int z, IrisData data) {
|
||||
FloatingIslandSample s = sampleAt(x, z);
|
||||
if (s != null) {
|
||||
return s.topY();
|
||||
if (face == AnchorFace.TOP) {
|
||||
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
|
||||
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
|
||||
FloatingIslandSample s = sampleAt(x, z);
|
||||
if (s != null) {
|
||||
return s.topY();
|
||||
}
|
||||
return chunkMaxIslandTopY;
|
||||
return getHighest(x, z, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+164
-9
@@ -57,6 +57,14 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
public static final AtomicLong writesAttemptedTotal = new AtomicLong();
|
||||
public static final AtomicLong writesDroppedBelowTotal = 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 AtomicLong heavyClipWarnings = new AtomicLong();
|
||||
private static final int HEAVY_CLIP_WARNING_CAP = 30;
|
||||
@@ -83,6 +91,14 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
writesDroppedOverhangTotal.set(0);
|
||||
heavyClipWarnings.set(0);
|
||||
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) {
|
||||
@@ -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) {
|
||||
if (terrainMismatchWarnings.get() >= TERRAIN_MISMATCH_WARNING_CAP) {
|
||||
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();
|
||||
boolean hasSurface = surface != null && !surface.isEmpty();
|
||||
boolean hasExtras = extras != null && !extras.isEmpty();
|
||||
KList<Integer> interior = null;
|
||||
if (hasSurface || hasExtras) {
|
||||
KList<Integer> interior = interiorColumns(samples, columns);
|
||||
interior = interiorColumns(samples, columns);
|
||||
if (hasSurface) {
|
||||
for (IrisObjectPlacement placement : surface) {
|
||||
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) {
|
||||
int tallestKx = fp.getTallestKx();
|
||||
int tallestKz = fp.getTallestKz();
|
||||
@@ -449,14 +603,15 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
for (IrisFloatingChildBiomes entry : entries) {
|
||||
collectPlacementKeys(entry.getFloatingObjects(), objectKeys);
|
||||
collectPlacementKeys(entry.getExtraObjects(), objectKeys);
|
||||
if (entry.isInheritObjects()) {
|
||||
try {
|
||||
IrisBiome target = entry.getRealBiome(biome, data);
|
||||
if (target != null) {
|
||||
collectPlacementKeys(target.getSurfaceObjects(), objectKeys);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
collectPlacementKeys(entry.getTopObjectOverrides(), objectKeys);
|
||||
collectPlacementKeys(entry.getBottomObjectOverrides(), objectKeys);
|
||||
try {
|
||||
IrisBiome target = entry.getRealBiome(biome, data);
|
||||
if (target != null) {
|
||||
collectPlacementKeys(entry.resolveTopObjects(target), objectKeys);
|
||||
collectPlacementKeys(entry.resolveBottomObjects(target), objectKeys);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-6
@@ -392,8 +392,8 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
int xx = rng.i(x, x + 15);
|
||||
int zz = rng.i(z, z + 15);
|
||||
int surfaceObjectExclusionDepth = resolveSurfaceObjectExclusionDepth(surfaceObjectExclusionBaseDepth, v);
|
||||
int surfaceObjectExclusionRadius = resolveSurfaceObjectExclusionRadius(v);
|
||||
int surfaceObjectExclusionDepth = resolveSurfaceObjectExclusionDepth(surfaceObjectExclusionBaseDepth, v, objectPlacement);
|
||||
int surfaceObjectExclusionRadius = resolveSurfaceObjectExclusionRadius(v, objectPlacement);
|
||||
boolean overCave = surfaceObjectExclusionDepth > 0 && hasSurfaceCarveExposure(writer, surfaceHeightLookup, xx, zz, surfaceObjectExclusionDepth, surfaceObjectExclusionRadius);
|
||||
int id = rng.i(0, Integer.MAX_VALUE);
|
||||
IrisObjectPlacement effectivePlacement = resolveEffectivePlacement(objectPlacement, v);
|
||||
@@ -1038,23 +1038,30 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
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) {
|
||||
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)));
|
||||
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) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -117,6 +117,8 @@ public class IrisDepositModifier extends EngineAssignedModifier<BlockData> {
|
||||
if (y > k.getMaxHeight() || y < k.getMinHeight() || y > height - 2)
|
||||
continue;
|
||||
|
||||
IrisDimension dimension = getDimension();
|
||||
|
||||
for (BlockVector j : clump.getBlocks().keys()) {
|
||||
int nx = j.getBlockX() + x;
|
||||
int ny = j.getBlockY() + y;
|
||||
@@ -130,9 +132,63 @@ public class IrisDepositModifier extends EngineAssignedModifier<BlockData> {
|
||||
}
|
||||
|
||||
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()
|
||||
+ " writeDropOverhang=" + MantleFloatingObjectComponent.writesDroppedOverhangTotal.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())
|
||||
+ " topFloors:" + (topFloors.length() == 0 ? " <none>" : topFloors.toString()));
|
||||
}
|
||||
|
||||
@@ -92,6 +92,7 @@ public final class FloatingIslandSample {
|
||||
public final int topIdx;
|
||||
public final int solidCount;
|
||||
public final boolean[] solidMask;
|
||||
private transient int cachedBottomIdx = -2;
|
||||
|
||||
private FloatingIslandSample(IrisFloatingChildBiomes entry, int islandBaseY, int thickness, int topIdx, int solidCount, boolean[] solidMask) {
|
||||
this.entry = entry;
|
||||
@@ -102,10 +103,27 @@ public final class FloatingIslandSample {
|
||||
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() {
|
||||
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) {
|
||||
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) {
|
||||
return reject(REJECT_NO_SOLID);
|
||||
}
|
||||
@@ -268,6 +291,36 @@ public final class FloatingIslandSample {
|
||||
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) {
|
||||
int maxTopHeight = Math.max(0, entry.getMaxTopHeight());
|
||||
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 final int lowestSolidKeyY;
|
||||
private final int highestSolidKeyY;
|
||||
private final int centerX;
|
||||
private final int centerY;
|
||||
private final int centerZ;
|
||||
private final int tallestKx;
|
||||
private final int tallestKz;
|
||||
private final int tallestKxBottom;
|
||||
private final int tallestKzBottom;
|
||||
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.highestSolidKeyY = highestSolidKeyY;
|
||||
this.centerX = centerX;
|
||||
this.centerY = centerY;
|
||||
this.centerZ = centerZ;
|
||||
this.tallestKx = tallestKx;
|
||||
this.tallestKz = tallestKz;
|
||||
this.tallestKxBottom = tallestKxBottom;
|
||||
this.tallestKzBottom = tallestKzBottom;
|
||||
this.footprintXZ = footprintXZ;
|
||||
}
|
||||
|
||||
@@ -63,6 +69,10 @@ public class FloatingObjectFootprint {
|
||||
int cz = obj.getCenter().getBlockZ();
|
||||
Map<Long, int[]> columnStats = new HashMap<>();
|
||||
|
||||
int[] globalHighestY = {Integer.MIN_VALUE};
|
||||
int[] globalHighestKx = {0};
|
||||
int[] globalHighestKz = {0};
|
||||
|
||||
obj.getBlocks().forEach((BlockVector key, BlockData bd) -> {
|
||||
if (!B.isSolid(bd)) {
|
||||
return;
|
||||
@@ -81,6 +91,11 @@ public class FloatingObjectFootprint {
|
||||
}
|
||||
stats[1]++;
|
||||
}
|
||||
if (ky > globalHighestY[0]) {
|
||||
globalHighestY[0] = ky;
|
||||
globalHighestKx[0] = kx;
|
||||
globalHighestKz[0] = kz;
|
||||
}
|
||||
});
|
||||
|
||||
long[] footprintArray = new long[columnStats.size()];
|
||||
@@ -90,15 +105,16 @@ public class FloatingObjectFootprint {
|
||||
}
|
||||
|
||||
long tallestPacked = resolveTallestColumn(columnStats);
|
||||
int lowestSolidKeyY = columnStats.isEmpty()
|
||||
? cy
|
||||
: columnStats.get(tallestPacked)[0];
|
||||
int lowestSolidKeyY = columnStats.isEmpty() ? cy : columnStats.get(tallestPacked)[0];
|
||||
int highestSolidKeyY = columnStats.isEmpty() ? cy : globalHighestY[0];
|
||||
int tallestKx = columnStats.isEmpty() ? 0 : (int) (tallestPacked >> 32);
|
||||
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) {
|
||||
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) {
|
||||
@@ -232,6 +248,18 @@ public class FloatingObjectFootprint {
|
||||
return tallestKz;
|
||||
}
|
||||
|
||||
public int getHighestSolidKeyY() {
|
||||
return highestSolidKeyY;
|
||||
}
|
||||
|
||||
public int getTallestKxBottom() {
|
||||
return tallestKxBottom;
|
||||
}
|
||||
|
||||
public int getTallestKzBottom() {
|
||||
return tallestKzBottom;
|
||||
}
|
||||
|
||||
public long[] footprintXZ() {
|
||||
return footprintXZ;
|
||||
}
|
||||
|
||||
@@ -176,6 +176,9 @@ public class IrisBiome extends IrisRegistrant implements IRare {
|
||||
@ArrayType(min = 1, type = IrisDepositGenerator.class)
|
||||
@Desc("Define biome deposit generators that add onto the existing regional and global deposit generators")
|
||||
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;
|
||||
@Desc("Collection of ores to be generated")
|
||||
@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)
|
||||
@Desc("Define global deposit generators")
|
||||
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)
|
||||
@Desc("Overlay additional noise on top of the interoplated terrain.")
|
||||
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.")
|
||||
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() {
|
||||
return objectShrinkFactor > 0 && objectShrinkFactor < 1.0;
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ public class IrisGeneratorStyle {
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -205,7 +205,7 @@ public class IrisGeneratorStyle {
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean isFlat() {
|
||||
return style.equals(NoiseStyle.FLAT);
|
||||
return style == null || style.equals(NoiseStyle.FLAT);
|
||||
}
|
||||
|
||||
public double getMaxFractureDistance() {
|
||||
|
||||
@@ -59,6 +59,22 @@ public class IrisObjectRotation {
|
||||
@Desc("The z axis rotation")
|
||||
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) {
|
||||
IrisObjectRotation rt = new IrisObjectRotation();
|
||||
IrisAxisRotationClamp rtx = new IrisAxisRotationClamp();
|
||||
|
||||
@@ -141,6 +141,9 @@ public class IrisRegion extends IrisRegistrant implements IRare {
|
||||
@ArrayType(min = 1, type = IrisDepositGenerator.class)
|
||||
@Desc("Define regional deposit generators that add onto the global deposit generators")
|
||||
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")
|
||||
private IrisGeneratorStyle riverStyle = NoiseStyle.VASCULAR_THIN.style().zoomed(7.77);
|
||||
@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 {
|
||||
INMS.get().inject(world.getSeed(), engine, world);
|
||||
Iris.info("Injected Iris Biome Source into " + world.getName());
|
||||
J.s(() -> updateSpawnLocation(world), 1);
|
||||
if (!studio) {
|
||||
J.s(() -> updateSpawnLocation(world), 1);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
Iris.error("Failed to inject biome source into " + world.getName());
|
||||
|
||||
+74
@@ -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);
|
||||
}
|
||||
}
|
||||
+30
@@ -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);
|
||||
}
|
||||
}
|
||||
+18
-11
@@ -1,11 +1,16 @@
|
||||
package art.arcane.iris.core.runtime;
|
||||
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import org.bukkit.HeightMap;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
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.mock;
|
||||
|
||||
@@ -41,19 +46,21 @@ public class WorldRuntimeControlServiceSafeEntryTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scansFromRuntimeSurfaceBeforeFallingThroughRemainingWorldHeight() {
|
||||
public void resolvesSafeEntryImmediatelyWhenColumnIsAllWater() {
|
||||
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();
|
||||
doReturn(256).when(world).getMaxHeight();
|
||||
doReturn(179).when(world).getHighestBlockYAt(0, 0);
|
||||
Location source = new Location(world, 0.5D, 62D, 0.5D);
|
||||
Location result = WorldRuntimeControlService.findTopSafeLocation(world, source);
|
||||
|
||||
int[] scanOrder = WorldRuntimeControlService.buildSafeLocationScanOrder(world, new Location(world, 0.5D, 96D, 0.5D));
|
||||
|
||||
assertEquals(180, scanOrder[0]);
|
||||
assertEquals(179, scanOrder[1]);
|
||||
assertEquals(1, scanOrder[179]);
|
||||
assertEquals(181, scanOrder[180]);
|
||||
assertEquals(254, scanOrder[scanOrder.length - 1]);
|
||||
assertNotNull("Safe entry must resolve to a non-null location even for water-only columns", result);
|
||||
assertEquals(63, result.getBlockY());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+42
@@ -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());
|
||||
}
|
||||
}
|
||||
+33
@@ -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]);
|
||||
}
|
||||
}
|
||||
+112
@@ -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());
|
||||
}
|
||||
}
|
||||
+69
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user