diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java b/core/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java index 486cb5a37..ffdb4e1f5 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java @@ -26,6 +26,7 @@ import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.ServerConfigurator; import art.arcane.iris.core.nms.datapack.DataVersion; import art.arcane.iris.core.runtime.ChunkClearer; +import art.arcane.iris.core.runtime.GoldenHashScanner; import art.arcane.iris.core.runtime.InPlaceChunkRegenerator; import art.arcane.iris.core.service.IrisEngineSVC; import art.arcane.iris.core.service.StudioSVC; @@ -361,4 +362,49 @@ public class CommandDeveloper implements DirectorExecutor { new InPlaceChunkRegenerator(world, engine, sender(), centerX, centerZ, radius).start(); } + @Director(name = "goldenhash", aliases = {"gold"}, description = "Generate chunks into buffers (no world writes) and hash blocks+biomes; captures a golden file or verifies against an existing one. Resets mantle in the scanned area - use on disposable test worlds.", origin = DirectorOrigin.BOTH) + public void goldenhash( + @Param(description = "The world to scan", contextual = true) + World world, + @Param(name = "radius", description = "Radius in chunks around the center", defaultValue = "8") + int radius, + @Param(name = "center-x", description = "Center chunk X", defaultValue = "0") + int centerX, + @Param(name = "center-z", description = "Center chunk Z", defaultValue = "0") + int centerZ, + @Param(name = "reset-mantle", description = "Delete mantle data in the scan area first for full regeneration from scratch", defaultValue = "true") + boolean resetMantle, + @Param(name = "threads", description = "Concurrent chunk generations; 1 = strictly serial for order-dependence testing", defaultValue = "8") + int threads, + @Param(name = "deep", description = "Also dump full per-chunk non-air blockstates for offline diffing", defaultValue = "false") + boolean deep + ) { + if (radius < 0) { + sender().sendMessage(C.RED + "Radius must be 0 or greater."); + return; + } + + if (world == null || !IrisToolbelt.isIrisWorld(world)) { + sender().sendMessage(C.RED + "Target must be an Iris world."); + return; + } + + PlatformChunkGenerator access = IrisToolbelt.access(world); + if (access == null || access.getEngine() == null) { + sender().sendMessage(C.RED + "The engine access for this world is null."); + return; + } + + int chunks = (radius * 2 + 1) * (radius * 2 + 1); + sender().sendMessage(C.GREEN + "GoldenHash started: " + C.GOLD + chunks + C.GREEN + + " chunk(s) around " + C.GOLD + centerX + "," + centerZ + C.GREEN + + " in buffers (world untouched)."); + Iris.info("goldenhash start: world=" + world.getName() + + " center=" + centerX + "," + centerZ + + " radius=" + radius + + " chunks=" + chunks); + + new GoldenHashScanner(world, access.getEngine(), sender(), centerX, centerZ, radius, resetMantle, threads, deep).start(); + } + } diff --git a/core/src/main/java/art/arcane/iris/core/runtime/GoldenHashScanner.java b/core/src/main/java/art/arcane/iris/core/runtime/GoldenHashScanner.java new file mode 100644 index 000000000..186b75cd8 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/GoldenHashScanner.java @@ -0,0 +1,443 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.runtime; + +import art.arcane.iris.Iris; +import art.arcane.iris.engine.data.chunk.TerrainChunk; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.mantle.EngineMantle; +import art.arcane.iris.util.common.format.C; +import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.block.Biome; +import org.bukkit.block.data.BlockData; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HexFormat; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicInteger; + +public final class GoldenHashScanner { + private static final AtomicInteger ACTIVE_SCANS = new AtomicInteger(0); + private static final String FORMAT = "iris-goldenhash v1"; + + public static boolean isScanActive() { + return ACTIVE_SCANS.get() > 0; + } + private static final int BIOME_STEP = 4; + private static final int MAX_REPORTED_MISMATCHES = 10; + private static final byte[] NULL_BIOME = "null\n".getBytes(StandardCharsets.UTF_8); + + private final World world; + private final Engine engine; + private final VolmitSender sender; + private final int centerChunkX; + private final int centerChunkZ; + private final int radius; + private final boolean resetMantle; + private final int threads; + private final boolean deep; + private final File goldenFile; + private final ChunkJobReporter reporter; + + public GoldenHashScanner(World world, Engine engine, VolmitSender sender, int centerChunkX, int centerChunkZ, int radius, boolean resetMantle, int threads, boolean deep) { + this.world = world; + this.engine = engine; + this.sender = sender; + this.centerChunkX = centerChunkX; + this.centerChunkZ = centerChunkZ; + this.radius = Math.max(0, radius); + this.resetMantle = resetMantle; + this.threads = Math.max(1, threads); + this.deep = deep; + this.goldenFile = Iris.instance.getDataFile("golden", engine.getDimension().getLoadKey() + + "-s" + world.getSeed() + + "-c" + centerChunkX + "x" + centerChunkZ + + "-r" + this.radius + ".hashes"); + this.reporter = new ChunkJobReporter(sender, "GoldenHash", world); + } + + public void start() { + reporter.start(); + Thread thread = new Thread(this::run, "Iris GoldenHash"); + thread.setDaemon(true); + thread.start(); + } + + private void run() { + boolean error = false; + ACTIVE_SCANS.incrementAndGet(); + try { + if (resetMantle) { + reporter.setStage("Resetting mantle"); + resetMantleFull(); + } + + List targets = ChunkJobReporter.orderedTargets(centerChunkX, centerChunkZ, radius); + reporter.setTotal(targets.size()); + reporter.setStage("Generating"); + Map lines = scan(targets); + + if (lines.size() != targets.size()) { + sender.sendMessage(C.RED + "GoldenHash aborted: " + (targets.size() - lines.size()) + " chunk(s) failed to generate. No golden file written."); + error = true; + return; + } + + reporter.setStage("Comparing"); + if (goldenFile.exists()) { + error = !verify(lines); + } else { + capture(lines); + } + } catch (Throwable e) { + Iris.reportError(e); + error = true; + } finally { + ACTIVE_SCANS.decrementAndGet(); + reporter.finish(error); + } + } + + private void resetMantleFull() { + Mantle mantle = engine.getMantle().getMantle(); + mantle.saveAll(); + File folder = mantle.getDataFolder(); + File[] files = folder.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isFile()) { + file.delete(); + } + } + } + } + + private Map scan(List targets) throws InterruptedException { + Map lines = new ConcurrentHashMap<>(); + Semaphore inFlight = new Semaphore(threads); + CountDownLatch done = new CountDownLatch(targets.size()); + + for (int[] target : targets) { + int chunkX = target[0]; + int chunkZ = target[1]; + + inFlight.acquire(); + MultiBurst.burst.lazy(() -> { + boolean ok = false; + try { + TerrainChunk buffer = TerrainChunk.create(world); + engine.generate(chunkX << 4, chunkZ << 4, buffer, false); + lines.put(chunkKey(chunkX, chunkZ), hashChunk(chunkX, chunkZ, buffer)); + if (deep) { + writeDeepDump(chunkX, chunkZ, buffer); + } + ok = true; + } catch (Throwable e) { + Iris.reportError(e); + } finally { + reporter.countApplied(ok); + inFlight.release(); + done.countDown(); + } + }); + } + + done.await(); + return lines; + } + + private String hashChunk(int chunkX, int chunkZ, TerrainChunk buffer) { + MessageDigest blockDigest = sha256(); + MessageDigest biomeDigest = sha256(); + int minY = buffer.getMinHeight(); + int maxY = buffer.getMaxHeight(); + IdentityHashMap blockCache = new IdentityHashMap<>(); + Map biomeCache = new HashMap<>(); + + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + for (int y = minY; y < maxY; y++) { + BlockData data = buffer.getBlockData(x, y, z); + byte[] bytes = blockCache.computeIfAbsent(data, d -> (d.getAsString() + "\n").getBytes(StandardCharsets.UTF_8)); + blockDigest.update(bytes); + } + } + } + + for (int x = 0; x < 16; x += BIOME_STEP) { + for (int z = 0; z < 16; z += BIOME_STEP) { + for (int y = minY; y < maxY; y += BIOME_STEP) { + Biome biome = buffer.getBiome(x, y, z); + byte[] bytes = biome == null + ? NULL_BIOME + : biomeCache.computeIfAbsent(biome, b -> (biomeKey(b) + "\n").getBytes(StandardCharsets.UTF_8)); + biomeDigest.update(bytes); + } + } + } + + return chunkX + " " + chunkZ + " " + + HexFormat.of().formatHex(blockDigest.digest()) + " " + + HexFormat.of().formatHex(biomeDigest.digest()); + } + + private void capture(Map lines) throws IOException { + List body = orderedBody(lines); + String combined = combinedHash(body); + List out = new ArrayList<>(); + out.add("#" + FORMAT); + out.add("#world=" + world.getName()); + out.add("#dim=" + engine.getDimension().getLoadKey()); + out.add("#seed=" + world.getSeed()); + out.add("#mc=" + Bukkit.getBukkitVersion()); + out.add("#minY=" + world.getMinHeight() + " maxY=" + world.getMaxHeight()); + out.add("#center=" + centerChunkX + "," + centerChunkZ); + out.add("#radius=" + radius); + out.addAll(body); + out.add("#combined=" + combined); + Files.write(goldenFile.toPath(), out, StandardCharsets.UTF_8); + + sender.sendMessage(C.GREEN + "Golden captured: " + C.GOLD + body.size() + " chunks" + C.GREEN + " combined=" + C.GOLD + shortHash(combined)); + sender.sendMessage(C.GRAY + goldenFile.getAbsolutePath()); + Iris.info("goldenhash captured: " + goldenFile.getAbsolutePath() + " combined=" + combined); + } + + private boolean verify(Map lines) throws IOException { + List existing = Files.readAllLines(goldenFile.toPath(), StandardCharsets.UTF_8); + Map meta = new HashMap<>(); + Map goldenChunks = new HashMap<>(); + for (String line : existing) { + if (line.startsWith("#")) { + int eq = line.indexOf('='); + if (eq > 0) { + meta.put(line.substring(1, eq), line.substring(eq + 1)); + } + } else if (!line.isBlank()) { + int second = line.indexOf(' ', line.indexOf(' ') + 1); + goldenChunks.put(line.substring(0, second), line); + } + } + + String expectedSeed = String.valueOf(world.getSeed()); + String expectedDim = engine.getDimension().getLoadKey(); + if (!expectedSeed.equals(meta.get("seed")) || !expectedDim.equals(meta.get("dim"))) { + sender.sendMessage(C.RED + "Golden file is for dim=" + meta.get("dim") + " seed=" + meta.get("seed") + + " but this world is dim=" + expectedDim + " seed=" + expectedSeed + ". Aborting."); + return false; + } + String mc = Bukkit.getBukkitVersion(); + if (!mc.equals(meta.get("mc"))) { + sender.sendMessage(C.YELLOW + "Golden was captured on mc=" + meta.get("mc") + ", running mc=" + mc + ". Diffs may be version-induced."); + } + + List body = orderedBody(lines); + List mismatches = new ArrayList<>(); + for (String line : body) { + int second = line.indexOf(' ', line.indexOf(' ') + 1); + String key = line.substring(0, second); + String golden = goldenChunks.get(key); + if (!line.equals(golden)) { + mismatches.add(key + (golden == null ? " (missing in golden)" : "")); + } + } + + String combined = combinedHash(body); + if (mismatches.isEmpty()) { + sender.sendMessage(C.GREEN + "GOLDEN MATCH: " + C.GOLD + body.size() + "/" + goldenChunks.size() + C.GREEN + + " chunks, combined=" + C.GOLD + shortHash(combined)); + Iris.info("goldenhash MATCH: " + goldenFile.getName() + " combined=" + combined); + return true; + } + + sender.sendMessage(C.RED + "GOLDEN MISMATCH: " + mismatches.size() + "/" + body.size() + " chunks differ."); + for (int i = 0; i < Math.min(MAX_REPORTED_MISMATCHES, mismatches.size()); i++) { + sender.sendMessage(C.RED + " chunk " + mismatches.get(i)); + } + if (mismatches.size() > MAX_REPORTED_MISMATCHES) { + sender.sendMessage(C.RED + " ... and " + (mismatches.size() - MAX_REPORTED_MISMATCHES) + " more"); + } + + File current = new File(goldenFile.getParentFile(), goldenFile.getName() + ".new"); + List out = new ArrayList<>(body); + out.add("#combined=" + combined); + Files.write(current.toPath(), out, StandardCharsets.UTF_8); + sender.sendMessage(C.YELLOW + "Current hashes written to " + current.getName()); + Iris.info("goldenhash MISMATCH: " + mismatches.size() + "/" + body.size() + " -> " + current.getAbsolutePath()); + + reporter.setStage("Diagnosing"); + diagnose(mismatches.getFirst()); + return false; + } + + private void diagnose(String mismatchKey) { + try { + String[] parts = mismatchKey.trim().split(" "); + int chunkX = Integer.parseInt(parts[0]); + int chunkZ = Integer.parseInt(parts[1]); + + TerrainChunk first = TerrainChunk.create(world); + engine.generate(chunkX << 4, chunkZ << 4, first, false); + TerrainChunk second = TerrainChunk.create(world); + engine.generate(chunkX << 4, chunkZ << 4, second, false); + + int minY = first.getMinHeight(); + int maxY = first.getMaxHeight(); + List diffs = new ArrayList<>(); + for (int x = 0; x < 16 && diffs.size() < 50; x++) { + for (int z = 0; z < 16 && diffs.size() < 50; z++) { + for (int y = minY; y < maxY && diffs.size() < 50; y++) { + String a = first.getBlockData(x, y, z).getAsString(); + String b = second.getBlockData(x, y, z).getAsString(); + if (!a.equals(b)) { + diffs.add(x + " " + y + " " + z + " | " + a + " | " + b); + } + } + } + } + + EngineMantle engineMantle = engine.getMantle(); + int margin = Math.max(engineMantle.getRadius(), engineMantle.getRealRadius()) + 1; + for (int dx = -margin; dx <= margin; dx++) { + for (int dz = -margin; dz <= margin; dz++) { + engineMantle.getMantle().deleteChunk(chunkX + dx, chunkZ + dz); + } + } + TerrainChunk reset = TerrainChunk.create(world); + engine.generate(chunkX << 4, chunkZ << 4, reset, false); + List mantleDiffs = new ArrayList<>(); + for (int x = 0; x < 16 && mantleDiffs.size() < 80; x++) { + for (int z = 0; z < 16 && mantleDiffs.size() < 80; z++) { + for (int y = minY; y < maxY && mantleDiffs.size() < 80; y++) { + String a = first.getBlockData(x, y, z).getAsString(); + String b = reset.getBlockData(x, y, z).getAsString(); + if (!a.equals(b)) { + mantleDiffs.add(x + " " + y + " " + z + " | scan: " + a + " | mantle-reset: " + b); + } + } + } + } + + List report = new ArrayList<>(); + report.add("#goldenhash diagnosis chunk=" + chunkX + "," + chunkZ); + report.add("#repeat-generation: " + (diffs.isEmpty() ? "STABLE (nondeterminism is order/state-dependent, not per-call)" : "UNSTABLE (" + diffs.size() + "+ diffs between two back-to-back generations)")); + report.addAll(diffs); + report.add("#mantle-reset regeneration: " + (mantleDiffs.isEmpty() ? "STABLE (mantle rebuild reproduces scan output)" : "DIVERGED (" + mantleDiffs.size() + "+ diffs - mantle build is state/order dependent)")); + report.addAll(mantleDiffs); + report.add("#full non-air dump of generation 1 follows (x y z state)"); + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + for (int y = minY; y < maxY; y++) { + String state = first.getBlockData(x, y, z).getAsString(); + if (!state.equals("minecraft:air") && !state.equals("minecraft:cave_air") && !state.equals("minecraft:void_air")) { + report.add(x + " " + y + " " + z + " " + state); + } + } + } + } + + File diag = new File(goldenFile.getParentFile(), goldenFile.getName() + ".diag-c" + chunkX + "x" + chunkZ + ".txt"); + Files.write(diag.toPath(), report, StandardCharsets.UTF_8); + sender.sendMessage((diffs.isEmpty() ? C.YELLOW + "Repeat-gen STABLE" : C.RED + "Repeat-gen UNSTABLE (" + diffs.size() + "+ block diffs)") + + C.GRAY + ", " + (mantleDiffs.isEmpty() ? C.YELLOW + "mantle-reset STABLE" : C.RED + "mantle-reset DIVERGED (" + mantleDiffs.size() + "+ diffs)") + + C.GRAY + " -> " + diag.getName()); + Iris.info("goldenhash diag: chunk=" + chunkX + "," + chunkZ + " repeatStable=" + diffs.isEmpty() + " -> " + diag.getAbsolutePath()); + } catch (Throwable e) { + Iris.reportError(e); + sender.sendMessage(C.RED + "Diagnosis failed: " + e.getMessage()); + } + } + + private List orderedBody(Map lines) { + Map sorted = new TreeMap<>(lines); + return new ArrayList<>(sorted.values()); + } + + private String combinedHash(List body) { + MessageDigest digest = sha256(); + for (String line : body) { + digest.update((line + "\n").getBytes(StandardCharsets.UTF_8)); + } + return HexFormat.of().formatHex(digest.digest()); + } + + private void writeDeepDump(int chunkX, int chunkZ, TerrainChunk buffer) throws IOException { + File dir = new File(goldenFile.getParentFile(), goldenFile.getName() + (goldenFile.exists() ? ".deep-verify" : ".deep")); + dir.mkdirs(); + int minY = buffer.getMinHeight(); + int maxY = buffer.getMaxHeight(); + List out = new ArrayList<>(); + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + for (int y = minY; y < maxY; y++) { + String state = buffer.getBlockData(x, y, z).getAsString(); + if (!state.equals("minecraft:air") && !state.equals("minecraft:cave_air") && !state.equals("minecraft:void_air")) { + out.add(x + " " + y + " " + z + " " + state); + } + } + } + } + Files.write(new File(dir, chunkX + "_" + chunkZ + ".txt").toPath(), out, StandardCharsets.UTF_8); + } + + private static String biomeKey(Biome biome) { + for (String method : new String[]{"getKeyOrNull", "getKeyOrThrow", "getKey"}) { + try { + Object key = Biome.class.getMethod(method).invoke(biome); + if (key != null) { + return key.toString(); + } + } catch (Throwable ignored) { + } + } + return biome.toString(); + } + + private static String shortHash(String hex) { + return hex.substring(0, 12); + } + + private static long chunkKey(int x, int z) { + return (((long) x) << 32) ^ (z & 0xFFFFFFFFL); + } + + private static MessageDigest sha256() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java b/core/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java index c01f923db..8fab1f93d 100644 --- a/core/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java +++ b/core/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java @@ -4,6 +4,7 @@ import com.google.common.util.concurrent.AtomicDouble; import art.arcane.iris.Iris; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.loader.ResourceLoader; +import art.arcane.iris.core.runtime.GoldenHashScanner; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.platform.PlatformChunkGenerator; @@ -310,6 +311,9 @@ public class IrisEngineSVC implements IrisService { if (shouldSkipForMaintenance(engineWorld)) { return; } + if (GoldenHashScanner.isScanActive()) { + return; + } try { engine.getMantle().trim(activeIdleDuration(engine), activeTectonicLimit(engine)); @@ -338,6 +342,10 @@ public class IrisEngineSVC implements IrisService { return; } + if (GoldenHashScanner.isScanActive()) { + return; + } + try { long unloadStart = System.currentTimeMillis(); int count = engine.getMantle().unloadTectonicPlate(IrisSettings.get().getPerformance().getEngineSVC().forceMulticoreWrite ? 0 : activeTectonicLimit(engine)); diff --git a/core/src/main/java/art/arcane/iris/engine/GenerationCacheWarmer.java b/core/src/main/java/art/arcane/iris/engine/GenerationCacheWarmer.java new file mode 100644 index 000000000..6aa09932a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/GenerationCacheWarmer.java @@ -0,0 +1,126 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.Iris; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDecorator; +import art.arcane.iris.engine.object.IrisObjectPlacement; +import art.arcane.iris.engine.object.IrisOreGenerator; +import art.arcane.iris.engine.object.IrisProceduralObjects; +import art.arcane.iris.engine.object.IrisProceduralPlacement; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.M; +import art.arcane.volmlib.util.math.RNG; + +import java.util.Comparator; + +public final class GenerationCacheWarmer { + private GenerationCacheWarmer() { + } + + public static void warm(Engine engine) { + long start = M.ms(); + IrisData data = engine.getData(); + RNG root = new RNG(engine.getSeedManager().getComponent() + 7777L); + int[] counter = {0}; + + try { + KList biomes = engine.getAllBiomes(); + biomes.sort(Comparator.comparing(IrisBiome::getLoadKey)); + for (IrisBiome biome : biomes) { + warmPlacements(biome.getObjects(), root, counter, data); + warmDecorators(biome.getDecorators(), root, counter, data); + warmOres(biome.getOres(), root, counter, data); + warmProcedural(biome.getProceduralObjects(), root, counter, data); + } + + KList regions = engine.getDimension().getAllRegions(engine); + regions.sort(Comparator.comparing(IrisRegion::getLoadKey)); + for (IrisRegion region : regions) { + warmPlacements(region.getObjects(), root, counter, data); + warmOres(region.getOres(), root, counter, data); + warmProcedural(region.getProceduralObjects(), root, counter, data); + } + + warmOres(engine.getDimension().getOres(), root, counter, data); + } catch (Throwable e) { + Iris.reportError(e); + Iris.warn("Generation cache warm pass failed: " + e.getMessage()); + } + + Iris.debug("[IrisEngine timing] cache warm " + counter[0] + " configs=" + (M.ms() - start) + "ms"); + } + + private static void warmPlacements(KList placements, RNG root, int[] counter, IrisData data) { + if (placements == null) { + return; + } + for (IrisObjectPlacement placement : placements) { + if (placement == null) { + continue; + } + RNG rng = root.nextParallelRNG(counter[0]++); + placement.getSurfaceWarp(rng, data); + placement.getDensity(rng, 0, 0, data); + } + } + + private static void warmDecorators(KList decorators, RNG root, int[] counter, IrisData data) { + if (decorators == null) { + return; + } + for (IrisDecorator decorator : decorators) { + if (decorator == null) { + continue; + } + RNG rng = root.nextParallelRNG(counter[0]++); + decorator.getHeightGenerator(rng, data); + decorator.getGenerator(rng, data); + decorator.getVarianceGenerator(rng, data); + } + } + + private static void warmOres(KList ores, RNG root, int[] counter, IrisData data) { + if (ores == null) { + return; + } + for (IrisOreGenerator ore : ores) { + if (ore == null) { + continue; + } + ore.warm(root.nextParallelRNG(counter[0]++), data); + } + } + + private static void warmProcedural(IrisProceduralObjects procedural, RNG root, int[] counter, IrisData data) { + if (procedural == null) { + return; + } + for (IrisProceduralPlacement placement : procedural.getAllPlacements()) { + if (placement == null) { + continue; + } + placement.getVariantObject(data, root.nextParallelRNG(counter[0]++)); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java index 1f50b2735..39fe0f686 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java @@ -161,6 +161,9 @@ public class IrisEngine implements Engine { _t0 = M.ms(); setupEngine(); Iris.debug("[IrisEngine timing] setupEngine total=" + (M.ms() - _t0) + "ms"); + _t0 = M.ms(); + GenerationCacheWarmer.warm(this); + Iris.debug("[IrisEngine timing] cache warm total=" + (M.ms() - _t0) + "ms"); Iris.debug("Engine Initialized " + getCacheID()); } diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/MatterGenerator.java b/core/src/main/java/art/arcane/iris/engine/mantle/MatterGenerator.java index 1cb429872..1f2c1ab59 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/MatterGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/MatterGenerator.java @@ -92,7 +92,7 @@ public interface MatterGenerator { int finalPassZ = passZ; MantleChunk finalChunk = chunk; MantleComponent finalComponent = component; - Runnable task = () -> finalChunk.raiseFlagUnchecked(finalComponent.getFlag(), + Runnable task = () -> finalChunk.raiseFlagSuspend(finalComponent.getFlag(), () -> finalComponent.generateLayer(writer, finalPassX, finalPassZ, context)); if (multicore) { diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java index 5804242b9..9d04f9e76 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java @@ -34,6 +34,8 @@ import art.arcane.iris.engine.object.IrisRange; import art.arcane.iris.util.project.context.ChunkContext; import art.arcane.iris.util.project.stream.ProceduralStream; import art.arcane.iris.util.project.stream.utility.ChunkFillableDoubleStream2D; +import art.arcane.iris.util.simd.SimdKernels; +import art.arcane.iris.util.simd.SimdSupport; import art.arcane.volmlib.util.documentation.ChunkCoordinates; import art.arcane.volmlib.util.mantle.flag.ReservedFlag; import art.arcane.volmlib.util.math.PowerOfTwoCoordinates; @@ -162,9 +164,11 @@ public class MantleCarvingComponent extends IrisMantleComponent { IrisCaveProfile[] profileField = blendScratch.profileField; Map columnProfileWeights = blendScratch.columnProfileWeights; IdentityHashMap activeProfiles = blendScratch.activeProfiles; + List profileOrder = blendScratch.profileOrder; IrisCaveProfile[] kernelProfiles = blendScratch.kernelProfiles; double[] kernelProfileWeights = blendScratch.kernelProfileWeights; activeProfiles.clear(); + profileOrder.clear(); fillProfileField(profileField, chunkX, chunkZ, complex, resolverState, blendScratch); for (int localX = 0; localX < CHUNK_SIZE; localX++) { @@ -217,34 +221,31 @@ public class MantleCarvingComponent extends IrisMantleComponent { } else if (!activeProfiles.containsKey(profile)) { Arrays.fill(weights, 0D); } - activeProfiles.put(profile, Boolean.TRUE); + if (activeProfiles.put(profile, Boolean.TRUE) == null) { + profileOrder.add(profile); + } weights[columnIndex] = columnWeight; } } } List columnWeightedProfiles = new ArrayList<>(); - for (IrisCaveProfile profile : activeProfiles.keySet()) { + for (IrisCaveProfile profile : profileOrder) { double[] weights = columnProfileWeights.get(profile); if (weights == null) { continue; } - double totalWeight = 0D; - double maxWeight = 0D; - for (double weight : weights) { - totalWeight += weight; - if (weight > maxWeight) { - maxWeight = weight; - } - } + SimdKernels kernels = SimdSupport.kernels(); + double totalWeight = kernels.sum(weights, weights.length); + double maxWeight = kernels.max(weights, weights.length); if (maxWeight < MIN_WEIGHT) { continue; } double averageWeight = totalWeight / CHUNK_AREA; - columnWeightedProfiles.add(new WeightedProfile(profile, weights, averageWeight, null)); + columnWeightedProfiles.add(new WeightedProfile(profile, weights, averageWeight, null, columnWeightedProfiles.size())); } List blendedProfiles = limitAndMergeBlendedProfiles(columnWeightedProfiles, MAX_BLENDED_PROFILE_PASSES, CHUNK_AREA); @@ -277,6 +278,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { buildDimensionColumnPlan(columnPlan, chunkX, chunkZ, entry, resolverState); Map rootProfileColumnWeights = new IdentityHashMap<>(); + List rootProfileOrder = new ArrayList<>(); IrisRange worldYRange = entry.getWorldYRange(); for (int columnIndex = 0; columnIndex < CHUNK_AREA; columnIndex++) { IrisDimensionCarvingEntry resolvedEntry = columnPlan[columnIndex]; @@ -290,14 +292,17 @@ public class MantleCarvingComponent extends IrisMantleComponent { continue; } - double[] columnWeights = rootProfileColumnWeights.computeIfAbsent(profile, key -> new double[CHUNK_AREA]); + double[] columnWeights = rootProfileColumnWeights.get(profile); + if (columnWeights == null) { + columnWeights = new double[CHUNK_AREA]; + rootProfileColumnWeights.put(profile, columnWeights); + rootProfileOrder.add(profile); + } columnWeights[columnIndex] = 1D; } - List> profileEntries = new ArrayList<>(rootProfileColumnWeights.entrySet()); - profileEntries.sort((a, b) -> Integer.compare(a.getKey().hashCode(), b.getKey().hashCode())); - for (Map.Entry profileEntry : profileEntries) { - weightedProfiles.add(new WeightedProfile(profileEntry.getKey(), profileEntry.getValue(), -1D, worldYRange)); + for (IrisCaveProfile profile : rootProfileOrder) { + weightedProfiles.add(new WeightedProfile(profile, rootProfileColumnWeights.get(profile), -1D, worldYRange, weightedProfiles.size())); } } @@ -545,7 +550,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { List mergedProfiles = new ArrayList<>(); for (WeightedProfile keptProfile : keptProfiles) { double averageWeight = computeAverageWeight(keptProfile.columnWeights, areaSize); - mergedProfiles.add(new WeightedProfile(keptProfile.profile, keptProfile.columnWeights, averageWeight, keptProfile.worldYRange)); + mergedProfiles.add(new WeightedProfile(keptProfile.profile, keptProfile.columnWeights, averageWeight, keptProfile.worldYRange, keptProfile.sequence)); } mergedProfiles.sort(MantleCarvingComponent::compareByCarveOrder); return mergedProfiles; @@ -556,7 +561,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { if (weightOrder != 0) { return weightOrder; } - return Integer.compare(profileSortKey(a.profile), profileSortKey(b.profile)); + return Integer.compare(a.sequence, b.sequence); } private static int compareByCarveOrder(WeightedProfile a, WeightedProfile b) { @@ -564,14 +569,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { if (weightOrder != 0) { return weightOrder; } - return Integer.compare(profileSortKey(a.profile), profileSortKey(b.profile)); - } - - private static int profileSortKey(IrisCaveProfile profile) { - if (profile == null) { - return 0; - } - return profile.hashCode(); + return Integer.compare(a.sequence, b.sequence); } private static double computeAverageWeight(double[] weights) { @@ -607,12 +605,14 @@ public class MantleCarvingComponent extends IrisMantleComponent { private final double[] columnWeights; private final double averageWeight; private final IrisRange worldYRange; + private final int sequence; - private WeightedProfile(IrisCaveProfile profile, double[] columnWeights, double averageWeight, IrisRange worldYRange) { + private WeightedProfile(IrisCaveProfile profile, double[] columnWeights, double averageWeight, IrisRange worldYRange, int sequence) { this.profile = profile; this.columnWeights = columnWeights; this.averageWeight = averageWeight; this.worldYRange = worldYRange; + this.sequence = sequence; } private double averageWeight() { @@ -627,6 +627,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { private final IdentityHashMap columnProfileWeights = new IdentityHashMap<>(); private final IdentityHashMap dimensionColumnPlans = new IdentityHashMap<>(); private final IdentityHashMap activeProfiles = new IdentityHashMap<>(); + private final List profileOrder = new ArrayList<>(); private final double[] fieldSurfaceHeights = new double[FIELD_SIZE * FIELD_SIZE]; private final IrisRegion[] fieldRegions = new IrisRegion[FIELD_SIZE * FIELD_SIZE]; private final IrisBiome[] fieldSurfaceBiomes = new IrisBiome[FIELD_SIZE * FIELD_SIZE]; diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java index 77155305a..062049e56 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java @@ -1381,8 +1381,10 @@ public class MantleObjectComponent extends IrisMantleComponent { vacuumPlacements.add(j); } } + updateProceduralRadiusBounds(region.getProceduralObjects(), xg, zg); } for (IrisBiome biome : dimension.getAllBiomes(this::getData)) { + updateProceduralRadiusBounds(biome.getProceduralObjects(), xg, zg); for (IrisObjectPlacement j : biome.getObjects()) { if (j.getScale().canScaleBeyond()) { scalars.put(j.getScale(), j.getPlace()); @@ -1414,6 +1416,24 @@ public class MantleObjectComponent extends IrisMantleComponent { return Math.max(xg.get(), zg.get()); } + private void updateProceduralRadiusBounds(IrisProceduralObjects procedural, AtomicInteger xg, AtomicInteger zg) { + if (procedural == null || procedural.isEmpty()) { + return; + } + for (IrisProceduralPlacement placement : procedural.getAllPlacements()) { + if (placement == null) { + continue; + } + for (IrisObject variant : placement.getVariantObjects(getData())) { + if (variant == null) { + continue; + } + xg.getAndSet(Math.max(variant.getW(), xg.get())); + zg.getAndSet(Math.max(variant.getD(), zg.get())); + } + } + } + private void updateRadiusBounds( KMap sizeCache, AtomicInteger xg, diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisPostModifier.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisPostModifier.java index 7ac27ceaf..1b0fdc309 100644 --- a/core/src/main/java/art/arcane/iris/engine/modifier/IrisPostModifier.java +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisPostModifier.java @@ -225,14 +225,17 @@ public class IrisPostModifier extends EngineAssignedModifier { // Foliage b = getPostBlock(x, h + 1, z, currentPostX, currentPostZ, currentData); - if (B.isVineBlock(b) && b instanceof MultipleFacing f) { + if (B.isVineBlock(b) && b instanceof MultipleFacing) { + MultipleFacing f = (MultipleFacing) b.clone(); int finalH = h + 1; f.getAllowedFaces().forEach(face -> { BlockData d = getPostBlock(x + face.getModX(), finalH + face.getModY(), z + face.getModZ(), currentPostX, currentPostZ, currentData); f.setFace(face, !B.isAir(d) && !B.isVineBlock(d)); }); - setPostBlock(x, h + 1, z, b, currentPostX, currentPostZ, currentData); + if (!f.equals(b)) { + setPostBlock(x, h + 1, z, f, currentPostX, currentPostZ, currentData); + } } if (B.isFoliage(b) || b.getMaterial().equals(Material.DEAD_BUSH)) { diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java index 88d1f6eb9..976ea3797 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java @@ -698,13 +698,19 @@ public class IrisObject extends IrisRegistrant { } double newRotation = config.getRotation().getYAxis().getMin() + slopeRotationY; + IrisObjectRotation originalRotation = config.getRotation(); + IrisObjectRotation slopeRotation = new IrisObjectRotation(); + slopeRotation.setXAxis(originalRotation.getXAxis()); + slopeRotation.setZAxis(originalRotation.getZAxis()); if (newRotation == 0) { - config.getRotation().setYAxis(new IrisAxisRotationClamp(false, false, 0, 0, 90)); - config.getRotation().setEnabled(config.getRotation().canRotateX() || config.getRotation().canRotateZ()); + slopeRotation.setYAxis(new IrisAxisRotationClamp(false, false, 0, 0, 90)); + slopeRotation.setEnabled(originalRotation.canRotateX() || originalRotation.canRotateZ()); } else { - config.getRotation().setYAxis(new IrisAxisRotationClamp(true, false, newRotation, newRotation, 90)); - config.getRotation().setEnabled(true); + slopeRotation.setYAxis(new IrisAxisRotationClamp(true, false, newRotation, newRotation, 90)); + slopeRotation.setEnabled(true); } + config = config.toPlacement(config.getPlace().toArray(new String[0])); + config.setRotation(slopeRotation); } } @@ -1048,7 +1054,9 @@ public class IrisObject extends IrisRegistrant { } if (placer.isPreventingDecay() && (data) instanceof Leaves && !((Leaves) (data)).isPersistent()) { - ((Leaves) data).setPersistent(true); + Leaves leaves = (Leaves) data.clone(); + leaves.setPersistent(true); + data = leaves; } for (IrisObjectReplace j : config.getEdit()) { @@ -1103,17 +1111,24 @@ public class IrisObject extends IrisRegistrant { } if (data instanceof Waterlogged && shouldAutoWaterlogBlock(placer, config, yv, xx, yy, zz)) { - ((Waterlogged) data).setWaterlogged(true); + Waterlogged waterlogged = (Waterlogged) data.clone(); + waterlogged.setWaterlogged(true); + data = waterlogged; } if (B.isVineBlock(data)) { - MultipleFacing f = (MultipleFacing) data; + MultipleFacing f = (MultipleFacing) data.clone(); + boolean facesChanged = false; for (BlockFace face : f.getAllowedFaces()) { BlockData facingBlock = placer.get(xx + face.getModX(), yy + face.getModY(), zz + face.getModZ()); if (B.isSolid(facingBlock) && !B.isVineBlock(facingBlock)) { f.setFace(face, true); + facesChanged = true; } } + if (facesChanged) { + data = f; + } } if (listener != null) { @@ -1314,8 +1329,11 @@ public class IrisObject extends IrisRegistrant { int highest = placer.getHighest(xx, zz, getLoader(), true); - if (d instanceof Waterlogged && shouldAutoWaterlogBlock(placer, config, yv, xx, highest, zz)) - ((Waterlogged) d).setWaterlogged(true); + if (d instanceof Waterlogged && shouldAutoWaterlogBlock(placer, config, yv, xx, highest, zz)) { + Waterlogged waterlogged = (Waterlogged) d.clone(); + waterlogged.setWaterlogged(true); + d = waterlogged; + } int lowerBound = highest - 1; if (settings != null) { @@ -1352,13 +1370,18 @@ public class IrisObject extends IrisRegistrant { } if (B.isVineBlock(d)) { - MultipleFacing f = (MultipleFacing) d; + MultipleFacing f = (MultipleFacing) d.clone(); + boolean facesChanged = false; for (BlockFace face : f.getAllowedFaces()) { BlockData facingBlock = placer.get(xx + face.getModX(), j + face.getModY(), zz + face.getModZ()); if (B.isSolid(facingBlock) && !B.isVineBlock(facingBlock)) { f.setFace(face, true); + facesChanged = true; } } + if (facesChanged) { + d = f; + } } placer.set(xx, j, zz, d); } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacement.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacement.java index f3660fba8..67f6e68b8 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacement.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacement.java @@ -21,6 +21,7 @@ package art.arcane.iris.engine.object; import art.arcane.iris.Iris; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.annotations.*; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; @@ -202,8 +203,11 @@ public class IrisObjectPlacement { } public CNG getSurfaceWarp(RNG rng, IrisData data) { - return surfaceWarp.aquire(() -> - getWarp().create(rng, data)); + return surfaceWarp.aquire(() -> { + Engine engine = data.getEngine(); + RNG warpRng = engine != null ? new RNG(engine.getSeedManager().getComponent() + 1024) : new RNG(8675309L); + return getWarp().create(warpRng, data); + }); } public double warp(RNG rng, double x, double y, double z, IrisData data) { diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisOreGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisOreGenerator.java index 63065776b..98e2a6881 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisOreGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisOreGenerator.java @@ -48,6 +48,11 @@ public class IrisOreGenerator { private transient AtomicCache chanceCache = new AtomicCache<>(); + public void warm(RNG rng, IrisData data) { + chanceCache.aquire(() -> chanceStyle.create(rng, data)); + palette.getLayerGenerator(rng, data); + } + public BlockData generate(int x, int y, int z, RNG rng, IrisData data) { if (palette.getPalette().isEmpty()) { return null; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisProceduralPlacement.java b/core/src/main/java/art/arcane/iris/engine/object/IrisProceduralPlacement.java index d0a8ed67c..904c471b4 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisProceduralPlacement.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisProceduralPlacement.java @@ -19,6 +19,7 @@ package art.arcane.iris.engine.object; import art.arcane.iris.core.loader.IrisData; +import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.math.RNG; /** @@ -40,4 +41,6 @@ public interface IrisProceduralPlacement { IrisObjectPlacement asPlacement(); IrisObject getVariantObject(IrisData data, RNG rng); + + KList getVariantObjects(IrisData data); } diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponentTop2BlendTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponentTop2BlendTest.java index c25edac81..ca11cf63b 100644 --- a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponentTop2BlendTest.java +++ b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponentTop2BlendTest.java @@ -23,7 +23,7 @@ public class MantleCarvingComponentTop2BlendTest { @BeforeClass public static void setup() throws Exception { Class weightedProfileClass = Class.forName("art.arcane.iris.engine.mantle.components.MantleCarvingComponent$WeightedProfile"); - weightedProfileConstructor = weightedProfileClass.getDeclaredConstructor(IrisCaveProfile.class, double[].class, double.class, Class.forName("art.arcane.iris.engine.object.IrisRange")); + weightedProfileConstructor = weightedProfileClass.getDeclaredConstructor(IrisCaveProfile.class, double[].class, double.class, Class.forName("art.arcane.iris.engine.object.IrisRange"), int.class); weightedProfileConstructor.setAccessible(true); limitMethod = MantleCarvingComponent.class.getDeclaredMethod("limitAndMergeBlendedProfiles", List.class, int.class, int.class); limitMethod.setAccessible(true); @@ -76,6 +76,33 @@ public class MantleCarvingComponentTop2BlendTest { assertEquals(1.0D, byProfile.get(second)[254], 0D); } + @Test + public void topTwoTieBreaksBySequenceNotIdentity() throws Exception { + IrisCaveProfile first = new IrisCaveProfile().setEnabled(true).setBaseWeight(1.0D); + IrisCaveProfile second = new IrisCaveProfile().setEnabled(true).setBaseWeight(1.0D); + IrisCaveProfile third = new IrisCaveProfile().setEnabled(true).setBaseWeight(1.0D); + + double[] weightsA = new double[256]; + double[] weightsB = new double[256]; + double[] weightsC = new double[256]; + weightsA[0] = 0.5D; + weightsB[0] = 0.5D; + weightsC[0] = 0.5D; + + List weighted = new ArrayList<>(); + weighted.add(weightedProfileConstructor.newInstance(first, weightsA, average(weightsA), null, 0)); + weighted.add(weightedProfileConstructor.newInstance(second, weightsB, average(weightsB), null, 1)); + weighted.add(weightedProfileConstructor.newInstance(third, weightsC, average(weightsC), null, 2)); + + List limited = invokeLimit(weighted, 2); + assertEquals(2, limited.size()); + + Map byProfile = extractWeightsByProfile(limited); + assertEquals(true, byProfile.containsKey(first)); + assertEquals(true, byProfile.containsKey(second)); + assertEquals(false, byProfile.containsKey(third)); + } + private WeightedInput createWeightedProfiles() throws Exception { IrisCaveProfile first = new IrisCaveProfile().setEnabled(true).setBaseWeight(1.31D); IrisCaveProfile second = new IrisCaveProfile().setEnabled(true).setBaseWeight(1.17D); @@ -99,9 +126,9 @@ public class MantleCarvingComponentTop2BlendTest { thirdWeights[255] = 0.4D; List weighted = new ArrayList<>(); - weighted.add(weightedProfileConstructor.newInstance(first, firstWeights, average(firstWeights), null)); - weighted.add(weightedProfileConstructor.newInstance(second, secondWeights, average(secondWeights), null)); - weighted.add(weightedProfileConstructor.newInstance(third, thirdWeights, average(thirdWeights), null)); + weighted.add(weightedProfileConstructor.newInstance(first, firstWeights, average(firstWeights), null, 0)); + weighted.add(weightedProfileConstructor.newInstance(second, secondWeights, average(secondWeights), null, 1)); + weighted.add(weightedProfileConstructor.newInstance(third, thirdWeights, average(thirdWeights), null, 2)); return new WeightedInput(weighted, profiles); }