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.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()
|
||||||
|
|||||||
+64
-17
@@ -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
|
||||||
|
|||||||
+160
-5
@@ -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);
|
||||||
|
|||||||
+13
-6
@@ -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());
|
||||||
|
|||||||
+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;
|
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]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+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