mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-11-01 08:50:06 +00:00
Compare commits
64
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0860f94fc | ||
|
|
2462330787 | ||
|
|
c2eca3e6bd | ||
|
|
304e66b74b | ||
|
|
f1c82e8cec | ||
|
|
9339c0869e | ||
|
|
bb32ea9d25 | ||
|
|
7ff4292013 | ||
|
|
1ed1044474 | ||
|
|
b00e28e42e | ||
|
|
b61d2f305b | ||
|
|
618bb28591 | ||
|
|
6ceda7c53b | ||
|
|
b475d20aec | ||
|
|
e9c18cdc08 | ||
|
|
b10966af47 | ||
|
|
8e625d4547 | ||
|
|
0d148c69b5 | ||
|
|
806e847e90 | ||
|
|
ed83c6fdeb | ||
|
|
b2990104d2 | ||
|
|
def73cff96 | ||
|
|
ca33273d17 | ||
|
|
8773d4b008 | ||
|
|
1a914a404a | ||
|
|
1899de7f3d | ||
|
|
3c39092cc7 | ||
|
|
0c7adc5da4 | ||
|
|
a879985875 | ||
|
|
c700138b66 | ||
|
|
21ab5baf27 | ||
|
|
58ab3fcbbe | ||
|
|
b6e9f8a14d | ||
|
|
0d76d81dab | ||
|
|
aec8f5be8e | ||
|
|
0f9c045524 | ||
|
|
1d4f95829b | ||
|
|
ce4771b609 | ||
|
|
7d73c4d150 | ||
|
|
b3579e623a | ||
|
|
300ecbfc8b | ||
|
|
0c760d9256 | ||
|
|
3dc958edcf | ||
|
|
f64a7c6753 | ||
|
|
7756598057 | ||
|
|
75837d757b | ||
|
|
2690e0878b | ||
|
|
494cdb1a5c | ||
|
|
1d182769d8 | ||
|
|
584e8758a7 | ||
|
|
9f49d29e2b | ||
|
|
e3bc27d50c | ||
|
|
6900f270ee | ||
|
|
db0f98d103 | ||
|
|
547c52540a | ||
|
|
806a33976a | ||
|
|
065c6fed1d | ||
|
|
360f38a819 | ||
|
|
5204d94ebe | ||
|
|
48bfa7d15b | ||
|
|
2acfac36fc | ||
|
|
54213d4b62 | ||
|
|
ff6668a48b | ||
|
|
29cca99947 |
No files matched your search
@@ -81,3 +81,5 @@ BUILDS/
|
|||||||
/docs/
|
/docs/
|
||||||
/.perf/
|
/.perf/
|
||||||
/.codegraph/
|
/.codegraph/
|
||||||
|
|
||||||
|
.superpowers/
|
||||||
+563
@@ -0,0 +1,563 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenDatapackExporter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportProgress;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportResult;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import net.minecraft.core.RegistryAccess;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.world.level.dimension.LevelStem;
|
||||||
|
import net.minecraft.world.level.levelgen.Aquifer;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseRouter;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.DensityFunction;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintStream;
|
||||||
|
import java.lang.management.ManagementFactory;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipFile;
|
||||||
|
|
||||||
|
public final class DatapackCostProbe {
|
||||||
|
private static final String LOG_PREFIX = "[datapack-cost]";
|
||||||
|
static final String VANILLA_PRESET = "minecraft:normal";
|
||||||
|
private static final int WARMUP_ORIGIN = -4096;
|
||||||
|
private static final int MINIMUM_CALLS = 8;
|
||||||
|
private static final int TOP_OFFENDERS = 10;
|
||||||
|
private static final long ANALYSIS_STACK_BYTES = 1L << 30;
|
||||||
|
private static final double MEGABYTE = 1024.0 * 1024.0;
|
||||||
|
private static final double NANOS_PER_SECOND = 1_000_000_000.0;
|
||||||
|
|
||||||
|
private DatapackCostProbe() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] arguments) {
|
||||||
|
PrintStream diagnostics = System.err;
|
||||||
|
try {
|
||||||
|
execute(Configuration.parse(arguments));
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
failure.printStackTrace(diagnostics);
|
||||||
|
diagnostics.flush();
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
System.out.flush();
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void execute(Configuration configuration) throws Exception {
|
||||||
|
HeadlessNativeBootstrap.initialize();
|
||||||
|
Files.createDirectories(configuration.work());
|
||||||
|
Path work = Files.createTempDirectory(configuration.work(), "run-");
|
||||||
|
List<HeadlessNativeRegistries> loaded = new ArrayList<>(2);
|
||||||
|
try {
|
||||||
|
log("exporting " + configuration.dimension() + " from " + configuration.pack());
|
||||||
|
ExportRun export = export(configuration, work);
|
||||||
|
log(String.format(Locale.ROOT, "exported in %.1f s: %d files, %d density functions", export.stats().seconds(),
|
||||||
|
export.stats().files(), export.stats().densityFunctions()));
|
||||||
|
DensityJsonBreakdown.Report densityJson = onLargeStack("density-json",
|
||||||
|
() -> DensityJsonBreakdown.analyze(export.files(), configuration.dimension()).report());
|
||||||
|
Path datapack = work.resolve("datapack");
|
||||||
|
writeFiles(export.files(), datapack);
|
||||||
|
Path vanillaPack = Files.createDirectories(work.resolve("vanilla"));
|
||||||
|
ExportStats exportStats = export.stats();
|
||||||
|
export.files().clear();
|
||||||
|
|
||||||
|
long baseline = settledHeap();
|
||||||
|
long loadStarted = System.nanoTime();
|
||||||
|
HeadlessNativeRegistries vanilla = isolatedLoad(vanillaPack);
|
||||||
|
double vanillaLoad = (System.nanoTime() - loadStarted) / NANOS_PER_SECOND;
|
||||||
|
loaded.add(vanilla);
|
||||||
|
long vanillaHeap = settledHeap();
|
||||||
|
loadStarted = System.nanoTime();
|
||||||
|
HeadlessNativeRegistries exported = isolatedLoad(datapack);
|
||||||
|
double exportLoad = (System.nanoTime() - loadStarted) / NANOS_PER_SECOND;
|
||||||
|
loaded.add(exported);
|
||||||
|
long exportHeap = settledHeap();
|
||||||
|
log(String.format(Locale.ROOT, "registries loaded: vanilla %.2f s, export %.2f s", vanillaLoad, exportLoad));
|
||||||
|
|
||||||
|
WorldgenStageBench vanillaBench = new WorldgenStageBench(new WorldgenStageBench.Options("vanilla",
|
||||||
|
presetOverworld(vanilla.registries(), VANILLA_PRESET), vanilla.registries(), configuration.seed()));
|
||||||
|
warm(vanillaBench, configuration);
|
||||||
|
long vanillaSamplers = settledHeap();
|
||||||
|
WorldgenStageBench exportBench = new WorldgenStageBench(new WorldgenStageBench.Options("export",
|
||||||
|
exportedOverworld(exported.registries()), exported.registries(), configuration.seed()));
|
||||||
|
warm(exportBench, configuration);
|
||||||
|
long exportSamplers = settledHeap();
|
||||||
|
|
||||||
|
double loadStart = ManagementFactory.getOperatingSystemMXBean().getSystemLoadAverage();
|
||||||
|
Measurement vanillaMeasurement = new Measurement(vanillaBench);
|
||||||
|
Measurement exportMeasurement = new Measurement(exportBench);
|
||||||
|
measure(List.of(vanillaMeasurement, exportMeasurement), configuration);
|
||||||
|
double loadEnd = ManagementFactory.getOperatingSystemMXBean().getSystemLoadAverage();
|
||||||
|
Map<String, GeneratorStats> generators = new LinkedHashMap<>();
|
||||||
|
generators.put("vanilla", vanillaMeasurement.stats(vanillaLoad, vanillaHeap - baseline, vanillaSamplers - exportHeap));
|
||||||
|
generators.put("export", exportMeasurement.stats(exportLoad, exportHeap - vanillaHeap, exportSamplers - vanillaSamplers));
|
||||||
|
|
||||||
|
log("analyzing density function graphs");
|
||||||
|
Map<String, List<DensityCostAnalysis.RootCost>> roots = new LinkedHashMap<>();
|
||||||
|
roots.put("vanilla", onLargeStack("density-cost-vanilla", () -> analyzeRoots(vanillaBench.settings(), configuration.dimension())));
|
||||||
|
roots.put("export", onLargeStack("density-cost-export", () -> analyzeRoots(exportBench.settings(), configuration.dimension())));
|
||||||
|
|
||||||
|
Report report = new Report(configuration.pack().toString(), configuration.dimension(), configuration.seed(),
|
||||||
|
configuration.rounds() * configuration.side() * configuration.side(), configuration.warmup() * configuration.warmup(),
|
||||||
|
configuration.rounds(), new MachineLoad(Runtime.getRuntime().availableProcessors(), loadStart, loadEnd),
|
||||||
|
"single thread for chunk stages and calls, vanilla and export interleaved per round; registry load uses HeadlessNativeRegistries' 4-thread loader; export uses Iris's own pools",
|
||||||
|
"feature decoration not measured: Paper's applyBiomeDecoration needs a live ServerLevel and the headless feature world lives in test fixtures",
|
||||||
|
exportStats, generators, densityJson, roots, definitions());
|
||||||
|
writeReport(report, configuration.output());
|
||||||
|
System.out.println(table(report));
|
||||||
|
log("report written to " + configuration.output());
|
||||||
|
} finally {
|
||||||
|
for (HeadlessNativeRegistries registries : loaded) {
|
||||||
|
registries.close();
|
||||||
|
}
|
||||||
|
deleteTree(work);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ExportRun export(Configuration configuration, Path work) throws Exception {
|
||||||
|
Path exportRoot = Files.createDirectories(work.resolve("exports"));
|
||||||
|
StageClock clock = new StageClock();
|
||||||
|
WorldgenExportResult result;
|
||||||
|
long started;
|
||||||
|
long finished;
|
||||||
|
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(new RealPackProbeSupport.WorkspaceOptions(
|
||||||
|
configuration.pack().toFile(), configuration.dimension(), LOG_PREFIX, new HeadlessNativeRuntime(), work))) {
|
||||||
|
WorldgenExportRequest request = WorldgenExportRequest.builder()
|
||||||
|
.pack(workspace.pack().toPath())
|
||||||
|
.dimension(configuration.dimension())
|
||||||
|
.exportRoot(exportRoot)
|
||||||
|
.objects(true)
|
||||||
|
.build();
|
||||||
|
started = System.nanoTime();
|
||||||
|
clock.start(started);
|
||||||
|
result = WorldgenDatapackExporter.create().export(request, clock);
|
||||||
|
finished = System.nanoTime();
|
||||||
|
}
|
||||||
|
if (!result.isSuccess()) {
|
||||||
|
throw new IllegalStateException("Export failed: " + result.message() + " " + result.diagnostics());
|
||||||
|
}
|
||||||
|
SortedMap<String, byte[]> files = zipEntries(result.output());
|
||||||
|
ExportStats stats = new ExportStats((finished - started) / NANOS_PER_SECOND, clock.seconds(finished), Files.size(result.output()),
|
||||||
|
files.size(), count(files, "/worldgen/density_function/"), bytes(files, "/worldgen/density_function/"),
|
||||||
|
count(files, "/worldgen/noise/"), count(files, "/worldgen/biome/"), result.diagnostics().size());
|
||||||
|
Files.delete(result.output());
|
||||||
|
return new ExportRun(stats, files);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void warm(WorldgenStageBench bench, Configuration configuration) {
|
||||||
|
long started = System.nanoTime();
|
||||||
|
bench.chunks(WARMUP_ORIGIN, WARMUP_ORIGIN, configuration.warmup(), new WorldgenStageBench.ChunkSamples());
|
||||||
|
WorldgenStageBench.CallBudget budget = new WorldgenStageBench.CallBudget(MINIMUM_CALLS * 4, MINIMUM_CALLS,
|
||||||
|
TimeUnit.SECONDS.toNanos(configuration.callSeconds()) / 4);
|
||||||
|
bench.uncachedBiome(budget, new WorldgenStageBench.CallSamples());
|
||||||
|
bench.baseHeight(budget, Heightmap.Types.WORLD_SURFACE_WG, new WorldgenStageBench.CallSamples());
|
||||||
|
bench.baseColumn(budget, new WorldgenStageBench.CallSamples());
|
||||||
|
log(String.format(Locale.ROOT, "%s warmed in %.1f s", bench.label(), (System.nanoTime() - started) / NANOS_PER_SECOND));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void measure(List<Measurement> measurements, Configuration configuration) {
|
||||||
|
int stride = configuration.side() + 4;
|
||||||
|
WorldgenStageBench.CallBudget budget = new WorldgenStageBench.CallBudget(
|
||||||
|
Math.max(1, configuration.calls() / configuration.rounds()), Math.max(1, MINIMUM_CALLS / configuration.rounds()),
|
||||||
|
TimeUnit.SECONDS.toNanos(configuration.callSeconds()) / configuration.rounds());
|
||||||
|
for (int round = 0; round < configuration.rounds(); round++) {
|
||||||
|
for (int index = 0; index < measurements.size(); index++) {
|
||||||
|
Measurement measurement = measurements.get((index + round) % measurements.size());
|
||||||
|
long started = System.nanoTime();
|
||||||
|
measurement.bench.chunks(round * stride, round * stride, configuration.side(), measurement.chunks);
|
||||||
|
measurement.bench.uncachedBiome(budget, measurement.uncachedBiome);
|
||||||
|
measurement.bench.baseHeight(budget, Heightmap.Types.WORLD_SURFACE_WG, measurement.surfaceHeight);
|
||||||
|
measurement.bench.baseHeight(budget, Heightmap.Types.OCEAN_FLOOR_WG, measurement.floorHeight);
|
||||||
|
measurement.bench.baseColumn(budget, measurement.column);
|
||||||
|
log(String.format(Locale.ROOT, "%s round %d measured in %.1f s", measurement.bench.label(), round,
|
||||||
|
(System.nanoTime() - started) / NANOS_PER_SECOND));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<DensityCostAnalysis.RootCost> analyzeRoots(NoiseGeneratorSettings settings, String dimension) {
|
||||||
|
DensityCostAnalysis analysis = new DensityCostAnalysis(id -> DensityJsonBreakdown.category(dimension, id), TOP_OFFENDERS);
|
||||||
|
Map<String, DensityFunction> roots = new LinkedHashMap<>();
|
||||||
|
NoiseRouter router = settings.noiseRouter();
|
||||||
|
roots.put("temperature", router.temperature());
|
||||||
|
roots.put("vegetation", router.vegetation());
|
||||||
|
roots.put("continents", router.continents());
|
||||||
|
roots.put("erosion", router.erosion());
|
||||||
|
roots.put("depth", router.depth());
|
||||||
|
roots.put("ridges", router.ridges());
|
||||||
|
roots.put("chunk_surface_level", router.chunkSurfaceLevel());
|
||||||
|
roots.put("final_density", router.finalDensity());
|
||||||
|
if (settings.aquifers().isPresent()) {
|
||||||
|
Aquifer.Config aquifer = settings.aquifers().get();
|
||||||
|
roots.put("aquifer.barrier", aquifer.barrierNoise());
|
||||||
|
roots.put("aquifer.fluid_level_floodedness", aquifer.fluidLevelFloodednessNoise());
|
||||||
|
roots.put("aquifer.fluid_level_spread", aquifer.fluidLevelSpreadNoise());
|
||||||
|
roots.put("aquifer.lava", aquifer.lavaNoise());
|
||||||
|
roots.put("aquifer.exclusion", aquifer.exclusion());
|
||||||
|
roots.put("aquifer.surface_level", aquifer.surfaceLevel());
|
||||||
|
}
|
||||||
|
List<DensityCostAnalysis.RootCost> costs = new ArrayList<>(roots.size());
|
||||||
|
for (Map.Entry<String, DensityFunction> root : roots.entrySet()) {
|
||||||
|
costs.add(analysis.analyze(root.getKey(), root.getValue()));
|
||||||
|
}
|
||||||
|
return costs;
|
||||||
|
}
|
||||||
|
|
||||||
|
static LevelStem presetOverworld(RegistryAccess registries, String preset) {
|
||||||
|
return registries.lookupOrThrow(Registries.WORLD_PRESET)
|
||||||
|
.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, Identifier.parse(preset))).value().overworld()
|
||||||
|
.orElseThrow(() -> new IllegalStateException("World preset " + preset + " has no overworld"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static LevelStem exportedOverworld(RegistryAccess registries) {
|
||||||
|
return registries.lookupOrThrow(Registries.LEVEL_STEM).getOrThrow(LevelStem.OVERWORLD).value();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HeadlessNativeRegistries isolatedLoad(Path directory) {
|
||||||
|
try (PaperRegistryScope scope = PaperRegistryScope.open()) {
|
||||||
|
return HeadlessNativeRegistries.load(directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long settledHeap() throws InterruptedException {
|
||||||
|
for (int pass = 0; pass < 4; pass++) {
|
||||||
|
System.gc();
|
||||||
|
Thread.sleep(200L);
|
||||||
|
}
|
||||||
|
return ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> T onLargeStack(String name, Callable<T> work) throws Exception {
|
||||||
|
AtomicReference<T> result = new AtomicReference<>();
|
||||||
|
AtomicReference<Throwable> failure = new AtomicReference<>();
|
||||||
|
Thread thread = new Thread(null, () -> {
|
||||||
|
try {
|
||||||
|
result.set(work.call());
|
||||||
|
} catch (Throwable problem) {
|
||||||
|
failure.set(problem);
|
||||||
|
}
|
||||||
|
}, name, ANALYSIS_STACK_BYTES);
|
||||||
|
thread.start();
|
||||||
|
thread.join();
|
||||||
|
if (failure.get() instanceof Exception exception) {
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
if (failure.get() instanceof Error error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return result.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SortedMap<String, byte[]> zipEntries(Path zip) throws IOException {
|
||||||
|
SortedMap<String, byte[]> files = new TreeMap<>();
|
||||||
|
try (ZipFile archive = new ZipFile(zip.toFile())) {
|
||||||
|
Enumeration<? extends ZipEntry> entries = archive.entries();
|
||||||
|
while (entries.hasMoreElements()) {
|
||||||
|
ZipEntry entry = entries.nextElement();
|
||||||
|
if (!entry.isDirectory()) {
|
||||||
|
files.put(entry.getName(), archive.getInputStream(entry).readAllBytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeFiles(SortedMap<String, byte[]> files, Path directory) throws IOException {
|
||||||
|
Files.createDirectories(directory);
|
||||||
|
for (Map.Entry<String, byte[]> file : files.entrySet()) {
|
||||||
|
Path target = directory.resolve(file.getKey()).normalize();
|
||||||
|
if (!target.startsWith(directory)) {
|
||||||
|
throw new IllegalArgumentException("Export file escapes the datapack root: " + file.getKey());
|
||||||
|
}
|
||||||
|
Files.createDirectories(target.getParent());
|
||||||
|
Files.write(target, file.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int count(SortedMap<String, byte[]> files, String folder) {
|
||||||
|
int count = 0;
|
||||||
|
for (String path : files.keySet()) {
|
||||||
|
if (path.contains(folder) && path.endsWith(".json")) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long bytes(SortedMap<String, byte[]> files, String folder) {
|
||||||
|
long bytes = 0;
|
||||||
|
for (Map.Entry<String, byte[]> file : files.entrySet()) {
|
||||||
|
if (file.getKey().contains(folder) && file.getKey().endsWith(".json")) {
|
||||||
|
bytes += file.getValue().length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeReport(Report report, Path output) throws IOException {
|
||||||
|
Files.createDirectories(output.getParent());
|
||||||
|
Gson gson = new GsonBuilder().setPrettyPrinting().serializeSpecialFloatingPointValues().create();
|
||||||
|
Files.writeString(output, gson.toJson(report), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> definitions() {
|
||||||
|
Map<String, String> definitions = new LinkedHashMap<>();
|
||||||
|
definitions.put("chunks", "per stage: wall ms and current-thread CPU ms per chunk; rounds of side x side chunks alternate vanilla and export");
|
||||||
|
definitions.put("chunks.biomes", "ChunkGenerator.createBiomes on a fresh ProtoChunk, ms per chunk over the grid plus a one-chunk ring");
|
||||||
|
definitions.put("chunks.noise", "NoiseChunk construction (aquifer init) plus NoiseBasedChunkGenerator.doFill, ms per chunk");
|
||||||
|
definitions.put("chunks.surface", "MaterialSystem.buildSurface with the fill's NoiseChunk and a region biome manager, ms per chunk");
|
||||||
|
definitions.put("chunks.carverBiome", "one uncached BiomeSource lookup at the chunk origin, as ChunkAccess.carverBiome memoizes per chunk, ms per chunk");
|
||||||
|
definitions.put("uncachedBiome", "BiomeSource.createUncachedResolver(...).getNoiseBiome at random quart positions within 32k blocks, wall us per call and mean thread CPU us per call");
|
||||||
|
definitions.put("baseHeight", "NoiseBasedChunkGenerator.getBaseHeight as structure placement calls it, us per call");
|
||||||
|
definitions.put("baseColumn", "NoiseBasedChunkGenerator.getBaseColumn, us per call");
|
||||||
|
definitions.put("retainedHeapBytes", "used heap after four System.gc passes, delta across the registry load");
|
||||||
|
definitions.put("samplerHeapBytes", "used heap delta across RandomState creation and warmup (compiled samplers and buffer pools)");
|
||||||
|
definitions.put("roots.dagNodes", "distinct non-constant density function and spline nodes reachable, references shared");
|
||||||
|
definitions.put("roots.cachedNodes", "non-constant evaluations per point with caches enabled: every distinct cache body once, all branches");
|
||||||
|
definitions.put("roots.expandedNodes", "non-constant evaluations per point with every reference and cache re-expanded, all branches");
|
||||||
|
definitions.put("roots.uncachedPointWorst", "worst single uncached point evaluation with lazy splines, interval_select and range_choice; interpolated off-lattice costs 8 corners at expanded cost; find_top_surface counts its probe steps");
|
||||||
|
definitions.put("roots.dagShare", "share of the distinct nodes by the category of the named file that holds them; offset copies are Grad2 and Taps duplicates");
|
||||||
|
definitions.put("roots.exclusive", "named density functions ranked by multiplicity x own nodes in the expanded tree");
|
||||||
|
definitions.put("roots.inclusive", "named density functions ranked by multiplicity x subtree nodes in the expanded tree");
|
||||||
|
definitions.put("densityJson.structurallyDuplicateBytes", "bytes beyond one file per JSON shape, with numbers, density function references and noise ids abstracted");
|
||||||
|
return definitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String table(Report report) {
|
||||||
|
StringBuilder out = new StringBuilder(4096);
|
||||||
|
GeneratorStats vanilla = report.generators().get("vanilla");
|
||||||
|
GeneratorStats export = report.generators().get("export");
|
||||||
|
out.append(String.format(Locale.ROOT, "%s %s seed %d: %d chunks in %d interleaved rounds after %d warmup chunks, one thread, load %.0f-%.0f on %d cores%n",
|
||||||
|
LOG_PREFIX, report.dimension(), report.seed(), report.chunks(), report.rounds(), report.warmupChunks(),
|
||||||
|
report.load().loadAverageStart(), report.load().loadAverageEnd(), report.load().processors()));
|
||||||
|
out.append(String.format(Locale.ROOT, "%-34s %10s %10s %8s %10s %10s %8s%n", "stage (mean)", "vanilla", "export", "ratio",
|
||||||
|
"cpu van", "cpu exp", "ratio"));
|
||||||
|
out.append(String.format(Locale.ROOT, "%-34s %10s %10.1f%n", "export (s)", "-", report.export().seconds()));
|
||||||
|
row(out, "registry load (s)", vanilla.registryLoadSeconds(), export.registryLoadSeconds());
|
||||||
|
row(out, "retained heap after load (MB)", vanilla.retainedHeapBytes() / MEGABYTE, export.retainedHeapBytes() / MEGABYTE);
|
||||||
|
row(out, "sampler heap after warmup (MB)", vanilla.samplerHeapBytes() / MEGABYTE, export.samplerHeapBytes() / MEGABYTE);
|
||||||
|
stage(out, "biome fill (ms/chunk)", vanilla.chunks().biomes(), export.chunks().biomes());
|
||||||
|
stage(out, "noise fill (ms/chunk)", vanilla.chunks().noise(), export.chunks().noise());
|
||||||
|
stage(out, "surface (ms/chunk)", vanilla.chunks().surface(), export.chunks().surface());
|
||||||
|
stage(out, "carver biome lookup (ms/chunk)", vanilla.chunks().carverBiome(), export.chunks().carverBiome());
|
||||||
|
row(out, "chunk total (ms/chunk)", vanilla.chunks().totalMean(), export.chunks().totalMean(),
|
||||||
|
vanilla.chunks().totalCpuMean(), export.chunks().totalCpuMean());
|
||||||
|
call(out, "uncached getNoiseBiome (us/call)", vanilla.uncachedBiome(), export.uncachedBiome());
|
||||||
|
call(out, "getBaseHeight WORLD_SURFACE (us)", vanilla.baseHeightSurface(), export.baseHeightSurface());
|
||||||
|
call(out, "getBaseHeight OCEAN_FLOOR (us)", vanilla.baseHeightFloor(), export.baseHeightFloor());
|
||||||
|
call(out, "getBaseColumn (us/call)", vanilla.baseColumn(), export.baseColumn());
|
||||||
|
out.append(String.format(Locale.ROOT, "%-24s %9s %9s %9s %10s %10s %10s %10s%n", "router root", "dag(v)", "dag",
|
||||||
|
"cached", "expand(v)", "expanded", "uncPt(v)", "uncachedPt"));
|
||||||
|
List<DensityCostAnalysis.RootCost> vanillaRoots = report.roots().get("vanilla");
|
||||||
|
List<DensityCostAnalysis.RootCost> exportRoots = report.roots().get("export");
|
||||||
|
for (DensityCostAnalysis.RootCost root : exportRoots) {
|
||||||
|
DensityCostAnalysis.RootCost reference = find(vanillaRoots, root.root());
|
||||||
|
out.append(String.format(Locale.ROOT, "%-24s %9s %9s %9s %10s %10s %10s %10s%n", root.root(),
|
||||||
|
reference == null ? "-" : compact(reference.dagNodes()), compact(root.dagNodes()), compact(root.cachedNodes()),
|
||||||
|
reference == null ? "-" : compact(reference.expandedNodes()), compact(root.expandedNodes()),
|
||||||
|
reference == null ? "-" : compact(reference.uncachedPointWorst()), compact(root.uncachedPointWorst())));
|
||||||
|
}
|
||||||
|
out.append("top expanded offenders (exclusive share of root, multiplicity)\n");
|
||||||
|
for (DensityCostAnalysis.RootCost root : exportRoots) {
|
||||||
|
if (root.exclusive().isEmpty() || root.expandedNodes() < 1.0e4) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
StringBuilder line = new StringBuilder(String.format(Locale.ROOT, " %-22s", root.root()));
|
||||||
|
for (int index = 0; index < Math.min(2, root.exclusive().size()); index++) {
|
||||||
|
DensityCostAnalysis.Offender offender = root.exclusive().get(index);
|
||||||
|
line.append(String.format(Locale.ROOT, " %4.1f%% %s x%s;", 100.0 * offender.contribution() / root.expandedNodes(),
|
||||||
|
shortName(offender.name()), compact(offender.multiplicity())));
|
||||||
|
}
|
||||||
|
out.append(line).append('\n');
|
||||||
|
}
|
||||||
|
for (DensityCostAnalysis.RootCost root : exportRoots) {
|
||||||
|
if (root.dagNodes() < 1.0e4) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<Map.Entry<String, Double>> shares = new ArrayList<>(root.dagShare().entrySet());
|
||||||
|
shares.sort(Map.Entry.<String, Double>comparingByValue().reversed());
|
||||||
|
StringBuilder line = new StringBuilder(String.format(Locale.ROOT, " dag %-18s", root.root()));
|
||||||
|
for (int index = 0; index < Math.min(3, shares.size()); index++) {
|
||||||
|
line.append(String.format(Locale.ROOT, " %4.1f%% %s;", 100.0 * shares.get(index).getValue(), shares.get(index).getKey()));
|
||||||
|
}
|
||||||
|
out.append(line).append('\n');
|
||||||
|
}
|
||||||
|
DensityJsonBreakdown.Report json = report.densityJson();
|
||||||
|
out.append(String.format(Locale.ROOT, "density function JSON %.0f MB in %d files, %d cellular instances in %d shapes%n",
|
||||||
|
json.bytes() / MEGABYTE, json.files(), json.cellularInstances(), json.cellularShapes().size()));
|
||||||
|
for (int index = 0; index < Math.min(9, json.categories().size()); index++) {
|
||||||
|
DensityJsonBreakdown.CategoryBytes category = json.categories().get(index);
|
||||||
|
out.append(String.format(Locale.ROOT, " %-22s %8.1f MB %7d files %7d shapes %8.1f MB dup%n", category.category(),
|
||||||
|
category.bytes() / MEGABYTE, category.files(), category.distinctShapes(), category.structurallyDuplicateBytes() / MEGABYTE));
|
||||||
|
}
|
||||||
|
for (int index = 0; index < Math.min(4, json.libraryOwners().size()); index++) {
|
||||||
|
DensityJsonBreakdown.Bytes owner = json.libraryOwners().get(index);
|
||||||
|
out.append(String.format(Locale.ROOT, " %-48s %8.1f MB%n", owner.name(), owner.bytes() / MEGABYTE));
|
||||||
|
}
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void row(StringBuilder out, String label, double vanilla, double export) {
|
||||||
|
out.append(String.format(Locale.ROOT, "%-34s %10.3f %10.3f %8s%n", label, vanilla, export, ratio(vanilla, export)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void row(StringBuilder out, String label, double vanilla, double export, double vanillaCpu, double exportCpu) {
|
||||||
|
out.append(String.format(Locale.ROOT, "%-34s %10.3f %10.3f %8s %10.3f %10.3f %8s%n", label, vanilla, export, ratio(vanilla, export),
|
||||||
|
vanillaCpu, exportCpu, ratio(vanillaCpu, exportCpu)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void stage(StringBuilder out, String label, WorldgenStageBench.StageTiming vanilla, WorldgenStageBench.StageTiming export) {
|
||||||
|
row(out, label, vanilla.wall().mean(), export.wall().mean(), vanilla.cpu().mean(), export.cpu().mean());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void call(StringBuilder out, String label, WorldgenStageBench.CallTiming vanilla, WorldgenStageBench.CallTiming export) {
|
||||||
|
row(out, label, vanilla.wall().mean(), export.wall().mean(), vanilla.cpuMean(), export.cpuMean());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String ratio(double vanilla, double export) {
|
||||||
|
return vanilla > 0.0 ? String.format(Locale.ROOT, "%.1fx", export / vanilla) : "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DensityCostAnalysis.RootCost find(List<DensityCostAnalysis.RootCost> roots, String name) {
|
||||||
|
for (DensityCostAnalysis.RootCost root : roots) {
|
||||||
|
if (root.root().equals(name)) {
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String shortName(String name) {
|
||||||
|
int separator = name.indexOf(':');
|
||||||
|
String path = separator < 0 ? name : name.substring(separator + 1);
|
||||||
|
return path.length() <= 34 ? path : "..." + path.substring(path.length() - 31);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String compact(double value) {
|
||||||
|
if (value < 1.0e4) {
|
||||||
|
return String.format(Locale.ROOT, "%.0f", value);
|
||||||
|
}
|
||||||
|
return String.format(Locale.ROOT, "%.2e", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void log(String message) {
|
||||||
|
System.out.println(LOG_PREFIX + " " + message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void deleteTree(Path root) throws IOException {
|
||||||
|
if (!Files.exists(root)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
HeadlessNativeRegistries.delete(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
record Configuration(Path pack, String dimension, long seed, int side, int rounds, int warmup, int calls, long callSeconds,
|
||||||
|
Path output, Path work) {
|
||||||
|
Configuration {
|
||||||
|
if (side < 1 || rounds < 1 || warmup < 1 || calls < MINIMUM_CALLS || callSeconds < 1) {
|
||||||
|
throw new IllegalArgumentException("side, rounds, warmup and callSeconds must be positive and calls at least " + MINIMUM_CALLS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Configuration parse(String[] arguments) {
|
||||||
|
if (arguments.length != 10) {
|
||||||
|
throw new IllegalArgumentException("Usage: pack dimension seed side rounds warmupSide calls callSeconds output work");
|
||||||
|
}
|
||||||
|
return new Configuration(new File(arguments[0]).toPath().toAbsolutePath(), arguments[1], Long.parseLong(arguments[2]),
|
||||||
|
Integer.parseInt(arguments[3]), Integer.parseInt(arguments[4]), Integer.parseInt(arguments[5]),
|
||||||
|
Integer.parseInt(arguments[6]), Long.parseLong(arguments[7]), new File(arguments[8]).toPath().toAbsolutePath(),
|
||||||
|
new File(arguments[9]).toPath().toAbsolutePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record ExportStats(double seconds, Map<String, Double> stageSeconds, long zipBytes, int files, int densityFunctions,
|
||||||
|
long densityFunctionBytes, int noises, int biomes, int diagnostics) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record GeneratorStats(double registryLoadSeconds, long retainedHeapBytes, long samplerHeapBytes, WorldgenStageBench.ChunkTimings chunks,
|
||||||
|
WorldgenStageBench.CallTiming uncachedBiome, WorldgenStageBench.CallTiming baseHeightSurface,
|
||||||
|
WorldgenStageBench.CallTiming baseHeightFloor, WorldgenStageBench.CallTiming baseColumn) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record MachineLoad(int processors, double loadAverageStart, double loadAverageEnd) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record Report(String pack, String dimension, long seed, int chunks, int warmupChunks, int rounds, MachineLoad load,
|
||||||
|
String threading, String decoration,
|
||||||
|
ExportStats export, Map<String, GeneratorStats> generators, DensityJsonBreakdown.Report densityJson,
|
||||||
|
Map<String, List<DensityCostAnalysis.RootCost>> roots, Map<String, String> definitions) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ExportRun(ExportStats stats, SortedMap<String, byte[]> files) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Measurement {
|
||||||
|
private final WorldgenStageBench bench;
|
||||||
|
private final WorldgenStageBench.ChunkSamples chunks = new WorldgenStageBench.ChunkSamples();
|
||||||
|
private final WorldgenStageBench.CallSamples uncachedBiome = new WorldgenStageBench.CallSamples();
|
||||||
|
private final WorldgenStageBench.CallSamples surfaceHeight = new WorldgenStageBench.CallSamples();
|
||||||
|
private final WorldgenStageBench.CallSamples floorHeight = new WorldgenStageBench.CallSamples();
|
||||||
|
private final WorldgenStageBench.CallSamples column = new WorldgenStageBench.CallSamples();
|
||||||
|
|
||||||
|
private Measurement(WorldgenStageBench bench) {
|
||||||
|
this.bench = bench;
|
||||||
|
}
|
||||||
|
|
||||||
|
private GeneratorStats stats(double loadSeconds, long retainedHeap, long samplerHeap) {
|
||||||
|
return new GeneratorStats(loadSeconds, retainedHeap, samplerHeap, chunks.timings(), uncachedBiome.timing(),
|
||||||
|
surfaceHeight.timing(), floorHeight.timing(), column.timing());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class StageClock implements WorldgenExportProgress {
|
||||||
|
private final Map<String, Long> starts = new LinkedHashMap<>();
|
||||||
|
private long origin;
|
||||||
|
|
||||||
|
private void start(long nanos) {
|
||||||
|
origin = nanos;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stage(String stage) {
|
||||||
|
starts.put(stage, System.nanoTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void resources(int done, int total) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Double> seconds(long finished) {
|
||||||
|
Map<String, Double> seconds = new LinkedHashMap<>();
|
||||||
|
String previous = "setup";
|
||||||
|
long previousStart = origin;
|
||||||
|
for (Map.Entry<String, Long> entry : starts.entrySet()) {
|
||||||
|
seconds.put(previous, (entry.getValue() - previousStart) / NANOS_PER_SECOND);
|
||||||
|
previous = entry.getKey();
|
||||||
|
previousStart = entry.getValue();
|
||||||
|
}
|
||||||
|
seconds.put(previous, (finished - previousStart) / NANOS_PER_SECOND);
|
||||||
|
return seconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+331
@@ -0,0 +1,331 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenDatapackExporter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportProgress;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportResult;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportService;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.surface.MaterialProgramCompiler;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
|
||||||
|
import art.arcane.iris.probe.fidelity.BiomeShareReport;
|
||||||
|
import art.arcane.iris.probe.fidelity.BiomeTypes;
|
||||||
|
import art.arcane.iris.probe.fidelity.ColumnSource;
|
||||||
|
import art.arcane.iris.probe.fidelity.ColumnSurvey;
|
||||||
|
import art.arcane.iris.probe.fidelity.FidelityComparison;
|
||||||
|
import art.arcane.iris.probe.fidelity.FidelityReport;
|
||||||
|
import art.arcane.iris.probe.fidelity.FidelitySampling;
|
||||||
|
import art.arcane.iris.probe.fidelity.FidelitySamples;
|
||||||
|
import art.arcane.iris.probe.fidelity.FidelityScanner;
|
||||||
|
import art.arcane.iris.probe.fidelity.FidelitySource;
|
||||||
|
import art.arcane.iris.probe.fidelity.FidelityStats;
|
||||||
|
import art.arcane.iris.probe.fidelity.FidelityThresholds;
|
||||||
|
import art.arcane.iris.probe.fidelity.IrisEngineColumnSource;
|
||||||
|
import art.arcane.iris.probe.fidelity.IrisEngineFidelitySource;
|
||||||
|
import art.arcane.iris.probe.fidelity.SurveyWindow;
|
||||||
|
import art.arcane.iris.probe.fidelity.VanillaNoiseColumnSource;
|
||||||
|
import art.arcane.iris.probe.fidelity.VanillaNoiseFidelitySource;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
public final class DatapackFidelityProbe {
|
||||||
|
private static final String LOG_PREFIX = "[datapack-fidelity]";
|
||||||
|
private static final String USAGE = "Usage: datapackFidelityProbe pack dimension seed radiusChunks stride output surveyBlocks surveyStride surveySeeds mode";
|
||||||
|
private static final String TARGET_VERSION = "26.3";
|
||||||
|
private static final String EXPORT_FORMAT = "directory";
|
||||||
|
private static final String EXPORT_OUTPUT = "export";
|
||||||
|
private static final int MAX_STRIDE = 16;
|
||||||
|
private static final int CHUNK_WIDTH = 16;
|
||||||
|
private static final int ARGUMENTS = 10;
|
||||||
|
private static final String COLUMN_SCOPE = "columns";
|
||||||
|
private static final String CHUNK_SCOPE = "chunks";
|
||||||
|
private static final int EXIT_ERROR = 1;
|
||||||
|
private static final int EXIT_FIDELITY_FAILED = 3;
|
||||||
|
|
||||||
|
private DatapackFidelityProbe() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] arguments) {
|
||||||
|
int status;
|
||||||
|
try {
|
||||||
|
status = execute(arguments, WorldgenDatapackExporter.create()).passed() ? 0 : EXIT_FIDELITY_FAILED;
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
failure.printStackTrace(System.err);
|
||||||
|
status = EXIT_ERROR;
|
||||||
|
}
|
||||||
|
System.out.flush();
|
||||||
|
System.err.flush();
|
||||||
|
System.exit(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
static FidelityReport execute(String[] arguments, WorldgenExportService exporter) throws Exception {
|
||||||
|
Objects.requireNonNull(exporter, "exporter");
|
||||||
|
Configuration configuration = Configuration.parse(arguments);
|
||||||
|
System.out.println(LOG_PREFIX + " pack=" + configuration.pack() + " dimension=" + configuration.dimension()
|
||||||
|
+ " seed=" + configuration.seed() + " radiusChunks=" + configuration.radiusChunks() + " stride=" + configuration.stride()
|
||||||
|
+ " surveyBlocks=" + configuration.survey().size() + " surveyStride=" + configuration.survey().stride()
|
||||||
|
+ " surveySeeds=" + configuration.surveySeeds() + " mode=" + configuration.mode().label());
|
||||||
|
Outcome outcome = compare(configuration, exporter);
|
||||||
|
writeReport(configuration.output(), outcome.json());
|
||||||
|
System.out.print(outcome.table());
|
||||||
|
System.out.println(LOG_PREFIX + " report=" + configuration.output());
|
||||||
|
return outcome.report();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Outcome compare(Configuration configuration, WorldgenExportService exporter) throws Exception {
|
||||||
|
Path scratch = Files.createTempDirectory("iris-datapack-fidelity-");
|
||||||
|
try {
|
||||||
|
return compareIn(scratch, configuration, exporter);
|
||||||
|
} finally {
|
||||||
|
ProbeWorkspaceCleanup.deleteRecursively(scratch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Outcome compareIn(Path scratch, Configuration configuration, WorldgenExportService exporter) throws Exception {
|
||||||
|
HeadlessNativeRuntime runtime = new HeadlessNativeRuntime();
|
||||||
|
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(new RealPackProbeSupport.WorkspaceOptions(
|
||||||
|
configuration.pack().toFile(), configuration.dimension(), LOG_PREFIX, runtime, scratch))) {
|
||||||
|
WorldgenExportResult export = export(exporter, workspace.pack().toPath(), configuration.dimension(), scratch.resolve("exports"));
|
||||||
|
Target target = new Target(export.output(), WorldgenExportRequest.DEFAULT_NAMESPACE,
|
||||||
|
VanillaResourceIdentifier.sanitizePath(configuration.dimension()), export.loadKeyByBiomeId());
|
||||||
|
return switch (configuration.mode()) {
|
||||||
|
case FULL -> Outcome.of(compareAll(workspace, target, configuration));
|
||||||
|
case BIOMES -> Outcome.of(compareBiomes(workspace, target, configuration));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FidelityReport compareAll(RealPackProbeSupport.Workspace workspace, Target target, Configuration configuration) throws Exception {
|
||||||
|
List<Long> seeds = configuration.surveySeeds();
|
||||||
|
FidelityStats irisColumns = null;
|
||||||
|
FidelityStats exportColumns = null;
|
||||||
|
FidelitySamples chunks = null;
|
||||||
|
for (long seed : seeds) {
|
||||||
|
try (RealPackProbeSupport.EngineSession session = workspace.openEngine(seed, false, "fidelity-" + seed)) {
|
||||||
|
Surveyed surveyed = survey(session.engine(), target, seed, configuration.survey());
|
||||||
|
irisColumns = irisColumns == null ? surveyed.iris() : irisColumns.merge(surveyed.iris());
|
||||||
|
exportColumns = exportColumns == null ? surveyed.export() : exportColumns.merge(surveyed.export());
|
||||||
|
if (seed == configuration.seed()) {
|
||||||
|
chunks = scanChunks(session.engine(), target, configuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FidelitySampling columnSampling = new FidelitySampling(COLUMN_SCOPE, configuration.survey().size(), configuration.survey().stride(),
|
||||||
|
seeds, irisColumns.columns());
|
||||||
|
return FidelityComparison.compare(new FidelitySamples(irisColumns, exportColumns, columnSampling), chunks, FidelityThresholds.defaults());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BiomeShareReport compareBiomes(RealPackProbeSupport.Workspace workspace, Target target, Configuration configuration) throws Exception {
|
||||||
|
List<FidelitySamples> seeds = new ArrayList<>(configuration.seedCount());
|
||||||
|
Map<String, String> biomeTypes = Map.of();
|
||||||
|
for (long seed : configuration.surveySeeds()) {
|
||||||
|
try (RealPackProbeSupport.EngineSession session = workspace.openEngine(seed, false, "biomes-" + seed)) {
|
||||||
|
Engine engine = session.engine();
|
||||||
|
Surveyed surveyed = survey(engine, target, seed, configuration.survey());
|
||||||
|
if (biomeTypes.isEmpty()) {
|
||||||
|
biomeTypes = BiomeTypes.of(engine.getDimension().getAllRegions(engine), key -> engine.getData().getBiomeLoader().load(key));
|
||||||
|
}
|
||||||
|
seeds.add(new FidelitySamples(surveyed.iris(), surveyed.export(), new FidelitySampling(COLUMN_SCOPE, configuration.survey().size(),
|
||||||
|
configuration.survey().stride(), List.of(seed), surveyed.iris().columns())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FidelityComparison.compareBiomeShares(seeds, biomeTypes, FidelityThresholds.defaults());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Surveyed survey(Engine engine, Target target, long seed, SurveyWindow window) throws Exception {
|
||||||
|
try (ColumnSource iris = new IrisEngineColumnSource(engine);
|
||||||
|
ColumnSource vanilla = new VanillaNoiseColumnSource(target.sampler(seed), target.loadKeyByBiomeId())) {
|
||||||
|
requireSameHeightRange(iris.minY(), iris.maxY(), vanilla.minY(), vanilla.maxY());
|
||||||
|
return new Surveyed(surveyed(iris, window, seed), surveyed(vanilla, window, seed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FidelityStats surveyed(ColumnSource source, SurveyWindow window, long seed) {
|
||||||
|
long started = System.nanoTime();
|
||||||
|
FidelityStats stats = ColumnSurvey.scan(source, window, MultiBurst.burst.parallelism());
|
||||||
|
System.out.println(LOG_PREFIX + " surveyed " + source.label() + " seed=" + seed + " columns=" + stats.columns()
|
||||||
|
+ " in " + (System.nanoTime() - started) / 1_000_000L + "ms");
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FidelitySamples scanChunks(Engine engine, Target target, Configuration configuration) throws Exception {
|
||||||
|
try (IrisEngineFidelitySource iris = new IrisEngineFidelitySource(engine);
|
||||||
|
VanillaNoiseFidelitySource vanilla = new VanillaNoiseFidelitySource(target.sampler(configuration.seed()), target.loadKeyByBiomeId(),
|
||||||
|
target.namespace() + ":" + MaterialProgramCompiler.WALL_SHELL)) {
|
||||||
|
requireSameHeightRange(iris.minY(), iris.maxY(), vanilla.minY(), vanilla.maxY());
|
||||||
|
FidelityStats irisStats = scan(iris, configuration);
|
||||||
|
FidelityStats exportStats = scan(vanilla, configuration);
|
||||||
|
FidelitySampling sampling = new FidelitySampling(CHUNK_SCOPE, (2 * configuration.radiusChunks() + 1) * CHUNK_WIDTH,
|
||||||
|
configuration.stride(), List.of(configuration.seed()), irisStats.columns());
|
||||||
|
return new FidelitySamples(irisStats, exportStats, sampling);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorldgenExportResult export(WorldgenExportService exporter, Path pack, String dimension, Path exportRoot) throws IOException {
|
||||||
|
Files.createDirectories(exportRoot);
|
||||||
|
WorldgenExportRequest request = WorldgenExportRequest.builder()
|
||||||
|
.pack(pack)
|
||||||
|
.dimension(dimension)
|
||||||
|
.version(TARGET_VERSION)
|
||||||
|
.format(EXPORT_FORMAT)
|
||||||
|
.output(EXPORT_OUTPUT)
|
||||||
|
.exportRoot(exportRoot)
|
||||||
|
.build();
|
||||||
|
WorldgenExportResult result = exporter.export(request, WorldgenExportProgress.NONE);
|
||||||
|
if (!result.isSuccess()) {
|
||||||
|
throw new IllegalStateException("Datapack export of dimension '" + dimension + "' was " + result.status() + ": " + result.message());
|
||||||
|
}
|
||||||
|
System.out.println(LOG_PREFIX + " exported target=" + TARGET_VERSION + " biomes=" + result.loadKeyByBiomeId().size()
|
||||||
|
+ " diagnostics=" + result.diagnostics().size() + " " + diagnosticCounts(result.diagnostics()));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FidelityStats scan(FidelitySource source, Configuration configuration) {
|
||||||
|
long started = System.nanoTime();
|
||||||
|
FidelityStats stats = FidelityScanner.scan(source, 0, 0, configuration.radiusChunks(), configuration.stride(),
|
||||||
|
MultiBurst.burst.parallelism());
|
||||||
|
System.out.println(LOG_PREFIX + " sampled " + source.label() + " columns=" + stats.columns() + " terrainColumns=" + stats.heightColumns()
|
||||||
|
+ " in " + (System.nanoTime() - started) / 1_000_000L + "ms");
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void requireSameHeightRange(int irisMinY, int irisMaxY, int exportMinY, int exportMaxY) {
|
||||||
|
if (irisMinY != exportMinY || irisMaxY != exportMaxY) {
|
||||||
|
throw new IllegalStateException("Iris height range [" + irisMinY + ", " + irisMaxY
|
||||||
|
+ ") differs from the exported dimension range [" + exportMinY + ", " + exportMaxY + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SortedMap<String, Integer> diagnosticCounts(List<ExportDiagnostic> diagnostics) {
|
||||||
|
SortedMap<String, Integer> counts = new TreeMap<>();
|
||||||
|
for (ExportDiagnostic diagnostic : diagnostics) {
|
||||||
|
counts.merge(diagnostic.limit().name(), 1, Integer::sum);
|
||||||
|
}
|
||||||
|
return counts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeReport(Path output, String json) throws IOException {
|
||||||
|
Path parent = output.getParent();
|
||||||
|
Files.createDirectories(parent);
|
||||||
|
Path staged = Files.createTempFile(parent, ".datapack-fidelity-", ".json");
|
||||||
|
try {
|
||||||
|
Files.writeString(staged, json + "\n", StandardCharsets.UTF_8);
|
||||||
|
Files.move(staged, output, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||||
|
} finally {
|
||||||
|
Files.deleteIfExists(staged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum Mode {
|
||||||
|
FULL,
|
||||||
|
BIOMES;
|
||||||
|
|
||||||
|
private static Mode parse(String value) {
|
||||||
|
String normalized = value.trim().toLowerCase(Locale.ROOT);
|
||||||
|
for (Mode mode : values()) {
|
||||||
|
if (mode.label().equals(normalized)) {
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("mode must be full or biomes, got '" + value + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String label() {
|
||||||
|
return name().toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Outcome(FidelityReport report, String json, String table) {
|
||||||
|
private static Outcome of(FidelityReport report) {
|
||||||
|
return new Outcome(report, report.toJson(), report.toTable());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Outcome of(BiomeShareReport report) {
|
||||||
|
return new Outcome(report.mean(), report.toJson(), report.toTable());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Surveyed(FidelityStats iris, FidelityStats export) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Target(Path output, String namespace, String dimension, Map<String, String> loadKeyByBiomeId) {
|
||||||
|
private WorldgenExportSampler sampler(long seed) throws IOException {
|
||||||
|
return WorldgenExportSampler.open(output, WorldgenExportMode.OVERWORLD, namespace + ":" + dimension, seed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Configuration(Path pack, String dimension, long seed, int radiusChunks, int stride, Path output, SurveyWindow survey,
|
||||||
|
int seedCount, Mode mode) {
|
||||||
|
private static Configuration parse(String[] arguments) {
|
||||||
|
if (arguments.length != ARGUMENTS) {
|
||||||
|
throw new IllegalArgumentException(USAGE);
|
||||||
|
}
|
||||||
|
Path pack = Path.of(arguments[0]).toAbsolutePath().normalize();
|
||||||
|
if (!Files.isDirectory(pack)) {
|
||||||
|
throw new IllegalArgumentException("Pack folder " + pack + " does not exist");
|
||||||
|
}
|
||||||
|
String dimension = arguments[1].trim();
|
||||||
|
if (dimension.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Dimension key must not be blank");
|
||||||
|
}
|
||||||
|
long seed = parseLong(arguments[2], "seed");
|
||||||
|
int radiusChunks = parseInt(arguments[3], "radiusChunks");
|
||||||
|
if (radiusChunks < 0) {
|
||||||
|
throw new IllegalArgumentException("radiusChunks must be zero or positive, got " + radiusChunks);
|
||||||
|
}
|
||||||
|
int stride = parseInt(arguments[4], "stride");
|
||||||
|
if (stride < 1 || stride > MAX_STRIDE) {
|
||||||
|
throw new IllegalArgumentException("stride must be within [1, " + MAX_STRIDE + "], got " + stride);
|
||||||
|
}
|
||||||
|
Path output = Path.of(arguments[5]).toAbsolutePath().normalize();
|
||||||
|
if (Files.isDirectory(output)) {
|
||||||
|
throw new IllegalArgumentException("Report output " + output + " is a directory");
|
||||||
|
}
|
||||||
|
SurveyWindow survey = new SurveyWindow(0, 0, parseInt(arguments[6], "surveyBlocks"), parseInt(arguments[7], "surveyStride"));
|
||||||
|
int seedCount = parseInt(arguments[8], "surveySeeds");
|
||||||
|
if (seedCount < 1) {
|
||||||
|
throw new IllegalArgumentException("surveySeeds must be at least 1, got " + seedCount);
|
||||||
|
}
|
||||||
|
Mode mode = Mode.parse(arguments[9]);
|
||||||
|
return new Configuration(pack, dimension, seed, radiusChunks, stride, output, survey, seedCount, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Long> surveySeeds() {
|
||||||
|
List<Long> seeds = new ArrayList<>(seedCount);
|
||||||
|
for (int index = 0; index < seedCount; index++) {
|
||||||
|
seeds.add(seed + index);
|
||||||
|
}
|
||||||
|
return seeds;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long parseLong(String value, String name) {
|
||||||
|
try {
|
||||||
|
return Long.parseLong(value.trim());
|
||||||
|
} catch (NumberFormatException failure) {
|
||||||
|
throw new IllegalArgumentException(name + " must be an integer, got '" + value + "'", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int parseInt(String value, String name) {
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(value.trim());
|
||||||
|
} catch (NumberFormatException failure) {
|
||||||
|
throw new IllegalArgumentException(name + " must be an integer, got '" + value + "'", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+349
@@ -0,0 +1,349 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.util.CubicSpline;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.DensityFunction;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.DensityFunctions;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.generator.ConstantFunction;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.op.CacheFunction;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.op.FindTopSurfaceFunction;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.op.InterpolatedFunction;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.op.IntervalSelectFunction;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.op.RangeChoiceFunction;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.op.SplineFunction;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
final class DensityCostAnalysis {
|
||||||
|
private static final double LATTICE_CORNERS = 8.0;
|
||||||
|
private static final int MAX_SURFACE_STEPS = 4096;
|
||||||
|
private static final String INLINE = "<inline>";
|
||||||
|
|
||||||
|
private final Function<String, String> categories;
|
||||||
|
private final int topCount;
|
||||||
|
private final Map<Object, Costs> costs = new IdentityHashMap<>();
|
||||||
|
private final Map<DensityFunction, Body> bodies = new IdentityHashMap<>();
|
||||||
|
private final Map<DensityFunction, String> names = new IdentityHashMap<>();
|
||||||
|
|
||||||
|
DensityCostAnalysis(Function<String, String> categories, int topCount) {
|
||||||
|
this.categories = categories;
|
||||||
|
this.topCount = topCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
RootCost analyze(String root, DensityFunction function) {
|
||||||
|
Costs total = cost(function);
|
||||||
|
Reach reach = new Reach();
|
||||||
|
reach(function, reach, categories.apply(rootName(function, root)));
|
||||||
|
double cached = total.cachedLocal();
|
||||||
|
for (CacheFunction cache : reach.caches.keySet()) {
|
||||||
|
cached += cost(cache.input()).cachedLocal();
|
||||||
|
}
|
||||||
|
Body rootBody = new Body();
|
||||||
|
local(function, rootBody, true);
|
||||||
|
Map<DensityFunction, Double> multiplicity = multiplicity(rootBody);
|
||||||
|
List<Offender> offenders = new ArrayList<>(multiplicity.size());
|
||||||
|
Map<String, Double> shares = new TreeMap<>();
|
||||||
|
double expanded = total.expanded();
|
||||||
|
shares.merge(categories.apply(rootName(function, root)), rootBody.self / expanded, Double::sum);
|
||||||
|
for (Map.Entry<DensityFunction, Double> entry : multiplicity.entrySet()) {
|
||||||
|
DensityFunction value = entry.getKey();
|
||||||
|
Body body = body(value);
|
||||||
|
String name = names.get(value);
|
||||||
|
String category = categories.apply(name);
|
||||||
|
double contribution = entry.getValue() * body.self;
|
||||||
|
shares.merge(category, contribution / expanded, Double::sum);
|
||||||
|
offenders.add(new Offender(name, category, entry.getValue(), body.self, cost(value).expanded(),
|
||||||
|
contribution, entry.getValue() * cost(value).expanded()));
|
||||||
|
}
|
||||||
|
List<Offender> exclusive = top(offenders, Comparator.comparingDouble(Offender::contribution).reversed());
|
||||||
|
List<Offender> inclusive = top(offenders, Comparator.comparingDouble(Offender::inclusive).reversed());
|
||||||
|
Map<String, Double> dagShares = new TreeMap<>();
|
||||||
|
for (Map.Entry<String, Double> entry : reach.categories.entrySet()) {
|
||||||
|
dagShares.put(entry.getKey(), entry.getValue() / Math.max(1L, reach.nodes));
|
||||||
|
}
|
||||||
|
return new RootCost(root, reach.nodes, cached, expanded, total.lazy(), multiplicity.size(), exclusive, inclusive, shares, dagShares);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Offender> top(List<Offender> offenders, Comparator<Offender> order) {
|
||||||
|
List<Offender> sorted = new ArrayList<>(offenders);
|
||||||
|
sorted.sort(order);
|
||||||
|
return List.copyOf(sorted.subList(0, Math.min(topCount, sorted.size())));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String rootName(DensityFunction function, String root) {
|
||||||
|
if (function instanceof DensityFunctions.HolderHolder(Holder<DensityFunction> holder)) {
|
||||||
|
return name(holder);
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Costs cost(DensityFunction function) {
|
||||||
|
if (function instanceof DensityFunctions.HolderHolder(Holder<DensityFunction> holder)) {
|
||||||
|
DensityFunction value = holder.value();
|
||||||
|
names.putIfAbsent(value, name(holder));
|
||||||
|
return cost(value);
|
||||||
|
}
|
||||||
|
Costs known = costs.get(function);
|
||||||
|
if (known != null) {
|
||||||
|
return known;
|
||||||
|
}
|
||||||
|
Costs computed = compute(function);
|
||||||
|
costs.put(function, computed);
|
||||||
|
return computed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Costs compute(DensityFunction function) {
|
||||||
|
return switch (function) {
|
||||||
|
case ConstantFunction ignored -> new Costs(0.0, 0.0, 0.0);
|
||||||
|
case CacheFunction cache -> {
|
||||||
|
Costs input = cost(cache.input());
|
||||||
|
yield new Costs(1.0 + input.expanded(), 1.0 + input.lazy(), 1.0);
|
||||||
|
}
|
||||||
|
case SplineFunction spline -> {
|
||||||
|
Costs inner = spline(spline.spline());
|
||||||
|
yield new Costs(1.0 + inner.expanded(), 1.0 + inner.lazy(), 1.0 + inner.cachedLocal());
|
||||||
|
}
|
||||||
|
case IntervalSelectFunction select -> {
|
||||||
|
Costs input = cost(select.input());
|
||||||
|
double expanded = 1.0 + input.expanded();
|
||||||
|
double cached = 1.0 + input.cachedLocal();
|
||||||
|
double lazy = 0.0;
|
||||||
|
for (DensityFunction branch : select.functions()) {
|
||||||
|
Costs branchCost = cost(branch);
|
||||||
|
expanded += branchCost.expanded();
|
||||||
|
cached += branchCost.cachedLocal();
|
||||||
|
lazy = Math.max(lazy, branchCost.lazy());
|
||||||
|
}
|
||||||
|
yield new Costs(expanded, 1.0 + input.lazy() + lazy, cached);
|
||||||
|
}
|
||||||
|
case RangeChoiceFunction choice -> {
|
||||||
|
Costs input = cost(choice.input());
|
||||||
|
Costs inside = cost(choice.whenInRange());
|
||||||
|
Costs outside = cost(choice.whenOutOfRange());
|
||||||
|
yield new Costs(1.0 + input.expanded() + inside.expanded() + outside.expanded(),
|
||||||
|
1.0 + input.lazy() + Math.max(inside.lazy(), outside.lazy()),
|
||||||
|
1.0 + input.cachedLocal() + inside.cachedLocal() + outside.cachedLocal());
|
||||||
|
}
|
||||||
|
case InterpolatedFunction grid -> {
|
||||||
|
Costs input = cost(grid.input());
|
||||||
|
yield new Costs(1.0 + input.expanded(), 1.0 + LATTICE_CORNERS * input.expanded(), 1.0 + input.cachedLocal());
|
||||||
|
}
|
||||||
|
case FindTopSurfaceFunction surface -> {
|
||||||
|
Costs density = cost(surface.density());
|
||||||
|
Costs upper = cost(surface.upperBound());
|
||||||
|
double steps = surfaceSteps(surface);
|
||||||
|
yield new Costs(1.0 + density.expanded() + upper.expanded(), 1.0 + upper.lazy() + steps * density.lazy(),
|
||||||
|
1.0 + density.cachedLocal() + upper.cachedLocal());
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
double expanded = 1.0;
|
||||||
|
double lazy = 1.0;
|
||||||
|
double cached = 1.0;
|
||||||
|
for (DensityFunction child : children(function)) {
|
||||||
|
Costs childCost = cost(child);
|
||||||
|
expanded += childCost.expanded();
|
||||||
|
lazy += childCost.lazy();
|
||||||
|
cached += childCost.cachedLocal();
|
||||||
|
}
|
||||||
|
yield new Costs(expanded, lazy, cached);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Costs spline(CubicSpline<SplineFunction.Coordinate> spline) {
|
||||||
|
if (!(spline instanceof CubicSpline.Multipoint<SplineFunction.Coordinate> multipoint)) {
|
||||||
|
return new Costs(0.0, 0.0, 0.0);
|
||||||
|
}
|
||||||
|
Costs known = costs.get(multipoint);
|
||||||
|
if (known != null) {
|
||||||
|
return known;
|
||||||
|
}
|
||||||
|
Costs coordinate = cost(multipoint.coordinate().function());
|
||||||
|
List<CubicSpline<SplineFunction.Coordinate>> values = multipoint.values();
|
||||||
|
double expanded = 1.0 + coordinate.expanded();
|
||||||
|
double cached = 1.0 + coordinate.cachedLocal();
|
||||||
|
double[] lazy = new double[values.size()];
|
||||||
|
for (int index = 0; index < values.size(); index++) {
|
||||||
|
Costs value = spline(values.get(index));
|
||||||
|
expanded += value.expanded();
|
||||||
|
cached += value.cachedLocal();
|
||||||
|
lazy[index] = value.lazy();
|
||||||
|
}
|
||||||
|
double worstPair = lazy.length == 1 ? lazy[0] : 0.0;
|
||||||
|
for (int index = 0; index + 1 < lazy.length; index++) {
|
||||||
|
worstPair = Math.max(worstPair, lazy[index] + lazy[index + 1]);
|
||||||
|
}
|
||||||
|
Costs computed = new Costs(expanded, 1.0 + coordinate.lazy() + worstPair, cached);
|
||||||
|
costs.put(multipoint, computed);
|
||||||
|
return computed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reach(DensityFunction function, Reach reach, String category) {
|
||||||
|
if (function instanceof DensityFunctions.HolderHolder(Holder<DensityFunction> holder)) {
|
||||||
|
reach(holder.value(), reach, categories.apply(name(holder)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (function instanceof ConstantFunction || reach.visited.put(function, Boolean.TRUE) != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reach.count(category);
|
||||||
|
if (function instanceof CacheFunction cache) {
|
||||||
|
reach.caches.put(cache, Boolean.TRUE);
|
||||||
|
}
|
||||||
|
if (function instanceof SplineFunction spline) {
|
||||||
|
reachSpline(spline.spline(), reach, category);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (DensityFunction child : children(function)) {
|
||||||
|
reach(child, reach, category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reachSpline(CubicSpline<SplineFunction.Coordinate> spline, Reach reach, String category) {
|
||||||
|
if (!(spline instanceof CubicSpline.Multipoint<SplineFunction.Coordinate> multipoint)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (reach.visited.put(multipoint, Boolean.TRUE) != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reach.count(category);
|
||||||
|
reach(multipoint.coordinate().function(), reach, category);
|
||||||
|
for (CubicSpline<SplineFunction.Coordinate> value : multipoint.values()) {
|
||||||
|
reachSpline(value, reach, category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Body body(DensityFunction value) {
|
||||||
|
Body known = bodies.get(value);
|
||||||
|
if (known != null) {
|
||||||
|
return known;
|
||||||
|
}
|
||||||
|
Body body = new Body();
|
||||||
|
local(value, body, true);
|
||||||
|
bodies.put(value, body);
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void local(DensityFunction function, Body body, boolean top) {
|
||||||
|
if (function instanceof DensityFunctions.HolderHolder(Holder<DensityFunction> holder)) {
|
||||||
|
if (!top) {
|
||||||
|
DensityFunction value = holder.value();
|
||||||
|
names.putIfAbsent(value, name(holder));
|
||||||
|
body.references.merge(value, 1.0, Double::sum);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
local(holder.value(), body, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (function instanceof ConstantFunction) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
body.self++;
|
||||||
|
if (function instanceof SplineFunction spline) {
|
||||||
|
localSpline(spline.spline(), body);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (DensityFunction child : children(function)) {
|
||||||
|
local(child, body, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void localSpline(CubicSpline<SplineFunction.Coordinate> spline, Body body) {
|
||||||
|
if (!(spline instanceof CubicSpline.Multipoint<SplineFunction.Coordinate> multipoint)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
body.self++;
|
||||||
|
local(multipoint.coordinate().function(), body, false);
|
||||||
|
for (CubicSpline<SplineFunction.Coordinate> value : multipoint.values()) {
|
||||||
|
localSpline(value, body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<DensityFunction, Double> multiplicity(Body root) {
|
||||||
|
List<DensityFunction> order = new ArrayList<>();
|
||||||
|
Map<DensityFunction, Boolean> visited = new IdentityHashMap<>();
|
||||||
|
for (DensityFunction child : root.references.keySet()) {
|
||||||
|
postOrder(child, visited, order);
|
||||||
|
}
|
||||||
|
Collections.reverse(order);
|
||||||
|
Map<DensityFunction, Double> multiplicity = new IdentityHashMap<>(order.size() * 2);
|
||||||
|
for (Map.Entry<DensityFunction, Double> entry : root.references.entrySet()) {
|
||||||
|
multiplicity.merge(entry.getKey(), entry.getValue(), Double::sum);
|
||||||
|
}
|
||||||
|
for (DensityFunction value : order) {
|
||||||
|
double times = multiplicity.getOrDefault(value, 0.0);
|
||||||
|
for (Map.Entry<DensityFunction, Double> entry : body(value).references.entrySet()) {
|
||||||
|
multiplicity.merge(entry.getKey(), times * entry.getValue(), Double::sum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return multiplicity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void postOrder(DensityFunction value, Map<DensityFunction, Boolean> visited, List<DensityFunction> order) {
|
||||||
|
if (visited.put(value, Boolean.TRUE) != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (DensityFunction child : body(value).references.keySet()) {
|
||||||
|
postOrder(child, visited, order);
|
||||||
|
}
|
||||||
|
order.add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double surfaceSteps(FindTopSurfaceFunction surface) {
|
||||||
|
double upper = surface.upperBound().range().max();
|
||||||
|
if (!Double.isFinite(upper)) {
|
||||||
|
return MAX_SURFACE_STEPS;
|
||||||
|
}
|
||||||
|
double steps = Math.floor((upper - surface.lowerBound()) / surface.cellHeight()) + 1.0;
|
||||||
|
return Math.clamp(steps, 1.0, MAX_SURFACE_STEPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<DensityFunction> children(DensityFunction function) {
|
||||||
|
List<DensityFunction> children = new ArrayList<>(4);
|
||||||
|
function.rewriteChildren(child -> {
|
||||||
|
children.add(child);
|
||||||
|
return child;
|
||||||
|
});
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String name(Holder<DensityFunction> holder) {
|
||||||
|
return holder.unwrapKey().map(key -> key.identifier().toString()).orElse(INLINE);
|
||||||
|
}
|
||||||
|
|
||||||
|
record Costs(double expanded, double lazy, double cachedLocal) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record Offender(String name, String category, double multiplicity, double selfNodes, double subtreeNodes,
|
||||||
|
double contribution, double inclusive) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record RootCost(String root, long dagNodes, double cachedNodes, double expandedNodes, double uncachedPointWorst,
|
||||||
|
int namedSubtrees, List<Offender> exclusive, List<Offender> inclusive, Map<String, Double> categoryShare,
|
||||||
|
Map<String, Double> dagShare) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Reach {
|
||||||
|
private final Map<Object, Boolean> visited = new IdentityHashMap<>();
|
||||||
|
private final Map<CacheFunction, Boolean> caches = new IdentityHashMap<>();
|
||||||
|
private final Map<String, Double> categories = new TreeMap<>();
|
||||||
|
private long nodes;
|
||||||
|
|
||||||
|
private void count(String category) {
|
||||||
|
nodes++;
|
||||||
|
categories.merge(category, 1.0, Double::sum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Body {
|
||||||
|
private final Map<DensityFunction, Double> references = new IdentityHashMap<>();
|
||||||
|
private double self;
|
||||||
|
}
|
||||||
|
}
|
||||||
+409
@@ -0,0 +1,409 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import com.google.gson.JsonPrimitive;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
final class DensityJsonBreakdown {
|
||||||
|
private static final String DENSITY_FUNCTIONS = "/worldgen/density_function/";
|
||||||
|
private static final String NOISES = "/worldgen/noise/";
|
||||||
|
private static final String JSON = ".json";
|
||||||
|
private static final String CELLULAR = "cellular";
|
||||||
|
private static final String NOISE_STYLE = "noise style";
|
||||||
|
private static final String UNREFERENCED = "unreferenced";
|
||||||
|
private static final String OFFSET_MARKER = "/offset_";
|
||||||
|
private static final String OFFSET_SUFFIX = " offset copies";
|
||||||
|
private static final int INSTANCE_SEGMENTS = 3;
|
||||||
|
private static final Set<String> ROUTER_ROOTS = Set.of("final_density", "temperature", "vegetation", "continents", "erosion",
|
||||||
|
"depth", "ridges", "chunk_surface_level", "barrier", "fluid_level_floodedness", "fluid_level_spread", "lava", "exclusion",
|
||||||
|
"surface_level", "spawn_elevation");
|
||||||
|
private static final long FNV_OFFSET = 0xcbf29ce484222325L;
|
||||||
|
private static final long FNV_PRIME = 0x100000001b3L;
|
||||||
|
private static final int TOP = 10;
|
||||||
|
|
||||||
|
private final String dimension;
|
||||||
|
private final String dimensionPrefix;
|
||||||
|
private final Map<String, Integer> categoryIndex = new HashMap<>();
|
||||||
|
private final List<String> categoryNames = new ArrayList<>();
|
||||||
|
private final Map<String, Entry> entries = new TreeMap<>();
|
||||||
|
private final Map<String, List<String>> parents = new HashMap<>();
|
||||||
|
private final Map<String, Long> owners = new HashMap<>();
|
||||||
|
|
||||||
|
private DensityJsonBreakdown(String dimension) {
|
||||||
|
this.dimension = dimension;
|
||||||
|
dimensionPrefix = dimension + "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
static DensityJsonBreakdown analyze(SortedMap<String, byte[]> files, String dimension) {
|
||||||
|
DensityJsonBreakdown breakdown = new DensityJsonBreakdown(dimension);
|
||||||
|
breakdown.load(files);
|
||||||
|
return breakdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String category(String dimension, String id) {
|
||||||
|
String base = baseCategory(dimension, id);
|
||||||
|
return id.contains(OFFSET_MARKER) ? base + OFFSET_SUFFIX : base;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String baseCategory(String dimension, String id) {
|
||||||
|
int separator = id.indexOf(':');
|
||||||
|
String path = separator < 0 ? id : id.substring(separator + 1);
|
||||||
|
String dimensionPrefix = dimension + "/";
|
||||||
|
if (path.startsWith(dimensionPrefix)) {
|
||||||
|
path = path.substring(dimensionPrefix.length());
|
||||||
|
}
|
||||||
|
if (ROUTER_ROOTS.contains(path)) {
|
||||||
|
return "router";
|
||||||
|
}
|
||||||
|
if (path.startsWith("cell/")) {
|
||||||
|
return CELLULAR;
|
||||||
|
}
|
||||||
|
if (path.startsWith("style/")) {
|
||||||
|
return NOISE_STYLE;
|
||||||
|
}
|
||||||
|
if (path.startsWith("biome/selector/")) {
|
||||||
|
return "biome selector";
|
||||||
|
}
|
||||||
|
if (path.startsWith("biome/carving/")) {
|
||||||
|
return "biome carving";
|
||||||
|
}
|
||||||
|
if (path.startsWith("biome/")) {
|
||||||
|
return "biome decision";
|
||||||
|
}
|
||||||
|
if (path.equals("height") || path.startsWith("height/") || path.startsWith("terrain/height/")) {
|
||||||
|
return "terrain height";
|
||||||
|
}
|
||||||
|
if (path.startsWith("terrain/generator/")) {
|
||||||
|
return "terrain generator";
|
||||||
|
}
|
||||||
|
if (path.startsWith("terrain/volume/")) {
|
||||||
|
return "terrain volumetric";
|
||||||
|
}
|
||||||
|
if (path.startsWith("terrain/")) {
|
||||||
|
return "terrain bands";
|
||||||
|
}
|
||||||
|
if (path.startsWith("cave")) {
|
||||||
|
return "caves";
|
||||||
|
}
|
||||||
|
if (path.startsWith("surface/")) {
|
||||||
|
return "surface";
|
||||||
|
}
|
||||||
|
int slash = path.indexOf('/');
|
||||||
|
return slash < 0 ? path : path.substring(0, slash);
|
||||||
|
}
|
||||||
|
|
||||||
|
Report report() {
|
||||||
|
Map<String, CategoryAccumulator> categories = new TreeMap<>();
|
||||||
|
Map<String, CategoryAccumulator> groups = new TreeMap<>();
|
||||||
|
Map<String, Long> libraryOwners = new TreeMap<>();
|
||||||
|
long totalBytes = 0;
|
||||||
|
long totalNodes = 0;
|
||||||
|
for (Map.Entry<String, Entry> item : entries.entrySet()) {
|
||||||
|
String id = item.getKey();
|
||||||
|
Entry entry = item.getValue();
|
||||||
|
totalBytes += entry.bytes;
|
||||||
|
totalNodes += entry.nodes;
|
||||||
|
String category = category(dimension, id);
|
||||||
|
categories.computeIfAbsent(category, ignored -> new CategoryAccumulator()).add(entry);
|
||||||
|
groups.computeIfAbsent(group(id), ignored -> new CategoryAccumulator()).add(entry);
|
||||||
|
if (isLibrary(category)) {
|
||||||
|
libraryOwners.merge(category + " via " + ownerLabel(owners(id)), entry.bytes, Long::sum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<CategoryBytes> categoryRows = rows(categories, Integer.MAX_VALUE);
|
||||||
|
List<CategoryBytes> groupRows = rows(groups, 40);
|
||||||
|
List<Bytes> ownerRows = new ArrayList<>();
|
||||||
|
for (Map.Entry<String, Long> owner : libraryOwners.entrySet()) {
|
||||||
|
ownerRows.add(new Bytes(owner.getKey(), owner.getValue()));
|
||||||
|
}
|
||||||
|
ownerRows.sort(Comparator.comparingLong(Bytes::bytes).reversed());
|
||||||
|
List<Bytes> largest = new ArrayList<>();
|
||||||
|
for (Map.Entry<String, Entry> item : entries.entrySet()) {
|
||||||
|
largest.add(new Bytes(item.getKey(), item.getValue().bytes));
|
||||||
|
}
|
||||||
|
largest.sort(Comparator.comparingLong(Bytes::bytes).reversed());
|
||||||
|
return new Report(entries.size(), totalBytes, totalNodes, categoryRows, groupRows, ownerRows,
|
||||||
|
List.copyOf(largest.subList(0, Math.min(TOP, largest.size()))), cellularInstances());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void load(SortedMap<String, byte[]> files) {
|
||||||
|
Map<String, byte[]> bodies = new HashMap<>();
|
||||||
|
Set<String> noises = new HashSet<>();
|
||||||
|
for (Map.Entry<String, byte[]> file : files.entrySet()) {
|
||||||
|
String id = resourceId(file.getKey(), DENSITY_FUNCTIONS);
|
||||||
|
if (id != null) {
|
||||||
|
bodies.put(id, file.getValue());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String noise = resourceId(file.getKey(), NOISES);
|
||||||
|
if (noise != null) {
|
||||||
|
noises.add(noise);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, byte[]> body : bodies.entrySet()) {
|
||||||
|
JsonElement json = JsonParser.parseString(new String(body.getValue(), StandardCharsets.UTF_8));
|
||||||
|
Entry entry = new Entry(body.getValue().length);
|
||||||
|
entry.shape = shape(json, bodies.keySet(), noises, entry);
|
||||||
|
entries.put(body.getKey(), entry);
|
||||||
|
for (String reference : entry.references) {
|
||||||
|
parents.computeIfAbsent(reference, ignored -> new ArrayList<>()).add(body.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long shape(JsonElement element, Set<String> functions, Set<String> noises, Entry entry) {
|
||||||
|
if (element.isJsonObject()) {
|
||||||
|
entry.nodes++;
|
||||||
|
JsonObject object = element.getAsJsonObject();
|
||||||
|
long hash = mix(FNV_OFFSET, '{');
|
||||||
|
for (String key : new TreeMap<>(object.asMap()).keySet()) {
|
||||||
|
hash = mix(mix(hash, key.hashCode()), shape(object.get(key), functions, noises, entry));
|
||||||
|
}
|
||||||
|
return mix(hash, '}');
|
||||||
|
}
|
||||||
|
if (element.isJsonArray()) {
|
||||||
|
JsonArray array = element.getAsJsonArray();
|
||||||
|
long hash = mix(FNV_OFFSET, '[');
|
||||||
|
for (JsonElement item : array) {
|
||||||
|
hash = mix(hash, shape(item, functions, noises, entry));
|
||||||
|
}
|
||||||
|
return mix(hash, ']');
|
||||||
|
}
|
||||||
|
if (element.isJsonPrimitive()) {
|
||||||
|
JsonPrimitive primitive = element.getAsJsonPrimitive();
|
||||||
|
if (primitive.isNumber()) {
|
||||||
|
return mix(FNV_OFFSET, 'N');
|
||||||
|
}
|
||||||
|
if (primitive.isString()) {
|
||||||
|
String value = primitive.getAsString();
|
||||||
|
if (functions.contains(value)) {
|
||||||
|
entry.references.add(value);
|
||||||
|
return mix(FNV_OFFSET, 'R');
|
||||||
|
}
|
||||||
|
if (noises.contains(value)) {
|
||||||
|
return mix(FNV_OFFSET, 'Z');
|
||||||
|
}
|
||||||
|
return mix(FNV_OFFSET, value.hashCode());
|
||||||
|
}
|
||||||
|
return mix(FNV_OFFSET, primitive.getAsBoolean() ? 'T' : 'F');
|
||||||
|
}
|
||||||
|
return mix(FNV_OFFSET, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<CellularShape> cellularInstances() {
|
||||||
|
Map<String, List<String>> instances = new TreeMap<>();
|
||||||
|
for (String id : entries.keySet()) {
|
||||||
|
if (!isLibrary(category(dimension, id)) || !id.contains(":cell/")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
instances.computeIfAbsent(instanceKey(id), ignored -> new ArrayList<>()).add(id);
|
||||||
|
}
|
||||||
|
Map<Long, CellularAccumulator> shapes = new HashMap<>();
|
||||||
|
for (Map.Entry<String, List<String>> instance : instances.entrySet()) {
|
||||||
|
List<String> members = instance.getValue();
|
||||||
|
members.sort(String::compareTo);
|
||||||
|
long hash = FNV_OFFSET;
|
||||||
|
long bytes = 0;
|
||||||
|
long owner = 0L;
|
||||||
|
StringBuilder layout = new StringBuilder();
|
||||||
|
for (String member : members) {
|
||||||
|
Entry entry = entries.get(member);
|
||||||
|
String suffix = member.substring(instance.getKey().length());
|
||||||
|
hash = mix(mix(hash, suffix.hashCode()), entry.shape);
|
||||||
|
bytes += entry.bytes;
|
||||||
|
owner |= owners(member);
|
||||||
|
if (!layout.isEmpty()) {
|
||||||
|
layout.append(',');
|
||||||
|
}
|
||||||
|
layout.append(suffix);
|
||||||
|
}
|
||||||
|
CellularAccumulator accumulator = shapes.computeIfAbsent(hash, ignored -> new CellularAccumulator(instance.getKey(), layout.toString()));
|
||||||
|
accumulator.instances++;
|
||||||
|
accumulator.bytes += bytes;
|
||||||
|
accumulator.owners |= owner;
|
||||||
|
}
|
||||||
|
List<CellularShape> rows = new ArrayList<>(shapes.size());
|
||||||
|
for (CellularAccumulator accumulator : shapes.values()) {
|
||||||
|
rows.add(new CellularShape(accumulator.example, accumulator.layout, accumulator.instances, accumulator.bytes, ownerLabel(accumulator.owners)));
|
||||||
|
}
|
||||||
|
rows.sort(Comparator.comparingLong(CellularShape::bytes).reversed());
|
||||||
|
return List.copyOf(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long owners(String id) {
|
||||||
|
Long known = owners.get(id);
|
||||||
|
if (known != null) {
|
||||||
|
return known;
|
||||||
|
}
|
||||||
|
owners.put(id, 0L);
|
||||||
|
long mask = 0L;
|
||||||
|
List<String> referrers = parents.get(id);
|
||||||
|
if (referrers == null) {
|
||||||
|
mask = bit(UNREFERENCED);
|
||||||
|
} else {
|
||||||
|
for (String parent : referrers) {
|
||||||
|
String category = category(dimension, parent);
|
||||||
|
mask |= isLibrary(category) ? owners(parent) : bit(category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
owners.put(id, mask);
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long bit(String category) {
|
||||||
|
int index = categoryIndex.computeIfAbsent(category, name -> {
|
||||||
|
categoryNames.add(name);
|
||||||
|
return categoryNames.size() - 1;
|
||||||
|
});
|
||||||
|
if (index >= Long.SIZE) {
|
||||||
|
throw new IllegalStateException("More than " + Long.SIZE + " density function owner categories");
|
||||||
|
}
|
||||||
|
return 1L << index;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String ownerLabel(long mask) {
|
||||||
|
if (mask == 0L) {
|
||||||
|
return UNREFERENCED;
|
||||||
|
}
|
||||||
|
List<String> labels = new ArrayList<>(Long.bitCount(mask));
|
||||||
|
for (int index = 0; index < categoryNames.size(); index++) {
|
||||||
|
if ((mask & (1L << index)) != 0L) {
|
||||||
|
labels.add(categoryNames.get(index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
labels.sort(String::compareTo);
|
||||||
|
return String.join("+", labels);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String group(String id) {
|
||||||
|
int separator = id.indexOf(':');
|
||||||
|
String path = separator < 0 ? id : id.substring(separator + 1);
|
||||||
|
if (path.startsWith(dimensionPrefix)) {
|
||||||
|
path = path.substring(dimensionPrefix.length());
|
||||||
|
}
|
||||||
|
String[] segments = path.split("/");
|
||||||
|
if (segments.length == 1) {
|
||||||
|
return segments[0];
|
||||||
|
}
|
||||||
|
if (segments[0].equals("cell") || segments[0].equals("style")) {
|
||||||
|
return segments[0] + "/*/" + segments[segments.length - 1];
|
||||||
|
}
|
||||||
|
return segments[0] + "/" + segments[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isLibrary(String category) {
|
||||||
|
return category.startsWith(CELLULAR) || category.startsWith(NOISE_STYLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String instanceKey(String id) {
|
||||||
|
int end = id.indexOf(':') + 1;
|
||||||
|
for (int segment = 0; segment < INSTANCE_SEGMENTS && end > 0; segment++) {
|
||||||
|
int slash = id.indexOf('/', end);
|
||||||
|
if (slash < 0) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
end = slash + 1;
|
||||||
|
}
|
||||||
|
return id.substring(0, end - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<CategoryBytes> rows(Map<String, CategoryAccumulator> accumulators, int limit) {
|
||||||
|
List<CategoryBytes> rows = new ArrayList<>(accumulators.size());
|
||||||
|
for (Map.Entry<String, CategoryAccumulator> item : accumulators.entrySet()) {
|
||||||
|
CategoryAccumulator accumulator = item.getValue();
|
||||||
|
long representative = 0;
|
||||||
|
for (long bytes : accumulator.representativeBytes.values()) {
|
||||||
|
representative += bytes;
|
||||||
|
}
|
||||||
|
rows.add(new CategoryBytes(item.getKey(), accumulator.files, accumulator.bytes, accumulator.nodes,
|
||||||
|
accumulator.representativeBytes.size(), accumulator.bytes - representative));
|
||||||
|
}
|
||||||
|
rows.sort(Comparator.comparingLong(CategoryBytes::bytes).reversed());
|
||||||
|
return List.copyOf(rows.subList(0, Math.min(limit, rows.size())));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String resourceId(String path, String folder) {
|
||||||
|
int marker = path.indexOf(folder);
|
||||||
|
if (!path.startsWith("data/") || marker < 0 || !path.endsWith(JSON)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return path.substring("data/".length(), marker) + ":" + path.substring(marker + folder.length(), path.length() - JSON.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long mix(long hash, long value) {
|
||||||
|
long mixed = hash ^ value;
|
||||||
|
mixed *= FNV_PRIME;
|
||||||
|
return mixed ^ (mixed >>> 29);
|
||||||
|
}
|
||||||
|
|
||||||
|
record Report(int files, long bytes, long nodes, List<CategoryBytes> categories, List<CategoryBytes> groups,
|
||||||
|
List<Bytes> libraryOwners, List<Bytes> largest, List<CellularShape> cellularShapes) {
|
||||||
|
int cellularInstances() {
|
||||||
|
int count = 0;
|
||||||
|
for (CellularShape shape : cellularShapes) {
|
||||||
|
count += shape.instances();
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record CategoryBytes(String category, int files, long bytes, long nodes, int distinctShapes, long structurallyDuplicateBytes) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record Bytes(String name, long bytes) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record CellularShape(String example, String members, int instances, long bytes, String owners) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Entry {
|
||||||
|
private final long bytes;
|
||||||
|
private final List<String> references = new ArrayList<>();
|
||||||
|
private long nodes;
|
||||||
|
private long shape;
|
||||||
|
|
||||||
|
private Entry(long bytes) {
|
||||||
|
this.bytes = bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class CategoryAccumulator {
|
||||||
|
private final Map<Long, Long> representativeBytes = new HashMap<>();
|
||||||
|
private int files;
|
||||||
|
private long bytes;
|
||||||
|
private long nodes;
|
||||||
|
|
||||||
|
private void add(Entry entry) {
|
||||||
|
files++;
|
||||||
|
bytes += entry.bytes;
|
||||||
|
nodes += entry.nodes;
|
||||||
|
representativeBytes.putIfAbsent(entry.shape, entry.bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class CellularAccumulator {
|
||||||
|
private final String example;
|
||||||
|
private final String layout;
|
||||||
|
private int instances;
|
||||||
|
private long bytes;
|
||||||
|
private long owners;
|
||||||
|
|
||||||
|
private CellularAccumulator(String example, String layout) {
|
||||||
|
this.example = example;
|
||||||
|
this.layout = layout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
@@ -1,6 +1,7 @@
|
|||||||
package art.arcane.iris.probe;
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
import art.arcane.iris.configuration.IrisSettings;
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
|
import art.arcane.iris.platform.ClasspathVanillaDataSource;
|
||||||
import art.arcane.iris.platform.bukkit.nms.BukkitGenerationRegistry;
|
import art.arcane.iris.platform.bukkit.nms.BukkitGenerationRegistry;
|
||||||
import art.arcane.iris.spi.IrisPlatform;
|
import art.arcane.iris.spi.IrisPlatform;
|
||||||
import art.arcane.iris.spi.PlatformGenerationRegistry;
|
import art.arcane.iris.spi.PlatformGenerationRegistry;
|
||||||
@@ -9,6 +10,7 @@ import art.arcane.iris.spi.PlatformBiomeWriter;
|
|||||||
import art.arcane.iris.spi.PlatformRegistries;
|
import art.arcane.iris.spi.PlatformRegistries;
|
||||||
import art.arcane.iris.spi.PlatformScheduler;
|
import art.arcane.iris.spi.PlatformScheduler;
|
||||||
import art.arcane.iris.spi.PlatformStructureHooks;
|
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||||
|
import art.arcane.iris.spi.VanillaDataSource;
|
||||||
import art.arcane.volmlib.nativelib.entity.NativeEntityType;
|
import art.arcane.volmlib.nativelib.entity.NativeEntityType;
|
||||||
import art.arcane.volmlib.nativelib.item.NativeItem;
|
import art.arcane.volmlib.nativelib.item.NativeItem;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
|
||||||
@@ -35,6 +37,7 @@ final class HeadlessNativePlatform implements IrisPlatform, PlatformRegistries,
|
|||||||
private final NativeRegistryAccess nativeAccess;
|
private final NativeRegistryAccess nativeAccess;
|
||||||
private final PlatformBiomeWriter biomeWriter;
|
private final PlatformBiomeWriter biomeWriter;
|
||||||
private final PlatformGenerationRegistry generationRegistry;
|
private final PlatformGenerationRegistry generationRegistry;
|
||||||
|
private final ClasspathVanillaDataSource vanillaData = new ClasspathVanillaDataSource(() -> SharedConstants.class);
|
||||||
private final NativeBlockResolver blocks = new NativeBlockResolver(this);
|
private final NativeBlockResolver blocks = new NativeBlockResolver(this);
|
||||||
private final NativeBlockState air = blocks.getAir();
|
private final NativeBlockState air = blocks.getAir();
|
||||||
private final ConcurrentHashMap<String, NativeBlockState> states = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<String, NativeBlockState> states = new ConcurrentHashMap<>();
|
||||||
@@ -215,6 +218,11 @@ final class HeadlessNativePlatform implements IrisPlatform, PlatformRegistries,
|
|||||||
return biomeWriter;
|
return biomeWriter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public VanillaDataSource vanillaData() {
|
||||||
|
return vanillaData;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public File dataFolder() {
|
public File dataFolder() {
|
||||||
return services.dataFolder();
|
return services.dataFolder();
|
||||||
|
|||||||
+11
-4
@@ -4,6 +4,7 @@ import art.arcane.iris.pack.datapack.IrisDatapackCompiler;
|
|||||||
import art.arcane.iris.pack.datapack.v263.DataFixerV263;
|
import art.arcane.iris.pack.datapack.v263.DataFixerV263;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
|
||||||
|
import net.minecraft.core.HolderLookup;
|
||||||
import net.minecraft.core.RegistryAccess;
|
import net.minecraft.core.RegistryAccess;
|
||||||
import net.minecraft.core.registries.BuiltInRegistries;
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
@@ -25,6 +26,7 @@ import java.util.Map;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.function.Function;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
final class HeadlessNativeRegistries implements AutoCloseable {
|
final class HeadlessNativeRegistries implements AutoCloseable {
|
||||||
@@ -70,7 +72,7 @@ final class HeadlessNativeRegistries implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HeadlessNativeRegistries load(Path directory) {
|
static HeadlessNativeRegistries load(Path directory) {
|
||||||
PackLocationInfo location = new PackLocationInfo("iris-headless", Component.literal("Iris"),
|
PackLocationInfo location = new PackLocationInfo("iris-headless", Component.literal("Iris"),
|
||||||
PackSource.WORLD, Optional.empty());
|
PackSource.WORLD, Optional.empty());
|
||||||
MultiPackResourceManager resources = new MultiPackResourceManager(PackType.SERVER_DATA, List.of(
|
MultiPackResourceManager resources = new MultiPackResourceManager(PackType.SERVER_DATA, List.of(
|
||||||
@@ -78,12 +80,17 @@ final class HeadlessNativeRegistries implements AutoCloseable {
|
|||||||
try {
|
try {
|
||||||
RegistryAccess.Frozen builtins = RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY);
|
RegistryAccess.Frozen builtins = RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY);
|
||||||
RegistryAccess.Frozen loaded;
|
RegistryAccess.Frozen loaded;
|
||||||
|
RegistryAccess.Frozen dimensions;
|
||||||
try (ExecutorService executor = Executors.newFixedThreadPool(4)) {
|
try (ExecutorService executor = Executors.newFixedThreadPool(4)) {
|
||||||
loaded = RegistryDataLoader.load(resources, builtins.listRegistries().toList(),
|
loaded = RegistryDataLoader.load(resources, builtins.listRegistries().toList(),
|
||||||
RegistryDataLoader.WORLD_REGISTRIES, executor).join();
|
RegistryDataLoader.WORLD_REGISTRIES, executor).join();
|
||||||
|
List<HolderLookup.RegistryLookup<?>> dimensionContext =
|
||||||
|
Stream.concat(builtins.listRegistries(), loaded.listRegistries()).toList();
|
||||||
|
dimensions = RegistryDataLoader.load(resources, dimensionContext,
|
||||||
|
RegistryDataLoader.DIMENSION_REGISTRIES, executor).join();
|
||||||
}
|
}
|
||||||
RegistryAccess.Frozen registries = new RegistryAccess.ImmutableRegistryAccess(
|
RegistryAccess.Frozen registries = new RegistryAccess.ImmutableRegistryAccess(Stream.of(
|
||||||
Stream.concat(builtins.registries(), loaded.registries())).freeze();
|
builtins.registries(), loaded.registries(), dimensions.registries()).flatMap(Function.identity())).freeze();
|
||||||
HeadlessNativeBootstrap.bindComponents(registries);
|
HeadlessNativeBootstrap.bindComponents(registries);
|
||||||
return new HeadlessNativeRegistries(directory, resources, registries);
|
return new HeadlessNativeRegistries(directory, resources, registries);
|
||||||
} catch (RuntimeException failure) {
|
} catch (RuntimeException failure) {
|
||||||
@@ -106,7 +113,7 @@ final class HeadlessNativeRegistries implements AutoCloseable {
|
|||||||
delete(directory);
|
delete(directory);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void delete(Path directory) throws IOException {
|
static void delete(Path directory) throws IOException {
|
||||||
try (Stream<Path> paths = Files.walk(directory)) {
|
try (Stream<Path> paths = Files.walk(directory)) {
|
||||||
for (Path path : paths.sorted(Comparator.reverseOrder()).toList()) {
|
for (Path path : paths.sorted(Comparator.reverseOrder()).toList()) {
|
||||||
Files.delete(path);
|
Files.delete(path);
|
||||||
|
|||||||
+8
-2
@@ -32,6 +32,7 @@ final class HeadlessNativeRuntime implements RealPackProbeSupport.RuntimeBinding
|
|||||||
private final Path stagingRoot;
|
private final Path stagingRoot;
|
||||||
private NativeRegionTerrainWriter writer;
|
private NativeRegionTerrainWriter writer;
|
||||||
private HeadlessNativeRegistries nativeRegistries;
|
private HeadlessNativeRegistries nativeRegistries;
|
||||||
|
private File preparedPack;
|
||||||
private HolderLookup.Provider registries;
|
private HolderLookup.Provider registries;
|
||||||
|
|
||||||
HeadlessNativeRuntime() {
|
HeadlessNativeRuntime() {
|
||||||
@@ -60,12 +61,16 @@ final class HeadlessNativeRuntime implements RealPackProbeSupport.RuntimeBinding
|
|||||||
@Override
|
@Override
|
||||||
public void prepare(IrisData data, IrisDimension dimension) {
|
public void prepare(IrisData data, IrisDimension dimension) {
|
||||||
try {
|
try {
|
||||||
if (nativeRegistries != null) {
|
File pack = data.getDataFolder().getCanonicalFile();
|
||||||
|
if (nativeRegistries != null && !pack.equals(preparedPack)) {
|
||||||
throw new IllegalStateException("Headless native runtime supports one immutable pack per process");
|
throw new IllegalStateException("Headless native runtime supports one immutable pack per process");
|
||||||
}
|
}
|
||||||
|
if (nativeRegistries == null) {
|
||||||
nativeRegistries = HeadlessNativeRegistries.compile(data.getDataFolder());
|
nativeRegistries = HeadlessNativeRegistries.compile(data.getDataFolder());
|
||||||
registries = nativeRegistries.registries();
|
preparedPack = pack;
|
||||||
writer = new NativeRegionTerrainWriter(nativeRegistries.registries());
|
writer = new NativeRegionTerrainWriter(nativeRegistries.registries());
|
||||||
|
}
|
||||||
|
registries = nativeRegistries.registries();
|
||||||
} catch (IOException failure) {
|
} catch (IOException failure) {
|
||||||
throw new UncheckedIOException("Cannot load native generation registries", failure);
|
throw new UncheckedIOException("Cannot load native generation registries", failure);
|
||||||
}
|
}
|
||||||
@@ -92,6 +97,7 @@ final class HeadlessNativeRuntime implements RealPackProbeSupport.RuntimeBinding
|
|||||||
if (nativeRegistries != null) {
|
if (nativeRegistries != null) {
|
||||||
nativeRegistries.close();
|
nativeRegistries.close();
|
||||||
nativeRegistries = null;
|
nativeRegistries = null;
|
||||||
|
preparedPack = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+30
-12
@@ -2,8 +2,9 @@ package art.arcane.iris.probe;
|
|||||||
|
|
||||||
import art.arcane.iris.configuration.IrisSettings;
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
|
import art.arcane.iris.generation.hydrology.engine.HydrologyPregeneration;
|
||||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
|
import art.arcane.iris.generation.hydrology.plan.WindowKey;
|
||||||
|
import art.arcane.iris.world.pregen.PregenHydrology;
|
||||||
import art.arcane.iris.world.pregen.PregenPerformanceProfile;
|
import art.arcane.iris.world.pregen.PregenPerformanceProfile;
|
||||||
|
|
||||||
import net.minecraft.core.registries.Registries;
|
import net.minecraft.core.registries.Registries;
|
||||||
@@ -31,11 +32,16 @@ import java.nio.file.Path;
|
|||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.nio.file.StandardOpenOption;
|
import java.nio.file.StandardOpenOption;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.HexFormat;
|
import java.util.HexFormat;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
public final class OfflineRegionGenerator {
|
public final class OfflineRegionGenerator {
|
||||||
|
private static final int CHUNKS_PER_HYDROLOGY_REGION = WindowKey.SIZE >> 4;
|
||||||
|
|
||||||
private OfflineRegionGenerator() {
|
private OfflineRegionGenerator() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,14 +103,13 @@ public final class OfflineRegionGenerator {
|
|||||||
PregenPerformanceProfile.apply(session.engine());
|
PregenPerformanceProfile.apply(session.engine());
|
||||||
System.out.println("generationProfile=pregen noiseCacheSize=" + IrisSettings.get().getPerformance().getNoiseCacheSize()
|
System.out.println("generationProfile=pregen noiseCacheSize=" + IrisSettings.get().getPerformance().getNoiseCacheSize()
|
||||||
+ " burstParallelism=" + MultiBurst.burst.parallelism() + " multicore=true");
|
+ " burstParallelism=" + MultiBurst.burst.parallelism() + " multicore=true");
|
||||||
IrisHydrologyRuntime hydrology = session.engine().getComplex().getHydrologyRuntime();
|
try (HydrologyPregeneration pregeneration = PregenHydrology.open(session.engine());
|
||||||
try (HydrologyTileCache.PregenerationScope hydrologyScope = hydrology == null ? null
|
|
||||||
: hydrology.preparePregeneration(configuration.hydrologyArea());
|
|
||||||
HeadlessTerrainContext context = HeadlessTerrainContext.create(new HeadlessTerrainContext.Options(
|
HeadlessTerrainContext context = HeadlessTerrainContext.create(new HeadlessTerrainContext.Options(
|
||||||
session.engine(), runtime, staged.resolve("templates"), configuration.levelKey(),
|
session.engine(), runtime, staged.resolve("templates"), configuration.levelKey(),
|
||||||
configuration.noiseSettings(), spigot));
|
configuration.noiseSettings(), spigot));
|
||||||
RegionGenerationWindow workers = new RegionGenerationWindow(configuration.parallelism(),
|
RegionGenerationWindow workers = new RegionGenerationWindow(configuration.parallelism(),
|
||||||
RegionGenerationWindow.Policy.STANDALONE)) {
|
RegionGenerationWindow.Policy.STANDALONE)) {
|
||||||
|
PregenHydrology.prepareAll(pregeneration, configuration.hydrologyRegions());
|
||||||
HeadlessRegionTerrain.Session rolling = new HeadlessRegionTerrain.Session(workers);
|
HeadlessRegionTerrain.Session rolling = new HeadlessRegionTerrain.Session(workers);
|
||||||
Path regionDirectory = session.engine().getTarget().getWorld().worldFolder().toPath().resolve("region");
|
Path regionDirectory = session.engine().getTarget().getWorld().worldFolder().toPath().resolve("region");
|
||||||
Files.createDirectory(regionDirectory);
|
Files.createDirectory(regionDirectory);
|
||||||
@@ -239,14 +244,27 @@ public final class OfflineRegionGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HydrologyTileCache.PregenerationArea hydrologyArea() {
|
List<long[]> hydrologyRegions() {
|
||||||
int halo = NativeRegionPlan.terrainHalo();
|
int halo = NativeRegionPlan.terrainHalo();
|
||||||
long minimumX = ((long) chunkX - halo) * 16L;
|
Set<Long> seen = new HashSet<>();
|
||||||
long minimumZ = ((long) chunkZ - halo) * 16L;
|
List<long[]> regions = new ArrayList<>();
|
||||||
long maximumX = ((long) chunkX + (long) width * regionsX + halo) * 16L - 1L;
|
for (int row = 0; row < regionsZ; row++) {
|
||||||
long maximumZ = ((long) chunkZ + (long) width * regionsZ + halo) * 16L - 1L;
|
for (int column = 0; column < regionsX; column++) {
|
||||||
return new HydrologyTileCache.PregenerationArea(Math.toIntExact((minimumX + maximumX) / 2L),
|
int regionX = row % 2 == 0 ? column : regionsX - column - 1;
|
||||||
Math.toIntExact((minimumZ + maximumZ) / 2L), minimumX, minimumZ, maximumX, maximumZ);
|
int minimumX = Math.floorDiv(chunkX + regionX * width - halo, CHUNKS_PER_HYDROLOGY_REGION);
|
||||||
|
int maximumX = Math.floorDiv(chunkX + (regionX + 1) * width - 1 + halo, CHUNKS_PER_HYDROLOGY_REGION);
|
||||||
|
int minimumZ = Math.floorDiv(chunkZ + row * width - halo, CHUNKS_PER_HYDROLOGY_REGION);
|
||||||
|
int maximumZ = Math.floorDiv(chunkZ + (row + 1) * width - 1 + halo, CHUNKS_PER_HYDROLOGY_REGION);
|
||||||
|
for (int hydrologyZ = minimumZ; hydrologyZ <= maximumZ; hydrologyZ++) {
|
||||||
|
for (int hydrologyX = minimumX; hydrologyX <= maximumX; hydrologyX++) {
|
||||||
|
if (seen.add(((long) hydrologyX << 32) | (hydrologyZ & 0xFFFFFFFFL))) {
|
||||||
|
regions.add(new long[]{hydrologyX, hydrologyZ});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return regions;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Configuration parse(String[] arguments) throws IOException {
|
static Configuration parse(String[] arguments) throws IOException {
|
||||||
|
|||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import io.papermc.paper.registry.PaperRegistryAccess;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
final class PaperRegistryScope implements AutoCloseable {
|
||||||
|
private final Map<Object, Object> registries;
|
||||||
|
private final Map<Object, Object> previous;
|
||||||
|
|
||||||
|
private PaperRegistryScope(Map<Object, Object> registries) {
|
||||||
|
this.registries = registries;
|
||||||
|
this.previous = new HashMap<>(registries);
|
||||||
|
registries.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
static PaperRegistryScope open() {
|
||||||
|
return new PaperRegistryScope(registries());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
registries.clear();
|
||||||
|
registries.putAll(previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static Map<Object, Object> registries() {
|
||||||
|
try {
|
||||||
|
Field field = PaperRegistryAccess.class.getDeclaredField("registries");
|
||||||
|
field.setAccessible(true);
|
||||||
|
return (Map<Object, Object>) field.get(PaperRegistryAccess.instance());
|
||||||
|
} catch (ReflectiveOperationException failure) {
|
||||||
|
throw new IllegalStateException("Paper registry access has no registries map to isolate", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+301
@@ -0,0 +1,301 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.QuartPos;
|
||||||
|
import net.minecraft.core.RegistryAccess;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.util.datafix.DataFixers;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
|
import net.minecraft.world.level.StructureManager;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.biome.BiomeManager;
|
||||||
|
import net.minecraft.world.level.biome.BiomeResolver;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.PalettedContainerFactory;
|
||||||
|
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||||
|
import net.minecraft.world.level.chunk.UpgradeData;
|
||||||
|
import net.minecraft.world.level.dimension.DimensionType;
|
||||||
|
import net.minecraft.world.level.dimension.LevelStem;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseChunk;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.RandomState;
|
||||||
|
import net.minecraft.world.level.levelgen.WorldGenerationContext;
|
||||||
|
import net.minecraft.world.level.levelgen.blending.Blender;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.DensityFunction;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.DensityVolume;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.ScopedDensityBuffer;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||||
|
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.SortedSet;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public final class WorldgenExportSampler implements AutoCloseable {
|
||||||
|
private static final String DENSITY_FUNCTIONS = "/worldgen/density_function/";
|
||||||
|
private static final int CHUNK_WIDTH = 16;
|
||||||
|
private static final Method CREATE_NOISE_CHUNK = generatorMethod("createNoiseChunk", ChunkAccess.class, StructureManager.class, Blender.class,
|
||||||
|
RandomState.class, NoiseSettings.class);
|
||||||
|
private static final Method DO_FILL = generatorMethod("doFill", NoiseChunk.class, ChunkAccess.class);
|
||||||
|
|
||||||
|
private final HeadlessNativeRegistries loaded;
|
||||||
|
private final NoiseBasedChunkGenerator generator;
|
||||||
|
private final RandomState randomState;
|
||||||
|
private final ThreadLocal<BiomeResolver> resolvers;
|
||||||
|
private final LevelHeightAccessor heights;
|
||||||
|
private final SortedSet<String> densityFunctionIds;
|
||||||
|
private final ThreadLocal<SamplerContext> densityContexts = ThreadLocal.withInitial(() -> SamplerContext.builder().enableCaches().build());
|
||||||
|
private final PalettedContainerFactory containers;
|
||||||
|
private final long seed;
|
||||||
|
private final long biomeZoomSeed;
|
||||||
|
|
||||||
|
private WorldgenExportSampler(HeadlessNativeRegistries loaded, LevelStem stem, long seed, SortedSet<String> densityFunctionIds) {
|
||||||
|
this.loaded = loaded;
|
||||||
|
if (!(stem.generator() instanceof NoiseBasedChunkGenerator noiseGenerator)) {
|
||||||
|
throw new IllegalStateException("Exported dimension uses " + stem.generator().getClass().getSimpleName() + ", not a noise generator");
|
||||||
|
}
|
||||||
|
this.generator = noiseGenerator;
|
||||||
|
this.randomState = RandomState.create(loaded.registries().lookupOrThrow(Registries.NOISE), seed, noiseGenerator.generatorSettings().value());
|
||||||
|
this.resolvers = ThreadLocal.withInitial(() -> noiseGenerator.getBiomeSource().createCachingResolver(randomState));
|
||||||
|
DimensionType type = stem.type().value();
|
||||||
|
this.heights = LevelHeightAccessor.create(type.minY(), type.height());
|
||||||
|
this.densityFunctionIds = densityFunctionIds;
|
||||||
|
this.containers = PalettedContainerFactory.create(loaded.registries());
|
||||||
|
this.seed = seed;
|
||||||
|
this.biomeZoomSeed = BiomeManager.obfuscateSeed(seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WorldgenExportSampler open(Map<String, byte[]> files, WorldgenExportMode mode, String dimensionId, long seed) throws IOException {
|
||||||
|
Objects.requireNonNull(mode);
|
||||||
|
Identifier dimension = Identifier.parse(dimensionId);
|
||||||
|
HeadlessNativeBootstrap.initialize();
|
||||||
|
Path directory = Files.createTempDirectory("iris-worldgen-export-");
|
||||||
|
HeadlessNativeRegistries loaded;
|
||||||
|
try {
|
||||||
|
for (Map.Entry<String, byte[]> file : files.entrySet()) {
|
||||||
|
Path target = directory.resolve(file.getKey()).normalize();
|
||||||
|
if (!target.startsWith(directory)) {
|
||||||
|
throw new IllegalArgumentException("Export file escapes the datapack root: " + file.getKey());
|
||||||
|
}
|
||||||
|
Files.createDirectories(target.getParent());
|
||||||
|
Files.write(target, file.getValue());
|
||||||
|
}
|
||||||
|
try (PaperRegistryScope scope = PaperRegistryScope.open()) {
|
||||||
|
loaded = HeadlessNativeRegistries.load(directory);
|
||||||
|
}
|
||||||
|
} catch (IOException | RuntimeException failure) {
|
||||||
|
HeadlessNativeRegistries.delete(directory);
|
||||||
|
throw failure;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new WorldgenExportSampler(loaded, stem(loaded.registries(), mode, dimension), seed, densityFunctionIds(files));
|
||||||
|
} catch (RuntimeException failure) {
|
||||||
|
loaded.close();
|
||||||
|
throw failure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WorldgenExportSampler open(Path datapack, WorldgenExportMode mode, String dimensionId, long seed) throws IOException {
|
||||||
|
return open(files(datapack), mode, dimensionId, seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float density(String functionId, int x, int y, int z) {
|
||||||
|
DensityFunction function = loaded.registries().lookupOrThrow(Registries.DENSITY_FUNCTION)
|
||||||
|
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, Identifier.parse(functionId))).value();
|
||||||
|
return randomState.getSampler(function).sampleValue(densityContexts.get(), x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float[] densityChunk(DensityFunction function, int chunkX, int chunkZ) {
|
||||||
|
DensityVolume volume = new DensityVolume(CHUNK_WIDTH, heights.getHeight(), CHUNK_WIDTH, chunkX * CHUNK_WIDTH, heights.getMinY(), chunkZ * CHUNK_WIDTH);
|
||||||
|
return sampleVolume(function, volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DensityFunction finalDensity() {
|
||||||
|
return generator.generatorSettings().value().noiseRouter().finalDensity();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int baseHeight(int x, int z) {
|
||||||
|
return generator.getBaseHeight(x, z, Heightmap.Types.WORLD_SURFACE_WG, heights, randomState);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int floorHeight(int x, int z) {
|
||||||
|
return generator.getBaseHeight(x, z, Heightmap.Types.OCEAN_FLOOR_WG, heights, randomState);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProtoChunk terrainChunk(int chunkX, int chunkZ) {
|
||||||
|
ProtoChunk chunk = new ProtoChunk(new ChunkPos(chunkX, chunkZ), UpgradeData.EMPTY, heights, containers, null);
|
||||||
|
NoiseGeneratorSettings settings = generator.generatorSettings().value();
|
||||||
|
NoiseSettings noise = settings.noiseSettings().clampToHeightAccessor(chunk.getHeightAccessorForGeneration());
|
||||||
|
try (NoiseChunk noiseChunk = (NoiseChunk) invoke(CREATE_NOISE_CHUNK, chunk, NoStructures.INSTANCE, Blender.empty(), randomState, noise)) {
|
||||||
|
invoke(DO_FILL, noiseChunk, chunk);
|
||||||
|
randomState.surfaceSystem().buildSurface(randomState, new BiomeManager(resolvers.get(), biomeZoomSeed),
|
||||||
|
new WorldGenerationContext(generator, chunk.getHeightAccessorForGeneration()), chunk, noiseChunk, settings.materialRule().value(), null);
|
||||||
|
}
|
||||||
|
return chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StructureStart structureStart(String structureId, int chunkX, int chunkZ) throws IOException {
|
||||||
|
Holder.Reference<Structure> structure = loaded.registries().lookupOrThrow(Registries.STRUCTURE)
|
||||||
|
.getOrThrow(ResourceKey.create(Registries.STRUCTURE, Identifier.parse(structureId)));
|
||||||
|
Path storageRoot = Files.createTempDirectory("iris-worldgen-export-structures-");
|
||||||
|
try (LevelStorageSource.LevelStorageAccess storage = LevelStorageSource.createDefault(storageRoot).createAccess("export")) {
|
||||||
|
StructureTemplateManager templates = new StructureTemplateManager(loaded.resources(), storage, DataFixers.getDataFixer(),
|
||||||
|
loaded.registries().lookupOrThrow(Registries.BLOCK));
|
||||||
|
return structure.value().generate(structure, Level.OVERWORLD, loaded.registries(), generator, generator.getBiomeSource(),
|
||||||
|
randomState.createClimateSampler(SamplerContext.builder().enableCaches().build()), randomState, templates, seed,
|
||||||
|
new ChunkPos(chunkX, chunkZ), 0, heights, structure.value().biomes()::contains);
|
||||||
|
} finally {
|
||||||
|
HeadlessNativeRegistries.delete(storageRoot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String biome(int x, int y, int z) {
|
||||||
|
Holder<Biome> biome = resolvers.get().getNoiseBiome(QuartPos.fromBlock(x), QuartPos.fromBlock(y), QuartPos.fromBlock(z));
|
||||||
|
return biome.unwrapKey().orElseThrow().identifier().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> possibleBiomes() {
|
||||||
|
Set<String> biomes = new TreeSet<>();
|
||||||
|
for (Holder<Biome> biome : generator.getBiomeSource().possibleBiomes()) {
|
||||||
|
biomes.add(biome.unwrapKey().orElseThrow().identifier().toString());
|
||||||
|
}
|
||||||
|
return biomes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SortedSet<String> densityFunctionIds() {
|
||||||
|
return densityFunctionIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int minY() {
|
||||||
|
return heights.getMinY();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int height() {
|
||||||
|
return heights.getHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int seaLevel() {
|
||||||
|
return generator.getSeaLevel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public RegistryAccess registries() {
|
||||||
|
return loaded.registries();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
loaded.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private float[] sampleVolume(DensityFunction function, DensityVolume volume) {
|
||||||
|
try (ScopedDensityBuffer buffer = randomState.getSampler(function).bind(densityContexts.get()).sampleVolume(volume)) {
|
||||||
|
float[] values = new float[volume.size()];
|
||||||
|
for (int index = 0; index < values.length; index++) {
|
||||||
|
values[index] = buffer.get(index);
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object invoke(Method method, Object... arguments) {
|
||||||
|
try {
|
||||||
|
return method.invoke(generator, arguments);
|
||||||
|
} catch (InvocationTargetException failure) {
|
||||||
|
if (failure.getCause() instanceof RuntimeException runtime) {
|
||||||
|
throw runtime;
|
||||||
|
}
|
||||||
|
if (failure.getCause() instanceof Error error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Terrain stage " + method.getName() + " failed", failure.getCause());
|
||||||
|
} catch (IllegalAccessException failure) {
|
||||||
|
throw new IllegalStateException("Terrain stage " + method.getName() + " is not accessible", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Method generatorMethod(String name, Class<?>... parameters) {
|
||||||
|
try {
|
||||||
|
Method method = NoiseBasedChunkGenerator.class.getDeclaredMethod(name, parameters);
|
||||||
|
method.setAccessible(true);
|
||||||
|
return method;
|
||||||
|
} catch (NoSuchMethodException failure) {
|
||||||
|
throw new IllegalStateException("NoiseBasedChunkGenerator has no terrain stage " + name, failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LevelStem stem(RegistryAccess registries, WorldgenExportMode mode, Identifier dimension) {
|
||||||
|
return switch (mode) {
|
||||||
|
case PRESET -> registries.lookupOrThrow(Registries.WORLD_PRESET)
|
||||||
|
.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, dimension)).value().overworld()
|
||||||
|
.orElseThrow(() -> new IllegalStateException("World preset " + dimension + " has no overworld"));
|
||||||
|
case DIMENSION -> levelStem(registries, dimension);
|
||||||
|
case OVERWORLD -> levelStem(registries, LevelStem.OVERWORLD.identifier());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LevelStem levelStem(RegistryAccess registries, Identifier id) {
|
||||||
|
return registries.lookupOrThrow(Registries.LEVEL_STEM).getOrThrow(ResourceKey.create(Registries.LEVEL_STEM, id)).value();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SortedMap<String, byte[]> files(Path datapack) throws IOException {
|
||||||
|
if (!Files.isDirectory(datapack)) {
|
||||||
|
throw new IOException("Exported datapack " + datapack + " is not a directory");
|
||||||
|
}
|
||||||
|
SortedMap<String, byte[]> files = new TreeMap<>();
|
||||||
|
try (Stream<Path> paths = Files.walk(datapack)) {
|
||||||
|
List<Path> regular = paths.filter(Files::isRegularFile).toList();
|
||||||
|
for (Path file : regular) {
|
||||||
|
files.put(datapack.relativize(file).toString().replace('\\', '/'), Files.readAllBytes(file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SortedSet<String> densityFunctionIds(Map<String, byte[]> files) {
|
||||||
|
SortedSet<String> ids = new TreeSet<>();
|
||||||
|
for (String path : files.keySet()) {
|
||||||
|
int marker = path.indexOf(DENSITY_FUNCTIONS);
|
||||||
|
if (path.startsWith("data/") && marker > 0 && path.endsWith(".json")) {
|
||||||
|
String namespace = path.substring("data/".length(), marker);
|
||||||
|
ids.add(namespace + ":" + path.substring(marker + DENSITY_FUNCTIONS.length(), path.length() - ".json".length()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class NoStructures extends StructureManager {
|
||||||
|
private static final NoStructures INSTANCE = new NoStructures();
|
||||||
|
|
||||||
|
private NoStructures() {
|
||||||
|
super(null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<StructureStart> startsForStructure(int chunkX, int chunkZ, Predicate<Structure> predicate) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+394
@@ -0,0 +1,394 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import io.papermc.paper.configuration.WorldConfiguration;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.QuartPos;
|
||||||
|
import net.minecraft.core.RegistryAccess;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.biome.BiomeManager;
|
||||||
|
import net.minecraft.world.level.biome.BiomeResolver;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.PalettedContainerFactory;
|
||||||
|
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||||
|
import net.minecraft.world.level.chunk.UpgradeData;
|
||||||
|
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||||
|
import net.minecraft.world.level.dimension.DimensionType;
|
||||||
|
import net.minecraft.world.level.dimension.LevelStem;
|
||||||
|
import net.minecraft.world.level.levelgen.Aquifer;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseChunk;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.RandomState;
|
||||||
|
import net.minecraft.world.level.levelgen.WorldGenerationContext;
|
||||||
|
import net.minecraft.world.level.levelgen.blending.Blender;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.DensityVolume;
|
||||||
|
import net.minecraft.world.level.levelgen.material.rule.MaterialRule;
|
||||||
|
import org.spigotmc.SpigotWorldConfig;
|
||||||
|
|
||||||
|
import java.lang.management.ManagementFactory;
|
||||||
|
import java.lang.management.ThreadMXBean;
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.SplittableRandom;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
final class WorldgenStageBench {
|
||||||
|
private static final int SAMPLE_RADIUS_BLOCKS = 32_768;
|
||||||
|
private static final double NANOS_PER_MILLI = 1_000_000.0;
|
||||||
|
private static final double NANOS_PER_MICRO = 1_000.0;
|
||||||
|
private static final ThreadMXBean THREADS = ManagementFactory.getThreadMXBean();
|
||||||
|
|
||||||
|
private final String label;
|
||||||
|
private final NoiseBasedChunkGenerator generator;
|
||||||
|
private final NoiseGeneratorSettings settings;
|
||||||
|
private final RandomState randomState;
|
||||||
|
private final LevelHeightAccessor heights;
|
||||||
|
private final PalettedContainerFactory containers;
|
||||||
|
private final long seed;
|
||||||
|
private final Method fill;
|
||||||
|
private final Aquifer.FluidPicker fluidPicker;
|
||||||
|
private final BiomeResolver uncached;
|
||||||
|
private final Level surfaceLevel;
|
||||||
|
|
||||||
|
WorldgenStageBench(Options options) {
|
||||||
|
label = options.label();
|
||||||
|
if (!(options.stem().generator() instanceof NoiseBasedChunkGenerator noiseGenerator)) {
|
||||||
|
throw new IllegalStateException(label + " uses " + options.stem().generator().getClass().getSimpleName() + ", not a noise generator");
|
||||||
|
}
|
||||||
|
generator = noiseGenerator;
|
||||||
|
settings = noiseGenerator.generatorSettings().value();
|
||||||
|
seed = options.seed();
|
||||||
|
randomState = RandomState.create(options.registries().lookupOrThrow(Registries.NOISE), seed, settings);
|
||||||
|
DimensionType type = options.stem().type().value();
|
||||||
|
heights = LevelHeightAccessor.create(type.minY(), type.height());
|
||||||
|
containers = PalettedContainerFactory.create(options.registries());
|
||||||
|
fill = fillMethod();
|
||||||
|
fluidPicker = fluidPicker(noiseGenerator);
|
||||||
|
uncached = noiseGenerator.getBiomeSource().createUncachedResolver(randomState);
|
||||||
|
surfaceLevel = paperConfiguredLevel();
|
||||||
|
}
|
||||||
|
|
||||||
|
String label() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
NoiseGeneratorSettings settings() {
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
void chunks(int originChunkX, int originChunkZ, int side, ChunkSamples samples) {
|
||||||
|
int ring = side + 2;
|
||||||
|
Map<Long, ProtoChunk> chunks = new HashMap<>(ring * ring * 2);
|
||||||
|
for (int dz = -1; dz <= side; dz++) {
|
||||||
|
for (int dx = -1; dx <= side; dx++) {
|
||||||
|
ChunkPos position = new ChunkPos(originChunkX + dx, originChunkZ + dz);
|
||||||
|
ProtoChunk chunk = new ProtoChunk(position, UpgradeData.EMPTY, heights, containers, null);
|
||||||
|
long started = System.nanoTime();
|
||||||
|
long startedCpu = THREADS.getCurrentThreadCpuTime();
|
||||||
|
generator.createBiomes(randomState, Blender.empty(), null, chunk).join();
|
||||||
|
samples.biomes.add(System.nanoTime() - started, THREADS.getCurrentThreadCpuTime() - startedCpu);
|
||||||
|
chunk.setPersistedStatus(ChunkStatus.BIOMES);
|
||||||
|
chunks.put(ChunkPos.pack(position.x(), position.z()), chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RegionBiomes region = new RegionBiomes(chunks, uncached);
|
||||||
|
BiomeManager biomeManager = new BiomeManager(region, BiomeManager.obfuscateSeed(seed));
|
||||||
|
MaterialRule materialRule = settings.materialRule().value();
|
||||||
|
for (int dz = 0; dz < side; dz++) {
|
||||||
|
for (int dx = 0; dx < side; dx++) {
|
||||||
|
int chunkX = originChunkX + dx;
|
||||||
|
int chunkZ = originChunkZ + dz;
|
||||||
|
ProtoChunk chunk = chunks.get(ChunkPos.pack(chunkX, chunkZ));
|
||||||
|
Set<Holder<Biome>> possible = possibleBiomes(chunks, chunkX, chunkZ);
|
||||||
|
long started = System.nanoTime();
|
||||||
|
long startedCpu = THREADS.getCurrentThreadCpuTime();
|
||||||
|
try (NoiseChunk noiseChunk = new NoiseChunk(randomState, null, settings, fluidPicker, Blender.empty(), volume(chunk))) {
|
||||||
|
fill(noiseChunk, chunk);
|
||||||
|
long filled = System.nanoTime();
|
||||||
|
long filledCpu = THREADS.getCurrentThreadCpuTime();
|
||||||
|
randomState.surfaceSystem().buildSurface(randomState, biomeManager,
|
||||||
|
new WorldGenerationContext(generator, chunk.getHeightAccessorForGeneration(), surfaceLevel), chunk, noiseChunk, materialRule, possible);
|
||||||
|
samples.surface.add(System.nanoTime() - filled, THREADS.getCurrentThreadCpuTime() - filledCpu);
|
||||||
|
samples.noise.add(filled - started, filledCpu - startedCpu);
|
||||||
|
}
|
||||||
|
ChunkPos position = chunk.getPos();
|
||||||
|
long carverStarted = System.nanoTime();
|
||||||
|
long carverStartedCpu = THREADS.getCurrentThreadCpuTime();
|
||||||
|
uncached.getNoiseBiome(QuartPos.fromBlock(position.getMinBlockX()), 0, QuartPos.fromBlock(position.getMinBlockZ()));
|
||||||
|
samples.carverBiome.add(System.nanoTime() - carverStarted, THREADS.getCurrentThreadCpuTime() - carverStartedCpu);
|
||||||
|
samples.chunks++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
samples.biomeFallbacks += region.fallbacks();
|
||||||
|
}
|
||||||
|
|
||||||
|
void uncachedBiome(CallBudget budget, CallSamples samples) {
|
||||||
|
SplittableRandom random = new SplittableRandom(seed ^ 0x5DEECE66DL ^ samples.count);
|
||||||
|
int minQuart = QuartPos.fromBlock(heights.getMinY());
|
||||||
|
int quartHeight = QuartPos.fromBlock(heights.getHeight());
|
||||||
|
long deadline = System.nanoTime() + budget.limitNanos();
|
||||||
|
long startedCpu = THREADS.getCurrentThreadCpuTime();
|
||||||
|
int taken = 0;
|
||||||
|
while (taken < budget.calls() && (taken < budget.minimumCalls() || System.nanoTime() < deadline)) {
|
||||||
|
int quartX = QuartPos.fromBlock(random.nextInt(-SAMPLE_RADIUS_BLOCKS, SAMPLE_RADIUS_BLOCKS));
|
||||||
|
int quartY = minQuart + random.nextInt(quartHeight);
|
||||||
|
int quartZ = QuartPos.fromBlock(random.nextInt(-SAMPLE_RADIUS_BLOCKS, SAMPLE_RADIUS_BLOCKS));
|
||||||
|
long started = System.nanoTime();
|
||||||
|
uncached.getNoiseBiome(quartX, quartY, quartZ);
|
||||||
|
samples.wall.add(System.nanoTime() - started);
|
||||||
|
taken++;
|
||||||
|
}
|
||||||
|
samples.finish(taken, THREADS.getCurrentThreadCpuTime() - startedCpu);
|
||||||
|
}
|
||||||
|
|
||||||
|
void baseHeight(CallBudget budget, Heightmap.Types type, CallSamples samples) {
|
||||||
|
SplittableRandom random = new SplittableRandom(seed ^ type.ordinal() ^ 0x2545F4914F6CDD1DL ^ samples.count);
|
||||||
|
long deadline = System.nanoTime() + budget.limitNanos();
|
||||||
|
long startedCpu = THREADS.getCurrentThreadCpuTime();
|
||||||
|
int taken = 0;
|
||||||
|
while (taken < budget.calls() && (taken < budget.minimumCalls() || System.nanoTime() < deadline)) {
|
||||||
|
int x = random.nextInt(-SAMPLE_RADIUS_BLOCKS, SAMPLE_RADIUS_BLOCKS);
|
||||||
|
int z = random.nextInt(-SAMPLE_RADIUS_BLOCKS, SAMPLE_RADIUS_BLOCKS);
|
||||||
|
long started = System.nanoTime();
|
||||||
|
generator.getBaseHeight(x, z, type, heights, randomState);
|
||||||
|
samples.wall.add(System.nanoTime() - started);
|
||||||
|
taken++;
|
||||||
|
}
|
||||||
|
samples.finish(taken, THREADS.getCurrentThreadCpuTime() - startedCpu);
|
||||||
|
}
|
||||||
|
|
||||||
|
void baseColumn(CallBudget budget, CallSamples samples) {
|
||||||
|
SplittableRandom random = new SplittableRandom(seed ^ 0x9E3779B97F4A7C15L ^ samples.count);
|
||||||
|
long deadline = System.nanoTime() + budget.limitNanos();
|
||||||
|
long startedCpu = THREADS.getCurrentThreadCpuTime();
|
||||||
|
int taken = 0;
|
||||||
|
while (taken < budget.calls() && (taken < budget.minimumCalls() || System.nanoTime() < deadline)) {
|
||||||
|
int x = random.nextInt(-SAMPLE_RADIUS_BLOCKS, SAMPLE_RADIUS_BLOCKS);
|
||||||
|
int z = random.nextInt(-SAMPLE_RADIUS_BLOCKS, SAMPLE_RADIUS_BLOCKS);
|
||||||
|
long started = System.nanoTime();
|
||||||
|
generator.getBaseColumn(x, z, heights, randomState);
|
||||||
|
samples.wall.add(System.nanoTime() - started);
|
||||||
|
taken++;
|
||||||
|
}
|
||||||
|
samples.finish(taken, THREADS.getCurrentThreadCpuTime() - startedCpu);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fill(NoiseChunk noiseChunk, ChunkAccess chunk) {
|
||||||
|
try {
|
||||||
|
fill.invoke(generator, noiseChunk, chunk);
|
||||||
|
} catch (IllegalAccessException failure) {
|
||||||
|
throw new IllegalStateException("Cannot invoke NoiseBasedChunkGenerator.doFill for " + label, failure);
|
||||||
|
} catch (InvocationTargetException failure) {
|
||||||
|
if (failure.getCause() instanceof RuntimeException runtime) {
|
||||||
|
throw runtime;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Noise fill failed for " + label, failure.getCause());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DensityVolume volume(ChunkAccess chunk) {
|
||||||
|
NoiseSettings noise = settings.noiseSettings().clampToHeightAccessor(chunk.getHeightAccessorForGeneration());
|
||||||
|
ChunkPos position = chunk.getPos();
|
||||||
|
return new DensityVolume(16, noise.height(), 16, position.getMinBlockX(), noise.minY(), position.getMinBlockZ());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<Holder<Biome>> possibleBiomes(Map<Long, ProtoChunk> chunks, int chunkX, int chunkZ) {
|
||||||
|
Set<Holder<Biome>> possible = new HashSet<>();
|
||||||
|
for (int dz = -1; dz <= 1; dz++) {
|
||||||
|
for (int dx = -1; dx <= 1; dx++) {
|
||||||
|
chunks.get(ChunkPos.pack(chunkX + dx, chunkZ + dz)).collectBiomesInPalette(possible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return possible;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Method fillMethod() {
|
||||||
|
try {
|
||||||
|
Method method = NoiseBasedChunkGenerator.class.getDeclaredMethod("doFill", NoiseChunk.class, ChunkAccess.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
return method;
|
||||||
|
} catch (NoSuchMethodException failure) {
|
||||||
|
throw new IllegalStateException("NoiseBasedChunkGenerator has no doFill(NoiseChunk, ChunkAccess)", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static Aquifer.FluidPicker fluidPicker(NoiseBasedChunkGenerator generator) {
|
||||||
|
try {
|
||||||
|
Field field = NoiseBasedChunkGenerator.class.getDeclaredField("globalFluidPicker");
|
||||||
|
field.setAccessible(true);
|
||||||
|
return ((Supplier<Aquifer.FluidPicker>) field.get(generator)).get();
|
||||||
|
} catch (ReflectiveOperationException failure) {
|
||||||
|
throw new IllegalStateException("NoiseBasedChunkGenerator has no globalFluidPicker supplier", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Level paperConfiguredLevel() {
|
||||||
|
try {
|
||||||
|
Constructor<WorldConfiguration> constructor = WorldConfiguration.class.getDeclaredConstructor(SpigotWorldConfig.class, Identifier.class);
|
||||||
|
constructor.setAccessible(true);
|
||||||
|
WorldConfiguration configuration = constructor.newInstance(null, Identifier.withDefaultNamespace("overworld"));
|
||||||
|
configuration.environment = configuration.new Environment();
|
||||||
|
Class<?> unsafeType = Class.forName("sun.misc.Unsafe");
|
||||||
|
Field unsafeField = unsafeType.getDeclaredField("theUnsafe");
|
||||||
|
unsafeField.setAccessible(true);
|
||||||
|
Method allocate = unsafeType.getMethod("allocateInstance", Class.class);
|
||||||
|
Level level = (Level) allocate.invoke(unsafeField.get(null), ServerLevel.class);
|
||||||
|
Field paperConfig = Level.class.getDeclaredField("paperConfig");
|
||||||
|
paperConfig.setAccessible(true);
|
||||||
|
paperConfig.set(level, configuration);
|
||||||
|
return level;
|
||||||
|
} catch (ReflectiveOperationException failure) {
|
||||||
|
throw new IllegalStateException("Cannot build the Paper world configuration that surface rules read", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record Options(String label, LevelStem stem, RegistryAccess registries, long seed) {
|
||||||
|
Options {
|
||||||
|
Objects.requireNonNull(label, "label");
|
||||||
|
Objects.requireNonNull(stem, "stem");
|
||||||
|
Objects.requireNonNull(registries, "registries");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record CallBudget(int calls, int minimumCalls, long limitNanos) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record ChunkTimings(int chunks, StageTiming biomes, StageTiming noise, StageTiming surface, StageTiming carverBiome,
|
||||||
|
long biomeFallbacks) {
|
||||||
|
double totalMean() {
|
||||||
|
return biomes.wall().mean() + noise.wall().mean() + surface.wall().mean() + carverBiome.wall().mean();
|
||||||
|
}
|
||||||
|
|
||||||
|
double totalCpuMean() {
|
||||||
|
return biomes.cpu().mean() + noise.cpu().mean() + surface.cpu().mean() + carverBiome.cpu().mean();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record StageTiming(Summary wall, Summary cpu) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record CallTiming(Summary wall, int calls, double cpuMean) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record Summary(int count, double mean, double median, double p90, double min, double max) {
|
||||||
|
private static Summary of(long[] nanos, double unit) {
|
||||||
|
if (nanos.length == 0) {
|
||||||
|
return new Summary(0, Double.NaN, Double.NaN, Double.NaN, Double.NaN, Double.NaN);
|
||||||
|
}
|
||||||
|
long[] sorted = nanos.clone();
|
||||||
|
Arrays.sort(sorted);
|
||||||
|
double total = 0.0;
|
||||||
|
for (long value : sorted) {
|
||||||
|
total += value;
|
||||||
|
}
|
||||||
|
return new Summary(sorted.length, total / sorted.length / unit, percentile(sorted, 0.5) / unit,
|
||||||
|
percentile(sorted, 0.9) / unit, sorted[0] / unit, sorted[sorted.length - 1] / unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double percentile(long[] sorted, double fraction) {
|
||||||
|
int index = (int) Math.min(sorted.length - 1, Math.round(fraction * (sorted.length - 1)));
|
||||||
|
return sorted[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class ChunkSamples {
|
||||||
|
private final StageSamples biomes = new StageSamples();
|
||||||
|
private final StageSamples noise = new StageSamples();
|
||||||
|
private final StageSamples surface = new StageSamples();
|
||||||
|
private final StageSamples carverBiome = new StageSamples();
|
||||||
|
private int chunks;
|
||||||
|
private long biomeFallbacks;
|
||||||
|
|
||||||
|
ChunkTimings timings() {
|
||||||
|
return new ChunkTimings(chunks, biomes.timing(), noise.timing(), surface.timing(), carverBiome.timing(), biomeFallbacks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class CallSamples {
|
||||||
|
private final Samples wall = new Samples();
|
||||||
|
private int count;
|
||||||
|
private long cpuNanos;
|
||||||
|
|
||||||
|
CallTiming timing() {
|
||||||
|
return new CallTiming(Summary.of(wall.values(), NANOS_PER_MICRO), count,
|
||||||
|
count == 0 ? Double.NaN : cpuNanos / (double) count / NANOS_PER_MICRO);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finish(int calls, long cpu) {
|
||||||
|
count += calls;
|
||||||
|
cpuNanos += cpu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class StageSamples {
|
||||||
|
private final Samples wall = new Samples();
|
||||||
|
private final Samples cpu = new Samples();
|
||||||
|
|
||||||
|
private void add(long wallNanos, long cpuNanos) {
|
||||||
|
wall.add(wallNanos);
|
||||||
|
cpu.add(cpuNanos);
|
||||||
|
}
|
||||||
|
|
||||||
|
private StageTiming timing() {
|
||||||
|
return new StageTiming(Summary.of(wall.values(), NANOS_PER_MILLI), Summary.of(cpu.values(), NANOS_PER_MILLI));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Samples {
|
||||||
|
private long[] values = new long[64];
|
||||||
|
private int size;
|
||||||
|
|
||||||
|
private void add(long value) {
|
||||||
|
if (size == values.length) {
|
||||||
|
values = Arrays.copyOf(values, size * 2);
|
||||||
|
}
|
||||||
|
values[size++] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long[] values() {
|
||||||
|
return Arrays.copyOf(values, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RegionBiomes implements BiomeResolver {
|
||||||
|
private final Map<Long, ProtoChunk> chunks;
|
||||||
|
private final BiomeResolver fallback;
|
||||||
|
private long fallbacks;
|
||||||
|
|
||||||
|
private RegionBiomes(Map<Long, ProtoChunk> chunks, BiomeResolver fallback) {
|
||||||
|
this.chunks = chunks;
|
||||||
|
this.fallback = fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Holder<Biome> getNoiseBiome(int quartX, int quartY, int quartZ) {
|
||||||
|
ProtoChunk chunk = chunks.get(ChunkPos.pack(QuartPos.toSection(quartX), QuartPos.toSection(quartZ)));
|
||||||
|
if (chunk == null) {
|
||||||
|
fallbacks++;
|
||||||
|
return fallback.getNoiseBiome(quartX, quartY, quartZ);
|
||||||
|
}
|
||||||
|
return chunk.getNoiseBiome(quartX, quartY, quartZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long fallbacks() {
|
||||||
|
return fallbacks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
package art.arcane.iris.probe.fidelity;
|
||||||
|
|
||||||
|
import art.arcane.iris.probe.WorldgenExportSampler;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class VanillaNoiseColumnSource implements ColumnSource {
|
||||||
|
private final WorldgenExportSampler sampler;
|
||||||
|
private final Map<String, String> loadKeyByBiomeId;
|
||||||
|
private final int minY;
|
||||||
|
private final int maxY;
|
||||||
|
private final int seaLevel;
|
||||||
|
|
||||||
|
public VanillaNoiseColumnSource(WorldgenExportSampler sampler, Map<String, String> loadKeyByBiomeId) {
|
||||||
|
this.sampler = Objects.requireNonNull(sampler, "sampler");
|
||||||
|
this.loadKeyByBiomeId = Map.copyOf(loadKeyByBiomeId);
|
||||||
|
this.minY = sampler.minY();
|
||||||
|
this.maxY = sampler.minY() + sampler.height();
|
||||||
|
this.seaLevel = sampler.seaLevel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String label() {
|
||||||
|
return "vanilla:26.3";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int minY() {
|
||||||
|
return minY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int maxY() {
|
||||||
|
return maxY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FidelityColumn column(int x, int z) {
|
||||||
|
int floor = sampler.floorHeight(x, z);
|
||||||
|
int terrainY = floor - 1;
|
||||||
|
boolean wet = floor < seaLevel && sampler.baseHeight(x, z) > floor;
|
||||||
|
return new FidelityColumn(loadKey(x, Math.max(minY, terrainY), z), terrainY, wet);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
sampler.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String loadKey(int x, int y, int z) {
|
||||||
|
String biomeId = sampler.biome(x, y, z);
|
||||||
|
String loadKey = loadKeyByBiomeId.get(biomeId);
|
||||||
|
if (loadKey == null) {
|
||||||
|
throw new IllegalStateException("Exported biome " + biomeId + " at " + x + "," + y + "," + z
|
||||||
|
+ " has no Iris load key in the export result");
|
||||||
|
}
|
||||||
|
return loadKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
+204
@@ -0,0 +1,204 @@
|
|||||||
|
package art.arcane.iris.probe.fidelity;
|
||||||
|
|
||||||
|
import art.arcane.iris.probe.WorldgenExportSampler;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
|
import net.minecraft.world.level.block.Block;
|
||||||
|
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||||
|
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.DensityFunction;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.DensityFunctions;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.op.BinaryFunction;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class VanillaNoiseFidelitySource implements FidelitySource {
|
||||||
|
private static final int CHUNK_WIDTH = 16;
|
||||||
|
private static final byte SOLID = 0;
|
||||||
|
private static final byte OPEN = 1;
|
||||||
|
private static final byte FLUID = 2;
|
||||||
|
private static final byte LAVA = 3;
|
||||||
|
|
||||||
|
private final WorldgenExportSampler sampler;
|
||||||
|
private final Map<String, String> loadKeyByBiomeId;
|
||||||
|
private final String wallShell;
|
||||||
|
private final DensityFunction massDensity;
|
||||||
|
private final boolean walls;
|
||||||
|
private final int minY;
|
||||||
|
private final int maxY;
|
||||||
|
|
||||||
|
public VanillaNoiseFidelitySource(WorldgenExportSampler sampler, Map<String, String> loadKeyByBiomeId, String wallShell) {
|
||||||
|
this.sampler = Objects.requireNonNull(sampler, "sampler");
|
||||||
|
this.loadKeyByBiomeId = Map.copyOf(loadKeyByBiomeId);
|
||||||
|
this.wallShell = Objects.requireNonNull(wallShell, "wallShell");
|
||||||
|
this.massDensity = terrainMass(sampler.finalDensity());
|
||||||
|
this.walls = sampler.densityFunctionIds().contains(wallShell);
|
||||||
|
this.minY = sampler.minY();
|
||||||
|
this.maxY = sampler.minY() + sampler.height();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String label() {
|
||||||
|
return "vanilla:26.3";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int minY() {
|
||||||
|
return minY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int maxY() {
|
||||||
|
return maxY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FidelityChunk sample(int chunkX, int chunkZ) {
|
||||||
|
return new TerrainChunk(sampler.terrainChunk(chunkX, chunkZ), chunkX * CHUNK_WIDTH, chunkZ * CHUNK_WIDTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
sampler.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String loadKey(int x, int y, int z) {
|
||||||
|
String biomeId = sampler.biome(x, y, z);
|
||||||
|
String loadKey = loadKeyByBiomeId.get(biomeId);
|
||||||
|
if (loadKey == null) {
|
||||||
|
throw new IllegalStateException("Exported biome " + biomeId + " at " + x + "," + y + "," + z
|
||||||
|
+ " has no Iris load key in the export result");
|
||||||
|
}
|
||||||
|
return loadKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
static DensityFunction terrainMass(DensityFunction finalDensity) {
|
||||||
|
DensityFunction root = finalDensity;
|
||||||
|
while (root instanceof DensityFunctions.HolderHolder(Holder<DensityFunction> holder)) {
|
||||||
|
root = holder.value();
|
||||||
|
}
|
||||||
|
return root instanceof BinaryFunction(BinaryFunction.Type type, DensityFunction terrain, DensityFunction ignored) && type == BinaryFunction.Type.MIN
|
||||||
|
? terrain : root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte classify(Block block) {
|
||||||
|
String id = BuiltInRegistries.BLOCK.getKey(block).toString();
|
||||||
|
if (FidelityBlocks.open(id)) {
|
||||||
|
return OPEN;
|
||||||
|
}
|
||||||
|
if (FidelityBlocks.lava(id)) {
|
||||||
|
return LAVA;
|
||||||
|
}
|
||||||
|
return FidelityBlocks.fluid(id) ? FLUID : SOLID;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Column(int surfaceY, String biome, byte[] kinds) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class TerrainChunk implements FidelityChunk {
|
||||||
|
private final ProtoChunk terrain;
|
||||||
|
private final int blockX;
|
||||||
|
private final int blockZ;
|
||||||
|
private final Column[] columns = new Column[CHUNK_WIDTH * CHUNK_WIDTH];
|
||||||
|
private final Map<Block, Byte> kinds = new IdentityHashMap<>();
|
||||||
|
private float[] masses;
|
||||||
|
|
||||||
|
private TerrainChunk(ProtoChunk terrain, int blockX, int blockZ) {
|
||||||
|
this.terrain = terrain;
|
||||||
|
this.blockX = blockX;
|
||||||
|
this.blockZ = blockZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int surfaceY(int localX, int localZ) {
|
||||||
|
return column(localX, localZ).surfaceY();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String surfaceBiome(int localX, int localZ) {
|
||||||
|
return column(localX, localZ).biome();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean open(int localX, int y, int localZ) {
|
||||||
|
return kind(localX, y, localZ) == OPEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean fluid(int localX, int y, int localZ) {
|
||||||
|
byte kind = kind(localX, y, localZ);
|
||||||
|
return kind == FLUID || kind == LAVA;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean lava(int localX, int y, int localZ) {
|
||||||
|
return kind(localX, y, localZ) == LAVA;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String block(int localX, int y, int localZ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean terrainMass(int localX, int y, int localZ) {
|
||||||
|
if (y < minY || y >= maxY) {
|
||||||
|
throw new IllegalArgumentException("Y " + y + " is outside [" + minY + ", " + maxY + ")");
|
||||||
|
}
|
||||||
|
if (localX < 0 || localX >= CHUNK_WIDTH || localZ < 0 || localZ >= CHUNK_WIDTH) {
|
||||||
|
throw new IllegalArgumentException("Column " + localX + "," + localZ + " is outside the chunk");
|
||||||
|
}
|
||||||
|
if (masses == null) {
|
||||||
|
masses = sampler.densityChunk(massDensity, Math.floorDiv(blockX, CHUNK_WIDTH), Math.floorDiv(blockZ, CHUNK_WIDTH));
|
||||||
|
}
|
||||||
|
return masses[(localZ * CHUNK_WIDTH + localX) * (maxY - minY) + y - minY] > 0.0F;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean wallsKnown() {
|
||||||
|
return walls;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean wall(int localX, int y, int localZ) {
|
||||||
|
return walls && sampler.density(wallShell, blockX + localX, y, blockZ + localZ) > 0.0F;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte kind(int localX, int y, int localZ) {
|
||||||
|
if (y < minY || y >= maxY) {
|
||||||
|
throw new IllegalArgumentException("Y " + y + " is outside [" + minY + ", " + maxY + ")");
|
||||||
|
}
|
||||||
|
return column(localX, localZ).kinds()[y - minY];
|
||||||
|
}
|
||||||
|
|
||||||
|
private Column column(int localX, int localZ) {
|
||||||
|
if (localX < 0 || localX >= CHUNK_WIDTH || localZ < 0 || localZ >= CHUNK_WIDTH) {
|
||||||
|
throw new IllegalArgumentException("Column " + localX + "," + localZ + " is outside the chunk");
|
||||||
|
}
|
||||||
|
int index = localZ * CHUNK_WIDTH + localX;
|
||||||
|
Column column = columns[index];
|
||||||
|
if (column == null) {
|
||||||
|
column = sampleColumn(localX, localZ);
|
||||||
|
columns[index] = column;
|
||||||
|
}
|
||||||
|
return column;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Column sampleColumn(int localX, int localZ) {
|
||||||
|
byte[] column = new byte[maxY - minY];
|
||||||
|
int surfaceY = minY - 1;
|
||||||
|
for (int y = maxY - 1; y >= minY; y--) {
|
||||||
|
LevelChunkSection section = terrain.getSection(terrain.getSectionIndex(y));
|
||||||
|
byte kind = kinds.computeIfAbsent(section.getBlockState(localX, y & (CHUNK_WIDTH - 1), localZ).getBlock(), VanillaNoiseFidelitySource::classify);
|
||||||
|
column[y - minY] = kind;
|
||||||
|
if (kind == SOLID && surfaceY < minY) {
|
||||||
|
surfaceY = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Column(surfaceY, loadKey(blockX + localX, Math.max(minY, surfaceY), blockZ + localZ), column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||||
|
import art.arcane.iris.spi.VanillaDataSource;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.server.packs.PackResources;
|
||||||
|
import net.minecraft.server.packs.PackType;
|
||||||
|
import net.minecraft.server.packs.repository.ServerPacksSource;
|
||||||
|
import net.minecraft.server.packs.resources.IoSupplier;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class BukkitVanillaDataTest {
|
||||||
|
private static final String PLAINS = "data/minecraft/worldgen/biome/plains.json";
|
||||||
|
private static final String BIOME_TAGS = "data/minecraft/tags/worldgen/biome";
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrap() {
|
||||||
|
HeadlessNativeBootstrap.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void bukkitPlatformReadsTheServerPlainsBiome() throws IOException {
|
||||||
|
byte[] plains = new BukkitPlatform().vanillaData().read(PLAINS).orElseThrow();
|
||||||
|
assertTrue(new String(plains, StandardCharsets.UTF_8).contains("\"temperature\""));
|
||||||
|
try (PackResources vanilla = ServerPacksSource.createVanillaPackSource().fullResources()) {
|
||||||
|
IoSupplier<InputStream> resource = vanilla.getResource(PackType.SERVER_DATA,
|
||||||
|
Identifier.withDefaultNamespace("worldgen/biome/plains.json"));
|
||||||
|
try (InputStream input = resource.get()) {
|
||||||
|
assertArrayEquals(input.readAllBytes(), plains);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertNull(Bukkit.getServer());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void bukkitPlatformListsTheBiomeTagsTheGameLoads() {
|
||||||
|
List<String> expected = new ArrayList<>();
|
||||||
|
try (PackResources vanilla = ServerPacksSource.createVanillaPackSource().fullResources()) {
|
||||||
|
vanilla.listResources(PackType.SERVER_DATA, "minecraft", "tags/worldgen/biome",
|
||||||
|
(id, resource) -> expected.add("data/" + id.getNamespace() + "/" + id.getPath()));
|
||||||
|
}
|
||||||
|
Collections.sort(expected);
|
||||||
|
List<String> listed = new BukkitPlatform().vanillaData().list(BIOME_TAGS);
|
||||||
|
assertFalse(listed.isEmpty());
|
||||||
|
assertTrue(listed.contains(BIOME_TAGS + "/is_overworld.json"));
|
||||||
|
assertEquals(expected, listed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void headlessPlatformReadsTheSameVanillaData() {
|
||||||
|
HeadlessNativePlatform platform = new HeadlessNativePlatform(new File("unused-native-platform"),
|
||||||
|
HeadlessNativeTestRegistries::get);
|
||||||
|
VanillaDataSource bukkit = new BukkitPlatform().vanillaData();
|
||||||
|
assertArrayEquals(bukkit.read(PLAINS).orElseThrow(), platform.vanillaData().read(PLAINS).orElseThrow());
|
||||||
|
assertEquals(bukkit.list(BIOME_TAGS), platform.vanillaData().list(BIOME_TAGS));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import net.minecraft.core.RegistryAccess;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.RandomState;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.CachingDensitySampler;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.DensityFunction;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.DensitySampler;
|
||||||
|
import net.minecraft.world.level.levelgen.material.rule.ConditionRule;
|
||||||
|
import net.minecraft.world.level.levelgen.material.rule.MaterialRule;
|
||||||
|
import net.minecraft.world.level.levelgen.material.rule.OreVeinRule;
|
||||||
|
import net.minecraft.world.level.levelgen.material.rule.SequenceRule;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
|
final class CompiledCaches {
|
||||||
|
private static final String MINECRAFT_PACKAGE = "net.minecraft.";
|
||||||
|
private static final String DENSITY_PACKAGE = "net.minecraft.world.level.levelgen.densityfunction.";
|
||||||
|
private static final String SPLINE_PACKAGE = "net.minecraft.util.";
|
||||||
|
private static final String SPLINE_NAME = "Spline";
|
||||||
|
|
||||||
|
private final RegistryAccess registries;
|
||||||
|
private final NoiseGeneratorSettings settings;
|
||||||
|
private final RandomState state;
|
||||||
|
|
||||||
|
private CompiledCaches(RegistryAccess registries, NoiseGeneratorSettings settings, long seed) {
|
||||||
|
this.registries = registries;
|
||||||
|
this.settings = settings;
|
||||||
|
this.state = RandomState.create(registries.lookupOrThrow(Registries.NOISE), seed, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
static CompiledCaches open(RegistryAccess registries, long seed) {
|
||||||
|
if (!(DatapackCostProbe.exportedOverworld(registries).generator() instanceof NoiseBasedChunkGenerator generator)) {
|
||||||
|
throw new IllegalStateException("Exported overworld has no noise generator");
|
||||||
|
}
|
||||||
|
return new CompiledCaches(registries, generator.generatorSettings().value(), seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
DensityFunction finalDensity() {
|
||||||
|
return settings.noiseRouter().finalDensity();
|
||||||
|
}
|
||||||
|
|
||||||
|
DensityFunction function(String id) {
|
||||||
|
return registries.lookupOrThrow(Registries.DENSITY_FUNCTION)
|
||||||
|
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, Identifier.parse(id))).value();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<OreVeinRule> oreVeins() {
|
||||||
|
List<OreVeinRule> veins = new ArrayList<>();
|
||||||
|
Deque<MaterialRule> pending = new ArrayDeque<>();
|
||||||
|
pending.push(settings.materialRule().value());
|
||||||
|
while (!pending.isEmpty()) {
|
||||||
|
MaterialRule rule = pending.pop();
|
||||||
|
switch (rule) {
|
||||||
|
case OreVeinRule vein -> veins.add(vein);
|
||||||
|
case SequenceRule sequence -> sequence.sequence().reversed().forEach(pending::push);
|
||||||
|
case ConditionRule condition -> pending.push(condition.thenRun());
|
||||||
|
default -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return veins;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rootId(DensityFunction function) {
|
||||||
|
if (!(state.getSampler(function) instanceof CachingDensitySampler cache)) {
|
||||||
|
throw new IllegalStateException("Density function " + function + " does not compile to a cache");
|
||||||
|
}
|
||||||
|
return cache.id();
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Integer> ids(DensityFunction function) {
|
||||||
|
Set<Integer> ids = new TreeSet<>();
|
||||||
|
Set<Object> visited = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||||
|
Deque<Object> pending = new ArrayDeque<>();
|
||||||
|
pending.push(state.getSampler(function));
|
||||||
|
while (!pending.isEmpty()) {
|
||||||
|
Object node = pending.pop();
|
||||||
|
if (node == null || !visited.add(node)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (node instanceof CachingDensitySampler cache) {
|
||||||
|
ids.add(cache.id());
|
||||||
|
}
|
||||||
|
for (Object child : children(node)) {
|
||||||
|
pending.push(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Object> children(Object node) {
|
||||||
|
List<Object> children = new ArrayList<>();
|
||||||
|
if (node instanceof Object[] array) {
|
||||||
|
Collections.addAll(children, array);
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
if (node instanceof Collection<?> collection) {
|
||||||
|
children.addAll(collection);
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
if (!traversed(node)) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
for (Class<?> type = node.getClass(); type != null && type.getName().startsWith(MINECRAFT_PACKAGE); type = type.getSuperclass()) {
|
||||||
|
for (Field field : type.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(field.getModifiers()) || field.getType().isPrimitive()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
children.add(read(field, node));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean traversed(Object node) {
|
||||||
|
if (node instanceof Enum<?>) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String name = node.getClass().getName();
|
||||||
|
return node instanceof DensitySampler || name.startsWith(DENSITY_PACKAGE) || name.startsWith(SPLINE_PACKAGE) && name.contains(SPLINE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object read(Field field, Object owner) {
|
||||||
|
try {
|
||||||
|
field.setAccessible(true);
|
||||||
|
return field.get(owner);
|
||||||
|
} catch (IllegalAccessException | RuntimeException failure) {
|
||||||
|
throw new IllegalStateException("Cannot read " + field + " of " + owner.getClass().getName(), failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.probe.fidelity.FidelityReport;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class DatapackFidelityProbeBiomeModeTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporary = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Test(timeout = 300_000L)
|
||||||
|
public void biomeModeJudgesTheSeedMeanWithoutTheChunkScan() throws Exception {
|
||||||
|
Path pack = DatapackFidelityProbeTest.pack(temporary);
|
||||||
|
Path output = temporary.getRoot().toPath().resolve("reports").resolve("biomes.json");
|
||||||
|
|
||||||
|
FidelityReport report = DatapackFidelityProbe.execute(
|
||||||
|
new String[]{pack.toString(), "main", "1337", "0", "1", output.toString(), "256", "32", "3", "biomes"}, DatapackFidelityProbeTest.exporter());
|
||||||
|
|
||||||
|
JsonObject json = JsonParser.parseString(Files.readString(output, StandardCharsets.UTF_8)).getAsJsonObject();
|
||||||
|
assertTrue(report.toTable(), report.passed());
|
||||||
|
assertTrue(json.get("passed").getAsBoolean());
|
||||||
|
assertEquals(1.0D, DatapackFidelityProbeTest.metric(report, "biome.share.plains").export(), 0.0D);
|
||||||
|
assertEquals(1, report.sampling().size());
|
||||||
|
assertEquals(List.of(1337L, 1338L, 1339L), report.sampling().getFirst().seeds());
|
||||||
|
assertEquals(3, json.getAsJsonArray("seeds").size());
|
||||||
|
assertEquals(3, json.getAsJsonObject("seedsPassing").getAsJsonObject("biome.share.plains").get("passing").getAsInt());
|
||||||
|
assertTrue(report.observations().stream().anyMatch(observation -> observation.name().equals("biome.type.land")
|
||||||
|
&& observation.iris() == 1.0D && observation.export() == 1.0D));
|
||||||
|
assertTrue(RealPackProbeSupport.drainReported().isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenDatapackExporter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.DimensionStage;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.ImageMapStage;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.PackCompiler;
|
||||||
|
import art.arcane.iris.pack.datapack.export.emit.WorldgenJsonWriter;
|
||||||
|
import art.arcane.iris.probe.fidelity.FidelityMetric;
|
||||||
|
import art.arcane.iris.probe.fidelity.FidelityReport;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class DatapackFidelityProbeTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporary = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Test(timeout = 300_000L)
|
||||||
|
public void probeExportsTheSyntheticPackAndComparesBothGenerators() throws Exception {
|
||||||
|
Path pack = pack(temporary);
|
||||||
|
Path output = temporary.getRoot().toPath().resolve("reports").resolve("fidelity.json");
|
||||||
|
|
||||||
|
FidelityReport report = DatapackFidelityProbe.execute(
|
||||||
|
new String[]{pack.toString(), "main", "1337", "1", "8", output.toString(), "256", "32", "2", "full"}, exporter());
|
||||||
|
|
||||||
|
JsonObject json = JsonParser.parseString(Files.readString(output, StandardCharsets.UTF_8)).getAsJsonObject();
|
||||||
|
assertTrue(report.toTable(), report.passed());
|
||||||
|
assertTrue(json.get("passed").getAsBoolean());
|
||||||
|
assertEquals(report.metrics().size(), json.getAsJsonArray("metrics").size());
|
||||||
|
FidelityMetric biome = metric(report, "biome.share.plains");
|
||||||
|
assertEquals(1.0D, biome.iris(), 0.0D);
|
||||||
|
assertEquals(1.0D, biome.export(), 0.0D);
|
||||||
|
assertTrue(biome.pass());
|
||||||
|
assertEquals(0.0D, metric(report, "biome.totalVariation").delta(), 0.0D);
|
||||||
|
assertEquals(1.0D, metric(report, "height.landFraction").export(), 0.0D);
|
||||||
|
JsonObject columns = json.getAsJsonArray("sampling").get(0).getAsJsonObject();
|
||||||
|
assertEquals("columns", columns.get("scope").getAsString());
|
||||||
|
assertEquals(256, columns.get("windowBlocks").getAsInt());
|
||||||
|
assertEquals(32, columns.get("strideBlocks").getAsInt());
|
||||||
|
assertEquals(2, columns.getAsJsonArray("seeds").size());
|
||||||
|
assertEquals(128L, columns.get("columns").getAsLong());
|
||||||
|
JsonObject chunks = json.getAsJsonArray("sampling").get(1).getAsJsonObject();
|
||||||
|
assertEquals("chunks", chunks.get("scope").getAsString());
|
||||||
|
assertEquals(48, chunks.get("windowBlocks").getAsInt());
|
||||||
|
assertTrue(RealPackProbeSupport.drainReported().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void malformedArgumentsAreRejectedBeforeThePackOpens() throws Exception {
|
||||||
|
Path pack = pack(temporary);
|
||||||
|
String output = temporary.getRoot().toPath().resolve("unused.json").toString();
|
||||||
|
|
||||||
|
IllegalArgumentException missing = assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> DatapackFidelityProbe.execute(new String[]{pack.toString(), "main"}, exporter()));
|
||||||
|
IllegalArgumentException stride = assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> DatapackFidelityProbe.execute(new String[]{pack.toString(), "main", "1337", "1", "17", output, "256", "32", "1", "full"}, exporter()));
|
||||||
|
IllegalArgumentException survey = assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> DatapackFidelityProbe.execute(new String[]{pack.toString(), "main", "1337", "1", "8", output, "250", "32", "1", "full"}, exporter()));
|
||||||
|
IllegalArgumentException seeds = assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> DatapackFidelityProbe.execute(new String[]{pack.toString(), "main", "1337", "1", "8", output, "256", "32", "0", "full"}, exporter()));
|
||||||
|
IllegalArgumentException mode = assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> DatapackFidelityProbe.execute(new String[]{pack.toString(), "main", "1337", "1", "8", output, "256", "32", "1", "caves"}, exporter()));
|
||||||
|
|
||||||
|
assertTrue(missing.getMessage(), missing.getMessage().startsWith("Usage:"));
|
||||||
|
assertTrue(stride.getMessage(), stride.getMessage().contains("stride"));
|
||||||
|
assertTrue(survey.getMessage(), survey.getMessage().contains("multiple"));
|
||||||
|
assertTrue(seeds.getMessage(), seeds.getMessage().contains("surveySeeds"));
|
||||||
|
assertTrue(mode.getMessage(), mode.getMessage().contains("caves"));
|
||||||
|
assertFalse(Files.exists(Path.of(output)));
|
||||||
|
}
|
||||||
|
|
||||||
|
static WorldgenDatapackExporter exporter() {
|
||||||
|
return new WorldgenDatapackExporter(WorldgenJsonWriter::forTarget, new PackCompiler(List.of(new DimensionStage(), new ImageMapStage())));
|
||||||
|
}
|
||||||
|
|
||||||
|
static FidelityMetric metric(FidelityReport report, String name) {
|
||||||
|
for (FidelityMetric metric : report.metrics()) {
|
||||||
|
if (metric.name().equals(name)) {
|
||||||
|
return metric;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new AssertionError("Report has no metric " + name + ": " + report.toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
static Path pack(TemporaryFolder temporary) throws Exception {
|
||||||
|
Path pack = temporary.newFolder().toPath();
|
||||||
|
for (String directory : List.of("dimensions", "regions", "biomes", "generators")) {
|
||||||
|
Files.createDirectories(pack.resolve(directory));
|
||||||
|
}
|
||||||
|
Files.writeString(pack.resolve("dimensions/main.json"), """
|
||||||
|
{"name":"Fidelity","focus":"plains","regions":["owned"],"landChance":1,"carvingEnabled":false}
|
||||||
|
""");
|
||||||
|
Files.writeString(pack.resolve("regions/owned.json"), "{\"landBiomes\":[\"plains\"]}");
|
||||||
|
Files.writeString(pack.resolve("generators/flat.json"), "{\"composite\":[]}");
|
||||||
|
Files.writeString(pack.resolve("biomes/plains.json"), """
|
||||||
|
{"derivative":"minecraft:plains","generators":[{"generator":"flat","min":0,"max":0}]}
|
||||||
|
""");
|
||||||
|
return pack;
|
||||||
|
}
|
||||||
|
}
|
||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.DensityFunction;
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
public final class DensityCostAnalysisTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
|
private static final double EPSILON = 1.0E-9;
|
||||||
|
private static final String LEAF = "{\"type\":\"noise\",\"noise\":\"minecraft:temperature\",\"xz_scale\":1.0,\"y_scale\":0.0}";
|
||||||
|
|
||||||
|
private static HeadlessNativeRegistries registries;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void loadGraphs() throws IOException {
|
||||||
|
Map<String, String> definitions = new LinkedHashMap<>();
|
||||||
|
definitions.put(path("leaf"), LEAF);
|
||||||
|
definitions.put(path("pair"), "{\"type\":\"add\",\"left\":\"t:leaf\",\"right\":\"t:leaf\"}");
|
||||||
|
definitions.put(path("quad"), "{\"type\":\"mul\",\"left\":\"t:pair\",\"right\":\"t:pair\"}");
|
||||||
|
definitions.put(path("cached"), "{\"type\":\"cache\",\"input\":\"t:pair\"}");
|
||||||
|
definitions.put(path("twice"), "{\"type\":\"add\",\"left\":\"t:cached\",\"right\":\"t:cached\"}");
|
||||||
|
definitions.put(path("select"), "{\"type\":\"interval_select\",\"input\":\"t:leaf\",\"thresholds\":[0.0],\"functions\":[\"t:quad\",\"t:pair\"]}");
|
||||||
|
definitions.put(path("spline"), "{\"type\":\"spline\",\"spline\":{\"coordinate\":\"t:leaf\",\"points\":["
|
||||||
|
+ "{\"location\":-1.0,\"value\":" + nested("t:quad") + ",\"derivative\":0.0},"
|
||||||
|
+ "{\"location\":0.0,\"value\":0.0,\"derivative\":0.0},"
|
||||||
|
+ "{\"location\":1.0,\"value\":" + nested("t:quad") + ",\"derivative\":0.0}]}}");
|
||||||
|
definitions.put(path("grid"), "{\"type\":\"interpolated\",\"input\":\"t:pair\",\"cell_size_xz\":4,\"cell_size_y\":8}");
|
||||||
|
registries = HeadlessNativeRegistries.load(definitions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void closeRegistries() throws IOException {
|
||||||
|
if (registries != null) {
|
||||||
|
registries.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void referencesExpandWhileTheDagSharesThem() {
|
||||||
|
DensityCostAnalysis.RootCost quad = analysis().analyze("quad", function("quad"));
|
||||||
|
assertEquals(3L, quad.dagNodes());
|
||||||
|
assertEquals(7.0, quad.expandedNodes(), EPSILON);
|
||||||
|
assertEquals(7.0, quad.cachedNodes(), EPSILON);
|
||||||
|
assertEquals(2, quad.namedSubtrees());
|
||||||
|
assertEquals("t:leaf", quad.exclusive().getFirst().name());
|
||||||
|
assertEquals(4.0, quad.exclusive().getFirst().multiplicity(), EPSILON);
|
||||||
|
assertEquals(4.0, quad.exclusive().getFirst().contribution(), EPSILON);
|
||||||
|
assertEquals("t:pair", quad.inclusive().getFirst().name());
|
||||||
|
assertEquals(6.0, quad.inclusive().getFirst().inclusive(), EPSILON);
|
||||||
|
assertEquals(1.0 / 3.0, quad.dagShare().get("leaf"), EPSILON);
|
||||||
|
assertEquals(1.0 / 3.0, quad.dagShare().get("quad"), EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cachesEvaluateTheirBodyOnceWhenEnabled() {
|
||||||
|
DensityCostAnalysis.RootCost twice = analysis().analyze("twice", function("twice"));
|
||||||
|
assertEquals(4L, twice.dagNodes());
|
||||||
|
assertEquals(9.0, twice.expandedNodes(), EPSILON);
|
||||||
|
assertEquals(6.0, twice.cachedNodes(), EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void uncachedPointEvaluationTakesOneSelectedBranch() {
|
||||||
|
DensityCostAnalysis analysis = analysis();
|
||||||
|
DensityCostAnalysis.RootCost select = analysis.analyze("select", function("select"));
|
||||||
|
assertEquals(12.0, select.expandedNodes(), EPSILON);
|
||||||
|
assertEquals(9.0, select.uncachedPointWorst(), EPSILON);
|
||||||
|
DensityCostAnalysis.RootCost spline = analysis.analyze("spline", function("spline"));
|
||||||
|
assertEquals(19.0, spline.expandedNodes(), EPSILON);
|
||||||
|
assertEquals(11.0, spline.uncachedPointWorst(), EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void offLatticeGridPointsSampleEightCorners() {
|
||||||
|
DensityCostAnalysis.RootCost grid = analysis().analyze("grid", function("grid"));
|
||||||
|
assertEquals(4.0, grid.expandedNodes(), EPSILON);
|
||||||
|
assertEquals(25.0, grid.uncachedPointWorst(), EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DensityCostAnalysis analysis() {
|
||||||
|
return new DensityCostAnalysis(id -> DensityJsonBreakdown.category("overworld", id), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DensityFunction function(String name) {
|
||||||
|
return registries.registries().lookupOrThrow(Registries.DENSITY_FUNCTION)
|
||||||
|
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, Identifier.parse("t:" + name))).value();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String nested(String coordinate) {
|
||||||
|
return "{\"coordinate\":\"" + coordinate + "\",\"points\":[{\"location\":0.0,\"value\":1.0,\"derivative\":0.0},"
|
||||||
|
+ "{\"location\":1.0,\"value\":2.0,\"derivative\":0.0}]}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String path(String name) {
|
||||||
|
return "data/t/worldgen/density_function/" + name + ".json";
|
||||||
|
}
|
||||||
|
}
|
||||||
+88
@@ -0,0 +1,88 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
public final class DensityJsonBreakdownTest {
|
||||||
|
private static final String TEMPERATURE = "{\"type\":\"add\",\"left\":\"x:biome/selector/0\",\"right\":1.0}";
|
||||||
|
private static final String SELECTOR = "{\"type\":\"mul\",\"left\":\"x:cell/cellular/a/value\",\"right\":2.0}";
|
||||||
|
private static final String CELL_A = "{\"type\":\"noise\",\"noise\":\"x:hash_a\",\"xz_scale\":0.5,\"y_scale\":0.0}";
|
||||||
|
private static final String CELL_B = "{\"type\":\"noise\",\"noise\":\"x:hash_b\",\"xz_scale\":0.25,\"y_scale\":0.0}";
|
||||||
|
private static final String FINAL_DENSITY = "{\"type\":\"min\",\"left\":\"x:cell/cellular/b/value\",\"right\":0.0}";
|
||||||
|
private static final String NOISE = "{\"firstOctave\":-1,\"amplitudes\":[1.0]}";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void categoriesFollowProducingCompilers() {
|
||||||
|
assertEquals("router", DensityJsonBreakdown.category("overworld", "x:overworld/final_density"));
|
||||||
|
assertEquals("cellular", DensityJsonBreakdown.category("overworld", "x:cell/cracks/abc/distance"));
|
||||||
|
assertEquals("biome selector", DensityJsonBreakdown.category("overworld", "x:biome/selector/12"));
|
||||||
|
assertEquals("biome decision", DensityJsonBreakdown.category("overworld", "x:biome/base_code"));
|
||||||
|
assertEquals("terrain height", DensityJsonBreakdown.category("overworld", "x:height"));
|
||||||
|
assertEquals("terrain bands", DensityJsonBreakdown.category("overworld", "x:terrain/plains/blend/3"));
|
||||||
|
assertEquals("noise style", DensityJsonBreakdown.category("overworld", "x:style/shared/ab12"));
|
||||||
|
assertEquals("cellular offset copies", DensityJsonBreakdown.category("overworld", "x:cell/cellular/abc/vx_0_0/offset_x4_z0"));
|
||||||
|
assertEquals("biome selector offset copies", DensityJsonBreakdown.category("overworld", "x:biome/selector/4/offset_x0_z4"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void reportsBytesShapesAndCellularOwners() {
|
||||||
|
SortedMap<String, byte[]> files = new TreeMap<>();
|
||||||
|
put(files, "density_function/overworld/temperature", TEMPERATURE);
|
||||||
|
put(files, "density_function/biome/selector/0", SELECTOR);
|
||||||
|
put(files, "density_function/cell/cellular/a/value", CELL_A);
|
||||||
|
put(files, "density_function/cell/cellular/b/value", CELL_B);
|
||||||
|
put(files, "density_function/overworld/final_density", FINAL_DENSITY);
|
||||||
|
put(files, "noise/hash_a", NOISE);
|
||||||
|
put(files, "noise/hash_b", NOISE);
|
||||||
|
|
||||||
|
DensityJsonBreakdown.Report report = DensityJsonBreakdown.analyze(files, "overworld").report();
|
||||||
|
|
||||||
|
assertEquals(5, report.files());
|
||||||
|
assertEquals(bytes(TEMPERATURE, SELECTOR, CELL_A, CELL_B, FINAL_DENSITY), report.bytes());
|
||||||
|
DensityJsonBreakdown.CategoryBytes cellular = category(report, "cellular");
|
||||||
|
assertEquals(2, cellular.files());
|
||||||
|
assertEquals(1, cellular.distinctShapes());
|
||||||
|
assertEquals(bytes(CELL_B), cellular.structurallyDuplicateBytes());
|
||||||
|
assertEquals(2, category(report, "router").files());
|
||||||
|
assertEquals(2, report.cellularInstances());
|
||||||
|
assertEquals(1, report.cellularShapes().size());
|
||||||
|
assertEquals("biome selector+router", report.cellularShapes().getFirst().owners());
|
||||||
|
assertEquals(bytes(CELL_A), owner(report, "cellular via biome selector"));
|
||||||
|
assertEquals(bytes(CELL_B), owner(report, "cellular via router"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DensityJsonBreakdown.CategoryBytes category(DensityJsonBreakdown.Report report, String name) {
|
||||||
|
for (DensityJsonBreakdown.CategoryBytes category : report.categories()) {
|
||||||
|
if (category.category().equals(name)) {
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new AssertionError("No category " + name + " in " + report.categories());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long owner(DensityJsonBreakdown.Report report, String name) {
|
||||||
|
for (DensityJsonBreakdown.Bytes owner : report.libraryOwners()) {
|
||||||
|
if (owner.name().equals(name)) {
|
||||||
|
return owner.bytes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new AssertionError("No owner " + name + " in " + report.libraryOwners());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long bytes(String... bodies) {
|
||||||
|
long total = 0;
|
||||||
|
for (String body : bodies) {
|
||||||
|
total += body.getBytes(StandardCharsets.UTF_8).length;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void put(SortedMap<String, byte[]> files, String path, String body) {
|
||||||
|
files.put("data/x/worldgen/" + path + ".json", body.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -137,7 +137,7 @@ public class HeadlessChunkGeneratorTest {
|
|||||||
}
|
}
|
||||||
Files.writeString(pack.resolve("dimensions/main.json"), """
|
Files.writeString(pack.resolve("dimensions/main.json"), """
|
||||||
{"name":"Native columns","focus":"swamp","regions":["owned"],"fluidHeight":40,"logicalHeight":256,
|
{"name":"Native columns","focus":"swamp","regions":["owned"],"fluidHeight":40,"logicalHeight":256,
|
||||||
"landChance":1,"carvingEnabled":false,"hydrology":{"rivers":{"enabled":false}}}
|
"landChance":1,"carvingEnabled":false}
|
||||||
""");
|
""");
|
||||||
Files.writeString(pack.resolve("regions/owned.json"), "{\"landBiomes\":[\"swamp\"]}");
|
Files.writeString(pack.resolve("regions/owned.json"), "{\"landBiomes\":[\"swamp\"]}");
|
||||||
Files.writeString(pack.resolve("generators/flat.json"), "{\"composite\":[]}");
|
Files.writeString(pack.resolve("generators/flat.json"), "{\"composite\":[]}");
|
||||||
|
|||||||
+5
-7
@@ -1,7 +1,6 @@
|
|||||||
package art.arcane.iris.probe;
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
import art.arcane.iris.structure.nativegen.NativeStructureVolumeIndex;
|
import art.arcane.iris.structure.nativegen.NativeStructureVolumeIndex;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
|
|
||||||
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
|
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import net.minecraft.nbt.CompoundTag;
|
import net.minecraft.nbt.CompoundTag;
|
||||||
@@ -68,11 +67,10 @@ public final class HeadlessRegionTerrainTest {
|
|||||||
OfflineRegionGenerator.Configuration export = new OfflineRegionGenerator.Configuration(
|
OfflineRegionGenerator.Configuration export = new OfflineRegionGenerator.Configuration(
|
||||||
workspace.pack(), "main", 69420L, -3, 2, 1, 2, 3, 4, configuration,
|
workspace.pack(), "main", 69420L, -3, 2, 1, 2, 3, 4, configuration,
|
||||||
"probe", Level.OVERWORLD, NoiseGeneratorSettings.OVERWORLD, root.resolve("export"));
|
"probe", Level.OVERWORLD, NoiseGeneratorSettings.OVERWORLD, root.resolve("export"));
|
||||||
HydrologyTileCache.PregenerationArea bounds = export.hydrologyArea();
|
List<long[]> regions = export.hydrologyRegions();
|
||||||
assertEquals(-80L, bounds.minimumBlockX());
|
assertEquals(2, regions.size());
|
||||||
assertEquals(0L, bounds.minimumBlockZ());
|
assertArrayEquals(new long[]{-1L, 0L}, regions.get(0));
|
||||||
assertEquals(15L, bounds.maximumBlockX());
|
assertArrayEquals(new long[]{0L, 0L}, regions.get(1));
|
||||||
assertEquals(111L, bounds.maximumBlockZ());
|
|
||||||
assertEquals(1, result.targetChunks());
|
assertEquals(1, result.targetChunks());
|
||||||
assertEquals(529, result.chunks().size());
|
assertEquals(529, result.chunks().size());
|
||||||
long terrain = result.chunks().stream().filter(chunk -> chunk.getPersistedStatus() == ChunkStatus.TERRAIN).count();
|
long terrain = result.chunks().stream().filter(chunk -> chunk.getPersistedStatus() == ChunkStatus.TERRAIN).count();
|
||||||
@@ -189,7 +187,7 @@ public final class HeadlessRegionTerrainTest {
|
|||||||
}
|
}
|
||||||
Files.writeString(pack.resolve("dimensions/main.json"), """
|
Files.writeString(pack.resolve("dimensions/main.json"), """
|
||||||
{"name":"Native region","focus":"swamp","regions":["owned"],"fluidHeight":40,"logicalHeight":256,
|
{"name":"Native region","focus":"swamp","regions":["owned"],"fluidHeight":40,"logicalHeight":256,
|
||||||
"landChance":1,"carvingEnabled":false,"hydrology":{"rivers":{"enabled":false}}}
|
"landChance":1,"carvingEnabled":false}
|
||||||
""");
|
""");
|
||||||
Files.writeString(pack.resolve("regions/owned.json"), "{\"landBiomes\":[\"swamp\"]}");
|
Files.writeString(pack.resolve("regions/owned.json"), "{\"landBiomes\":[\"swamp\"]}");
|
||||||
Files.writeString(pack.resolve("generators/flat.json"), "{\"composite\":[]}");
|
Files.writeString(pack.resolve("generators/flat.json"), "{\"composite\":[]}");
|
||||||
|
|||||||
+1
-19
@@ -1,37 +1,19 @@
|
|||||||
package art.arcane.iris.probe;
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
import io.papermc.paper.registry.PaperRegistryAccess;
|
|
||||||
import org.junit.rules.TestRule;
|
import org.junit.rules.TestRule;
|
||||||
import org.junit.runner.Description;
|
import org.junit.runner.Description;
|
||||||
import org.junit.runners.model.Statement;
|
import org.junit.runners.model.Statement;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public final class PaperRegistryIsolation implements TestRule {
|
public final class PaperRegistryIsolation implements TestRule {
|
||||||
@Override
|
@Override
|
||||||
public Statement apply(Statement base, Description description) {
|
public Statement apply(Statement base, Description description) {
|
||||||
return new Statement() {
|
return new Statement() {
|
||||||
@Override
|
@Override
|
||||||
public void evaluate() throws Throwable {
|
public void evaluate() throws Throwable {
|
||||||
Map<Object, Object> registries = registries();
|
try (PaperRegistryScope scope = PaperRegistryScope.open()) {
|
||||||
Map<Object, Object> previous = new HashMap<>(registries);
|
|
||||||
registries.clear();
|
|
||||||
try {
|
|
||||||
base.evaluate();
|
base.evaluate();
|
||||||
} finally {
|
|
||||||
registries.clear();
|
|
||||||
registries.putAll(previous);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private static Map<Object, Object> registries() throws ReflectiveOperationException {
|
|
||||||
Field field = PaperRegistryAccess.class.getDeclaredField("registries");
|
|
||||||
field.setAccessible(true);
|
|
||||||
return (Map<Object, Object>) field.get(PaperRegistryAccess.instance());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
|
import art.arcane.iris.pack.datapack.export.emit.DensityFunctionEmitter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.emit.FunctionTable;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Axis;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Field;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.NoiseDef;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ValueRange;
|
||||||
|
import art.arcane.iris.pack.datapack.export.lower.ExportCapabilities;
|
||||||
|
import art.arcane.iris.pack.datapack.export.lower.VanillaBounds;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.util.Interval;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class VanillaBoundsParity263Test {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
|
private static final String NAMESPACE = "iris_export";
|
||||||
|
private static final List<NoiseDef> NOISES = List.of(new NoiseDef("parity/base", -3, List.of(1.0, 0.5, 0.25)),
|
||||||
|
new NoiseDef("parity/sparse", 0, List.of(0.0, 1.5, 0.0, 0.7)));
|
||||||
|
private static final Field BASE = new Field.Noise(NAMESPACE + ":parity/base", 0.25, 0.0, null, null, null);
|
||||||
|
private static final Field SPARSE = new Field.Noise(NAMESPACE + ":parity/sparse", 0.5, 0.25, null, null, null);
|
||||||
|
private static final Field UNIT = new Field.Clamp(BASE, 0.0, 1.0);
|
||||||
|
private static final Field BAND = new Field.Clamp(SPARSE, 0.3, 0.9);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void boundsMatchMinecraftForEveryEmittedNode() throws Exception {
|
||||||
|
List<Field> fields = fields();
|
||||||
|
VanillaBounds bounds = VanillaBounds.of(NAMESPACE, NOISES);
|
||||||
|
DensityFunctionEmitter emitter = DensityFunctionEmitter.forTarget(ExportCapabilities.forTarget(DataVersion.V26_3), NAMESPACE, bounds);
|
||||||
|
FunctionTable table = new FunctionTable(NAMESPACE);
|
||||||
|
List<String> references = new ArrayList<>(fields.size());
|
||||||
|
for (int index = 0; index < fields.size(); index++) {
|
||||||
|
JsonElement emitted = emitter.emit(fields.get(index), table);
|
||||||
|
boolean reference = emitted.isJsonPrimitive() && emitted.getAsJsonPrimitive().isString();
|
||||||
|
references.add(reference ? emitted.getAsString() : table.register("parity/field_" + index, emitted));
|
||||||
|
}
|
||||||
|
Map<String, String> definitions = new LinkedHashMap<>();
|
||||||
|
for (Map.Entry<String, JsonElement> function : table.files().entrySet()) {
|
||||||
|
definitions.put("data/" + NAMESPACE + "/worldgen/density_function/" + function.getKey() + ".json", function.getValue().toString());
|
||||||
|
}
|
||||||
|
for (NoiseDef noise : NOISES) {
|
||||||
|
definitions.put("data/" + NAMESPACE + "/worldgen/noise/" + noise.id() + ".json", noiseJson(noise).toString());
|
||||||
|
}
|
||||||
|
try (HeadlessNativeRegistries registries = HeadlessNativeRegistries.load(definitions)) {
|
||||||
|
for (int index = 0; index < fields.size(); index++) {
|
||||||
|
Interval vanilla = registries.registries().lookupOrThrow(Registries.DENSITY_FUNCTION)
|
||||||
|
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, Identifier.parse(references.get(index)))).value().range();
|
||||||
|
Optional<ValueRange> modelled = bounds.range(fields.get(index));
|
||||||
|
assertTrue("field " + index + " " + fields.get(index) + " has no modelled range, Minecraft " + vanilla, modelled.isPresent());
|
||||||
|
assertEquals("field " + index + " " + fields.get(index), new ValueRange(vanilla.min(), vanilla.max()), modelled.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Field> fields() {
|
||||||
|
Field scaled = new Field.Binary(Field.BinaryOp.MUL, new Field.Const(0.3), BAND);
|
||||||
|
Field curve = new Field.Curve(new Field.Binary(Field.BinaryOp.MUL, BASE, new Field.Const(1.7)), List.of(
|
||||||
|
new Field.Knot(-1.0, new Field.Const(0.2), 0.75),
|
||||||
|
new Field.Knot(0.1, new Field.Curve(SPARSE, List.of(new Field.Knot(-0.5, new Field.Const(-1.0), 0.0),
|
||||||
|
new Field.Knot(0.5, new Field.Const(1.3), 2.0))), 0.0),
|
||||||
|
new Field.Knot(0.8, new Field.Const(0.6), -1.5)));
|
||||||
|
List<Field> fields = new ArrayList<>();
|
||||||
|
fields.add(BASE);
|
||||||
|
fields.add(SPARSE);
|
||||||
|
fields.add(new Field.Const(0.1));
|
||||||
|
fields.add(new Field.Coord(Axis.Y, Field.Tiling.CLAMP, -64.0, 320.0, 1.5, -0.7));
|
||||||
|
fields.add(new Field.Clamp(BASE, -0.3, 0.8));
|
||||||
|
for (Field.BinaryOp op : Field.BinaryOp.values()) {
|
||||||
|
fields.add(new Field.Binary(op, BASE, new Field.Binary(Field.BinaryOp.ADD, BAND, new Field.Const(0.6))));
|
||||||
|
}
|
||||||
|
fields.add(new Field.Binary(Field.BinaryOp.POW, UNIT, new Field.Const(2.5)));
|
||||||
|
fields.add(new Field.Binary(Field.BinaryOp.MAX, scaled, new Field.Const(0.0)));
|
||||||
|
fields.add(new Field.Binary(Field.BinaryOp.MIN, new Field.Const(2.0), scaled));
|
||||||
|
fields.add(new Field.Binary(Field.BinaryOp.MIN, new Field.Const(0.2), scaled));
|
||||||
|
for (Field.UnaryOp op : Field.UnaryOp.values()) {
|
||||||
|
fields.add(new Field.Unary(op, new Field.Binary(Field.BinaryOp.SUB, SPARSE, new Field.Const(0.4))));
|
||||||
|
}
|
||||||
|
fields.add(new Field.Lerp(UNIT, BASE, new Field.Binary(Field.BinaryOp.MUL, SPARSE, new Field.Const(3.0))));
|
||||||
|
for (Field.RoundMode mode : Field.RoundMode.values()) {
|
||||||
|
fields.add(new Field.Round(mode, new Field.Binary(Field.BinaryOp.MUL, BASE, new Field.Const(10.3)), new Field.Const(2.5)));
|
||||||
|
}
|
||||||
|
fields.add(curve);
|
||||||
|
fields.add(new Field.Curve(UNIT, List.of(new Field.Knot(0.0, new Field.Const(0.0), -3.0), new Field.Knot(1.0, new Field.Const(1.0), -3.0))));
|
||||||
|
fields.add(new Field.Partition(BASE, List.of(-0.2, 0.4), List.of(new Field.Const(-3.0), new Field.Const(0.5), new Field.Const(7.25))));
|
||||||
|
fields.add(new Field.Mux(new Field.Binary(Field.BinaryOp.MUL, UNIT, new Field.Const(2.0)), List.of(new Field.Const(4.0), BAND, scaled)));
|
||||||
|
fields.add(new Field.Gate(BASE, new Field.Binary(Field.BinaryOp.ADD, BAND, new Field.Const(40.0))));
|
||||||
|
fields.add(new Field.RangeIf(BASE, -0.1, 0.2, new Field.Const(-2.0), BAND));
|
||||||
|
fields.add(new Field.Grid(new Field.Binary(Field.BinaryOp.ADD, BASE, SPARSE), 4, 8));
|
||||||
|
fields.add(new Field.Shared("parity/shared", new Field.Binary(Field.BinaryOp.SUB, BAND, BASE)));
|
||||||
|
fields.add(new Field.Slice(new Field.Binary(Field.BinaryOp.MUL, SPARSE, new Field.Const(-2.0)), Axis.Y, 64));
|
||||||
|
fields.add(new Field.TopSurface(new Field.Binary(Field.BinaryOp.SUB, BASE, new Field.Const(0.1)), 200, -48, 4));
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonObject noiseJson(NoiseDef noise) {
|
||||||
|
JsonArray modifiers = new JsonArray(noise.amplitudes().size());
|
||||||
|
for (double amplitude : noise.amplitudes()) {
|
||||||
|
modifiers.add(amplitude);
|
||||||
|
}
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.addProperty("base_octave", noise.firstOctave());
|
||||||
|
json.addProperty("octave_count", noise.amplitudes().size());
|
||||||
|
json.add("amplitude_modifiers", modifiers);
|
||||||
|
json.addProperty("base_amplitude", 1.0);
|
||||||
|
json.addProperty("normalize", false);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
}
|
||||||
+242
@@ -0,0 +1,242 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.noise.VanillaNoiseModel;
|
||||||
|
import net.minecraft.core.HolderGetter;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.PositionalRandomFactory;
|
||||||
|
import net.minecraft.world.level.levelgen.RandomState;
|
||||||
|
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
|
||||||
|
import net.minecraft.world.level.levelgen.synth.Noise;
|
||||||
|
import net.minecraft.world.level.levelgen.synth.NormalNoise;
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.SplittableRandom;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class VanillaNoiseModelParity263Test {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
|
private static final int POINTS = 4096;
|
||||||
|
private static final double TOLERANCE = 1.0E-5D;
|
||||||
|
private static final double RUNTIME_TOLERANCE = 1.0E-4D;
|
||||||
|
private static final List<Long> WORLD_SEEDS = List.of(0L, 8_675_309L, -4_611_686_018_427_387_904L);
|
||||||
|
private static final List<ParityNoise> NOISES = List.of(
|
||||||
|
ParityNoise.mapped("iris_export:parity/single", -7, List.of(1.0D)),
|
||||||
|
ParityNoise.mapped("iris_export:parity/fbm", -9, List.of(1.0D, 1.0D, 0.5D, 0.25D)),
|
||||||
|
ParityNoise.mapped("iris_export:parity/sparse", -10, List.of(1.5D, 0.0D, 1.0D, 0.0D, 0.0D, 0.0D)),
|
||||||
|
ParityNoise.mapped("iris_export:parity/leading_zero", -6, List.of(0.0D, 0.0D, 1.0D, 2.0D)),
|
||||||
|
ParityNoise.mapped("iris_export:parity/positive", 2, List.of(1.0D, 0.5D)),
|
||||||
|
ParityNoise.profiled("iris_export:parity/enabled",
|
||||||
|
"{\"base_octave\":-10,\"octave_count\":6,\"amplitude_modifiers\":[1.5,0.0,1.0,0.0,0.0,0.0],"
|
||||||
|
+ "\"base_amplitude\":1.2453007926713473}",
|
||||||
|
new VanillaNoiseModel.OctaveProfile(-10, 6, List.of(1.5D, 0.0D, 1.0D, 0.0D, 0.0D, 0.0D),
|
||||||
|
1.2453007926713473D, VanillaNoiseModel.Normalization.ENABLED)),
|
||||||
|
ParityNoise.profiled("iris_export:parity/legacy",
|
||||||
|
"{\"base_octave\":-9,\"octave_count\":3,\"amplitude_modifiers\":[1.0,0.0,1.0],\"normalize\":\"legacy\"}",
|
||||||
|
new VanillaNoiseModel.OctaveProfile(-9, 3, List.of(1.0D, 0.0D, 1.0D), 1.0D,
|
||||||
|
VanillaNoiseModel.Normalization.LEGACY)),
|
||||||
|
ParityNoise.profiled("iris_export:parity/defaults", "{\"base_octave\":-5}",
|
||||||
|
new VanillaNoiseModel.OctaveProfile(-5, 1, List.of(), 1.0D, VanillaNoiseModel.Normalization.ENABLED)),
|
||||||
|
ParityNoise.profiled("iris_export:parity/scaled",
|
||||||
|
"{\"base_octave\":-6,\"octave_count\":2,\"normalize\":false,\"base_amplitude\":0.5}",
|
||||||
|
new VanillaNoiseModel.OctaveProfile(-6, 2, List.of(), 0.5D, VanillaNoiseModel.Normalization.DISABLED)));
|
||||||
|
|
||||||
|
private static HeadlessNativeRegistries registries;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void loadNoiseDefinitions() throws IOException {
|
||||||
|
Map<String, String> definitions = new LinkedHashMap<>();
|
||||||
|
for (ParityNoise noise : NOISES) {
|
||||||
|
definitions.put(resourcePath(noise.id()), noise.json());
|
||||||
|
}
|
||||||
|
registries = HeadlessNativeRegistries.load(definitions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void closeRegistries() throws IOException {
|
||||||
|
if (registries != null) {
|
||||||
|
registries.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void modelMatchesNoiseBuiltFromLoadedDefinitions() {
|
||||||
|
List<String> report = new ArrayList<>();
|
||||||
|
for (long seed : WORLD_SEEDS) {
|
||||||
|
PositionalRandomFactory world = new VanillaPrecisionRandom(new XoroshiroRandomSource(seed)).forkPositional();
|
||||||
|
for (ParityNoise noise : NOISES) {
|
||||||
|
NormalNoise definition = registries.registries().lookupOrThrow(Registries.NOISE).getOrThrow(key(noise.id())).value();
|
||||||
|
Noise real = definition.create(world.fromHashOf(Identifier.parse(noise.id())));
|
||||||
|
report.add(noise.id() + "@" + seed + "=" + assertParity(noise, seed, real, TOLERANCE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println("26.3 vanilla-precision parity " + report);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void modelMatchesRuntimeRandomStateNoise() {
|
||||||
|
List<String> report = new ArrayList<>();
|
||||||
|
for (long seed : WORLD_SEEDS) {
|
||||||
|
RandomState state = randomState(seed);
|
||||||
|
for (ParityNoise noise : NOISES) {
|
||||||
|
Noise real = state.getOrCreateNoise(key(noise.id()));
|
||||||
|
report.add(noise.id() + "@" + seed + "=" + assertParity(noise, seed, real, RUNTIME_TOLERANCE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println("26.3 runtime parity " + report);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double assertParity(ParityNoise noise, long seed, Noise real, double tolerance) {
|
||||||
|
VanillaNoiseModel model = noise.model(seed);
|
||||||
|
SplittableRandom random = new SplittableRandom(seed ^ noise.id().hashCode());
|
||||||
|
double worst = 0.0D;
|
||||||
|
double peak = 0.0D;
|
||||||
|
for (int index = 0; index < POINTS; index++) {
|
||||||
|
double range = random.nextInt(4) == 0 ? 30_000_000.0D : 100_000.0D;
|
||||||
|
double x = random.nextDouble(-range, range);
|
||||||
|
double y = random.nextDouble(-2_048.0D, 2_048.0D);
|
||||||
|
double z = random.nextDouble(-range, range);
|
||||||
|
double expected = real.get(x, y, z);
|
||||||
|
worst = Math.max(worst, Math.abs(expected - model.sample(x, y, z)));
|
||||||
|
peak = Math.max(peak, Math.abs(expected));
|
||||||
|
}
|
||||||
|
assertTrue(noise.id() + " seed " + seed + " differs by " + worst, worst <= tolerance);
|
||||||
|
assertTrue(noise.id() + " seed " + seed + " is flat", peak > 0.05D);
|
||||||
|
return worst;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RandomState randomState(long seed) {
|
||||||
|
HolderGetter<NormalNoise> noises = registries.registries().lookupOrThrow(Registries.NOISE);
|
||||||
|
NoiseGeneratorSettings settings = registries.registries().lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||||
|
.getOrThrow(NoiseGeneratorSettings.OVERWORLD).value();
|
||||||
|
return RandomState.create(noises, seed, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ResourceKey<NormalNoise> key(String id) {
|
||||||
|
return ResourceKey.create(Registries.NOISE, Identifier.parse(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String resourcePath(String id) {
|
||||||
|
Identifier identifier = Identifier.parse(id);
|
||||||
|
return "data/" + identifier.getNamespace() + "/worldgen/noise/" + identifier.getPath() + ".json";
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ParityNoise(String id, String json, int firstOctave, List<Double> amplitudes,
|
||||||
|
VanillaNoiseModel.OctaveProfile profile) {
|
||||||
|
private static ParityNoise mapped(String id, int firstOctave, List<Double> amplitudes) {
|
||||||
|
String modifiers = amplitudes.stream().map(String::valueOf).collect(Collectors.joining(","));
|
||||||
|
String json = "{\"base_octave\":" + firstOctave + ",\"octave_count\":" + amplitudes.size()
|
||||||
|
+ ",\"amplitude_modifiers\":[" + modifiers + "],\"base_amplitude\":1.0,\"normalize\":false}";
|
||||||
|
return new ParityNoise(id, json, firstOctave, amplitudes, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ParityNoise profiled(String id, String json, VanillaNoiseModel.OctaveProfile profile) {
|
||||||
|
return new ParityNoise(id, json, 0, List.of(), profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
private VanillaNoiseModel model(long seed) {
|
||||||
|
if (profile == null) {
|
||||||
|
return VanillaNoiseModel.create(id, firstOctave, amplitudes, seed, DataVersion.V26_3);
|
||||||
|
}
|
||||||
|
return VanillaNoiseModel.create(id, profile, seed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class VanillaPrecisionRandom implements RandomSource {
|
||||||
|
private static final double DOUBLE_UNIT = 1.1102230246251565E-16D;
|
||||||
|
|
||||||
|
private final RandomSource source;
|
||||||
|
|
||||||
|
private VanillaPrecisionRandom(RandomSource source) {
|
||||||
|
this.source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RandomSource fork() {
|
||||||
|
return new VanillaPrecisionRandom(source.fork());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PositionalRandomFactory forkPositional() {
|
||||||
|
return new VanillaPrecisionPositional(source.forkPositional());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSeed(long seed) {
|
||||||
|
source.setSeed(seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int nextInt() {
|
||||||
|
return source.nextInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int nextInt(int bound) {
|
||||||
|
return source.nextInt(bound);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long nextLong() {
|
||||||
|
return source.nextLong();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean nextBoolean() {
|
||||||
|
return source.nextBoolean();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float nextFloat() {
|
||||||
|
return source.nextFloat();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double nextDouble() {
|
||||||
|
return (source.nextLong() >>> 11) * DOUBLE_UNIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double nextGaussian() {
|
||||||
|
return source.nextGaussian();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record VanillaPrecisionPositional(PositionalRandomFactory factory) implements PositionalRandomFactory {
|
||||||
|
@Override
|
||||||
|
public RandomSource at(int x, int y, int z) {
|
||||||
|
return new VanillaPrecisionRandom(factory.at(x, y, z));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RandomSource fromHashOf(String name) {
|
||||||
|
return new VanillaPrecisionRandom(factory.fromHashOf(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RandomSource fromSeed(long seed) {
|
||||||
|
return new VanillaPrecisionRandom(factory.fromSeed(seed));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void parityConfigString(StringBuilder builder) {
|
||||||
|
factory.parityConfigString(builder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+700
@@ -0,0 +1,700 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
|
import art.arcane.iris.pack.datapack.export.AtomicDatapackPublisher;
|
||||||
|
import art.arcane.iris.pack.datapack.export.DatapackExportFormat;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ExportIrFixtures;
|
||||||
|
import art.arcane.iris.pack.datapack.export.GenerationCost;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenDatapackExporter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportProgress;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportResult;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.PackCompiler;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.surface.MaterialProgramCompiler;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.terrain.FieldMacros;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.terrain.HeightCompiler;
|
||||||
|
import art.arcane.iris.pack.datapack.export.emit.NestingSplitter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.emit.WorldgenJsonWriter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportIr;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Field;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.FieldEvaluator;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.NoiseDef;
|
||||||
|
import art.arcane.iris.pack.datapack.export.lower.GenerationBudget;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.tags.WorldPresetTags;
|
||||||
|
import net.minecraft.util.Interval;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||||
|
import net.minecraft.world.level.dimension.LevelStem;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.DensityFunction;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.DfRewriteRule;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.op.BinaryFunction;
|
||||||
|
import net.minecraft.world.level.levelgen.material.rule.OreVeinRule;
|
||||||
|
import net.minecraft.world.level.levelgen.presets.WorldPreset;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.Assume;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.zip.GZIPInputStream;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipFile;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class WorldgenExport263CodecTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
|
@ClassRule
|
||||||
|
public static final TemporaryFolder overworldFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
private static final long SEED = 1337L;
|
||||||
|
private static final String DIMENSION_ID = ExportIrFixtures.NAMESPACE + ":" + ExportIrFixtures.DIMENSION;
|
||||||
|
private static final String HEIGHT_ID = ExportIrFixtures.NAMESPACE + ":" + ExportIrFixtures.DIMENSION + "/height";
|
||||||
|
private static final double SURFACE_TOLERANCE = 2.0;
|
||||||
|
private static final String BIOME_PREFIX = "data/" + ExportIrFixtures.NAMESPACE + "/worldgen/biome/";
|
||||||
|
private static final int[] SAMPLE_Y = {-64, 0, 64, 200};
|
||||||
|
private static final int LATTICE = 16;
|
||||||
|
private static final int LATTICE_STEP = 61;
|
||||||
|
private static final int HEIGHT_POINTS = 8;
|
||||||
|
private static final int HEIGHT_STEP = 97;
|
||||||
|
private static final String PROBE_PACK = "iris.exportProbePack";
|
||||||
|
private static final String OVERWORLD = "overworld";
|
||||||
|
private static final String OVERWORLD_ID = WorldgenExportRequest.DEFAULT_NAMESPACE + ":" + OVERWORLD;
|
||||||
|
private static final String OVERWORLD_BIOME_PREFIX = "data/" + WorldgenExportRequest.DEFAULT_NAMESPACE + "/worldgen/biome/";
|
||||||
|
private static final String LOG_PREFIX = "[worldgen-export]";
|
||||||
|
private static final int OVERWORLD_LATTICE = 4;
|
||||||
|
private static final int OVERWORLD_STEP = 509;
|
||||||
|
private static final int MAX_NESTING = 255;
|
||||||
|
private static final String CAVE_FIELD = "cave/field";
|
||||||
|
private static final String WALL_FALLBACK = "uses the nearest kept palette (minecraft:";
|
||||||
|
private static final double WALL_SHELL_BAND_LIMIT = 0.25;
|
||||||
|
private static final String DENSITY_FUNCTION_RESOURCE = "worldgen/density_function";
|
||||||
|
private static final double MEGABYTE = 1024.0 * 1024.0;
|
||||||
|
private static final int OVERWORLD_LAVA_Y = -248;
|
||||||
|
private static final int LAVA_LINE = -54;
|
||||||
|
private static final int DEEP_CHUNKS = 2;
|
||||||
|
private static final int DEEP_STRIDE = 4;
|
||||||
|
private static final int CHUNK_WIDTH = 16;
|
||||||
|
private static final float OPEN_MARGIN = 1.0e-3f;
|
||||||
|
private static final int LAVA_SEARCH_RADIUS = 1024;
|
||||||
|
private static final int LAVA_SEARCH_STEP = 16;
|
||||||
|
private static final int LAVA_CELLS = 4;
|
||||||
|
private static final int AGREEMENT_POINTS = 16;
|
||||||
|
private static final double AGREEMENT_TOLERANCE = 1.0;
|
||||||
|
private static final int AGREEMENT_OUTLIERS = 2;
|
||||||
|
private static final String SUBTERRAIN_STRUCTURES = "data/" + WorldgenExportRequest.DEFAULT_NAMESPACE + "/worldgen/structure/subterrain/";
|
||||||
|
private static final String SUBTERRAIN_TEMPLATES = "data/" + WorldgenExportRequest.DEFAULT_NAMESPACE + "/structure/subterrain/";
|
||||||
|
private static final int ROOM_GRID = 24;
|
||||||
|
private static final int ROOM_STEP_CHUNKS = 8;
|
||||||
|
private static final int ROOM_ATTEMPTS = 4;
|
||||||
|
private static final int ROOM_REACH = 128;
|
||||||
|
private static final Map<WorldgenExportMode, SortedMap<String, byte[]>> FILES = new EnumMap<>(WorldgenExportMode.class);
|
||||||
|
private static final Map<WorldgenExportMode, WorldgenExportSampler> SAMPLERS = new EnumMap<>(WorldgenExportMode.class);
|
||||||
|
private static SortedMap<String, byte[]> exportedOverworld;
|
||||||
|
private static WorldgenExportResult exportedOverworldResult;
|
||||||
|
private static ExportIr exportedOverworldIr;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void loadExports() throws IOException {
|
||||||
|
for (WorldgenExportMode mode : WorldgenExportMode.values()) {
|
||||||
|
SortedMap<String, byte[]> files = WorldgenJsonWriter.forTarget(DataVersion.V26_3).write(ExportIrFixtures.flatNoise(DataVersion.V26_3, mode));
|
||||||
|
FILES.put(mode, files);
|
||||||
|
SAMPLERS.put(mode, WorldgenExportSampler.open(files, mode, DIMENSION_ID, SEED));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void closeExports() throws IOException {
|
||||||
|
for (WorldgenExportSampler sampler : SAMPLERS.values()) {
|
||||||
|
sampler.close();
|
||||||
|
}
|
||||||
|
SAMPLERS.clear();
|
||||||
|
FILES.clear();
|
||||||
|
exportedOverworld = null;
|
||||||
|
exportedOverworldResult = null;
|
||||||
|
exportedOverworldIr = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void fixtureLoadsInEveryMode() {
|
||||||
|
for (WorldgenExportMode mode : WorldgenExportMode.values()) {
|
||||||
|
WorldgenExportSampler sampler = SAMPLERS.get(mode);
|
||||||
|
assertEquals(ExportIrFixtures.MIN_Y, sampler.minY());
|
||||||
|
assertEquals(ExportIrFixtures.HEIGHT, sampler.height());
|
||||||
|
assertEquals(ExportIrFixtures.SEA_LEVEL, sampler.seaLevel());
|
||||||
|
assertFalse(sampler.densityFunctionIds().isEmpty());
|
||||||
|
for (String id : sampler.densityFunctionIds()) {
|
||||||
|
assertTrue(mode + " " + id, sampler.registries().lookupOrThrow(Registries.DENSITY_FUNCTION).containsKey(Identifier.parse(id)));
|
||||||
|
}
|
||||||
|
assertTrue(sampler.registries().lookupOrThrow(Registries.NOISE_SETTINGS).containsKey(Identifier.parse(DIMENSION_ID)));
|
||||||
|
assertTrue(sampler.registries().lookupOrThrow(Registries.DIMENSION_TYPE).containsKey(Identifier.parse(DIMENSION_ID)));
|
||||||
|
assertTrue(sampler.registries().lookupOrThrow(Registries.MATERIAL_RULE).containsKey(Identifier.parse(DIMENSION_ID)));
|
||||||
|
boolean preset = sampler.registries().lookupOrThrow(Registries.WORLD_PRESET).containsKey(Identifier.parse(DIMENSION_ID));
|
||||||
|
boolean dimension = sampler.registries().lookupOrThrow(Registries.LEVEL_STEM).containsKey(Identifier.parse(DIMENSION_ID));
|
||||||
|
boolean overworld = sampler.registries().lookupOrThrow(Registries.LEVEL_STEM).containsKey(LevelStem.OVERWORLD.identifier());
|
||||||
|
assertEquals(mode.toString(), mode == WorldgenExportMode.PRESET, preset);
|
||||||
|
assertEquals(mode.toString(), mode == WorldgenExportMode.DIMENSION, dimension);
|
||||||
|
assertEquals(mode.toString(), mode == WorldgenExportMode.OVERWORLD, overworld);
|
||||||
|
}
|
||||||
|
WorldgenExportSampler presetSampler = SAMPLERS.get(WorldgenExportMode.PRESET);
|
||||||
|
Holder<WorldPreset> preset = presetSampler.registries().lookupOrThrow(Registries.WORLD_PRESET)
|
||||||
|
.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, Identifier.parse(DIMENSION_ID)));
|
||||||
|
assertTrue(preset.value().createWorldDimensions().get(LevelStem.NETHER).isPresent());
|
||||||
|
assertTrue(preset.value().createWorldDimensions().get(LevelStem.END).isPresent());
|
||||||
|
HolderSet.Named<WorldPreset> normal = presetSampler.registries().lookupOrThrow(Registries.WORLD_PRESET).getOrThrow(WorldPresetTags.NORMAL);
|
||||||
|
assertTrue(normal.contains(preset));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void everyDensityFunctionEvaluatesFinite() {
|
||||||
|
for (WorldgenExportMode mode : WorldgenExportMode.values()) {
|
||||||
|
WorldgenExportSampler sampler = SAMPLERS.get(mode);
|
||||||
|
for (String id : sampler.densityFunctionIds()) {
|
||||||
|
for (int y : SAMPLE_Y) {
|
||||||
|
for (int xIndex = 0; xIndex < LATTICE; xIndex++) {
|
||||||
|
for (int zIndex = 0; zIndex < LATTICE; zIndex++) {
|
||||||
|
int x = (xIndex - LATTICE / 2) * LATTICE_STEP;
|
||||||
|
int z = (zIndex - LATTICE / 2) * LATTICE_STEP;
|
||||||
|
float value = sampler.density(id, x, y, z);
|
||||||
|
assertTrue(mode + " " + id + " at " + x + "," + y + "," + z + " = " + value, Float.isFinite(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void baseHeightWithinDimension() {
|
||||||
|
for (WorldgenExportMode mode : WorldgenExportMode.values()) {
|
||||||
|
WorldgenExportSampler sampler = SAMPLERS.get(mode);
|
||||||
|
Set<Integer> heights = new HashSet<>();
|
||||||
|
int land = 0;
|
||||||
|
for (int xIndex = 0; xIndex < HEIGHT_POINTS; xIndex++) {
|
||||||
|
for (int zIndex = 0; zIndex < HEIGHT_POINTS; zIndex++) {
|
||||||
|
int x = (xIndex - HEIGHT_POINTS / 2) * HEIGHT_STEP;
|
||||||
|
int z = (zIndex - HEIGHT_POINTS / 2) * HEIGHT_STEP;
|
||||||
|
int height = sampler.baseHeight(x, z);
|
||||||
|
assertTrue(mode + " height " + height + " at " + x + "," + z,
|
||||||
|
height >= sampler.minY() && height <= sampler.minY() + sampler.height());
|
||||||
|
float surface = sampler.density(HEIGHT_ID, x, 0, z);
|
||||||
|
int floor = sampler.floorHeight(x, z);
|
||||||
|
assertTrue(mode + " height " + height + " below floor " + floor, height >= floor);
|
||||||
|
if (surface > sampler.seaLevel() + 1) {
|
||||||
|
assertTrue(mode + " floor " + floor + " against surface " + surface, Math.abs(floor - surface) <= SURFACE_TOLERANCE);
|
||||||
|
land++;
|
||||||
|
}
|
||||||
|
heights.add(height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(mode + " surface never varies", heights.size() > 1);
|
||||||
|
assertTrue(mode + " has no land column above sea level", land > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resolverReturnsOnlyExportedBiomes() {
|
||||||
|
for (WorldgenExportMode mode : WorldgenExportMode.values()) {
|
||||||
|
WorldgenExportSampler sampler = SAMPLERS.get(mode);
|
||||||
|
Set<String> exported = exportedBiomes(FILES.get(mode), BIOME_PREFIX, ExportIrFixtures.NAMESPACE);
|
||||||
|
assertFalse(exported.isEmpty());
|
||||||
|
assertTrue(mode + " " + sampler.possibleBiomes(), exported.containsAll(sampler.possibleBiomes()));
|
||||||
|
for (int y : SAMPLE_Y) {
|
||||||
|
for (int xIndex = 0; xIndex < LATTICE; xIndex++) {
|
||||||
|
for (int zIndex = 0; zIndex < LATTICE; zIndex++) {
|
||||||
|
String biome = sampler.biome((xIndex - LATTICE / 2) * LATTICE_STEP, y, (zIndex - LATTICE / 2) * LATTICE_STEP);
|
||||||
|
assertTrue(mode + " " + biome, exported.contains(biome));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void overworldPackLoads() throws Exception {
|
||||||
|
SortedMap<String, byte[]> files = overworldFiles();
|
||||||
|
assertGenerationBudget(exportedOverworldResult);
|
||||||
|
assertWallPalettesFallBack(exportedOverworldResult);
|
||||||
|
for (Map.Entry<String, byte[]> file : files.entrySet()) {
|
||||||
|
if (file.getKey().endsWith(".json")) {
|
||||||
|
int depth = NestingSplitter.depth(JsonParser.parseString(new String(file.getValue(), StandardCharsets.UTF_8)));
|
||||||
|
assertTrue(file.getKey() + " nests " + depth, depth <= MAX_NESTING);
|
||||||
|
}
|
||||||
|
if (file.getKey().endsWith(".nbt")) {
|
||||||
|
assertCompleteGzip(file.getKey(), file.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try (WorldgenExportSampler sampler = WorldgenExportSampler.open(files, WorldgenExportMode.OVERWORLD, OVERWORLD_ID, SEED)) {
|
||||||
|
Set<String> roots = new TreeSet<>();
|
||||||
|
for (String id : sampler.densityFunctionIds()) {
|
||||||
|
if (id.startsWith(OVERWORLD_ID + "/")) {
|
||||||
|
roots.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(roots.toString(), roots.contains(OVERWORLD_ID + "/final_density"));
|
||||||
|
for (String id : roots) {
|
||||||
|
for (int y : SAMPLE_Y) {
|
||||||
|
for (int xIndex = 0; xIndex < OVERWORLD_LATTICE; xIndex++) {
|
||||||
|
for (int zIndex = 0; zIndex < OVERWORLD_LATTICE; zIndex++) {
|
||||||
|
int x = (xIndex - OVERWORLD_LATTICE / 2) * OVERWORLD_STEP;
|
||||||
|
int z = (zIndex - OVERWORLD_LATTICE / 2) * OVERWORLD_STEP;
|
||||||
|
float value = sampler.density(id, x, y, z);
|
||||||
|
assertTrue(id + " at " + x + "," + y + "," + z + " = " + value, Float.isFinite(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertNaturalHeightMatchesTheEvaluator(exportedOverworldIr, sampler);
|
||||||
|
Set<String> exported = exportedBiomes(files, OVERWORLD_BIOME_PREFIX, WorldgenExportRequest.DEFAULT_NAMESPACE);
|
||||||
|
assertFalse(exported.isEmpty());
|
||||||
|
assertTrue(sampler.possibleBiomes().toString(), exported.containsAll(sampler.possibleBiomes()));
|
||||||
|
for (int xIndex = 0; xIndex < HEIGHT_POINTS; xIndex++) {
|
||||||
|
for (int zIndex = 0; zIndex < HEIGHT_POINTS; zIndex++) {
|
||||||
|
int x = (xIndex - HEIGHT_POINTS / 2) * HEIGHT_STEP;
|
||||||
|
int z = (zIndex - HEIGHT_POINTS / 2) * HEIGHT_STEP;
|
||||||
|
int height = sampler.baseHeight(x, z);
|
||||||
|
assertTrue("height " + height + " at " + x + "," + z, height >= sampler.minY() && height <= sampler.minY() + sampler.height());
|
||||||
|
for (int y : SAMPLE_Y) {
|
||||||
|
String biome = sampler.biome(x, y, z);
|
||||||
|
assertTrue(biome + " at " + x + "," + y + "," + z, exported.contains(biome));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void overworldHasNoDisjointMinMax() throws Exception {
|
||||||
|
SortedMap<String, byte[]> files = overworldFiles();
|
||||||
|
try (WorldgenExportSampler sampler = WorldgenExportSampler.open(files, WorldgenExportMode.OVERWORLD, OVERWORLD_ID, SEED)) {
|
||||||
|
MinMaxAudit audit = new MinMaxAudit();
|
||||||
|
for (String id : sampler.densityFunctionIds()) {
|
||||||
|
audit.rewrite(sampler.registries().lookupOrThrow(Registries.DENSITY_FUNCTION)
|
||||||
|
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, Identifier.parse(id))).value());
|
||||||
|
}
|
||||||
|
System.out.println(LOG_PREFIX + " min/max nodes " + audit.checked() + ", disjoint " + audit.disjoint().size());
|
||||||
|
assertTrue(audit.checked() > 0);
|
||||||
|
assertTrue(audit.disjoint().size() + " disjoint min/max nodes: " + audit.disjoint(), audit.disjoint().isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void overworldDeepCavesOpenWithoutLava() throws Exception {
|
||||||
|
SortedMap<String, byte[]> files = overworldFiles();
|
||||||
|
String field = WorldgenExportRequest.DEFAULT_NAMESPACE + ":" + CAVE_FIELD;
|
||||||
|
long open = 0L;
|
||||||
|
long lava = 0L;
|
||||||
|
try (WorldgenExportSampler sampler = WorldgenExportSampler.open(files, WorldgenExportMode.OVERWORLD, OVERWORLD_ID, SEED)) {
|
||||||
|
assertTrue(field, sampler.densityFunctionIds().contains(field));
|
||||||
|
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
|
||||||
|
for (int chunkX = -DEEP_CHUNKS; chunkX < DEEP_CHUNKS; chunkX++) {
|
||||||
|
for (int chunkZ = -DEEP_CHUNKS; chunkZ < DEEP_CHUNKS; chunkZ++) {
|
||||||
|
ProtoChunk chunk = sampler.terrainChunk(chunkX, chunkZ);
|
||||||
|
for (int x = 0; x < CHUNK_WIDTH; x++) {
|
||||||
|
for (int z = 0; z < CHUNK_WIDTH; z++) {
|
||||||
|
for (int y = OVERWORLD_LAVA_Y + 1; y < LAVA_LINE; y++) {
|
||||||
|
BlockState state = chunk.getBlockState(position.set(x, y, z));
|
||||||
|
assertFalse(state + " at chunk " + chunkX + "," + chunkZ + " " + x + "," + y + "," + z, state.is(Blocks.LAVA));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int x = 0; x < CHUNK_WIDTH; x += DEEP_STRIDE) {
|
||||||
|
for (int z = 0; z < CHUNK_WIDTH; z += DEEP_STRIDE) {
|
||||||
|
int blockX = (chunkX << 4) + x;
|
||||||
|
int blockZ = (chunkZ << 4) + z;
|
||||||
|
for (int y = OVERWORLD_LAVA_Y + 1; y < LAVA_LINE; y++) {
|
||||||
|
if (sampler.density(field, blockX, y, blockZ) > -OPEN_MARGIN) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BlockState state = chunk.getBlockState(position.set(x, y, z));
|
||||||
|
assertTrue(state + " at " + blockX + "," + y + "," + blockZ, state.isAir());
|
||||||
|
open++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (BlockPos cell : openLavaBandCells(sampler, field)) {
|
||||||
|
ProtoChunk chunk = sampler.terrainChunk(cell.getX() >> 4, cell.getZ() >> 4);
|
||||||
|
BlockState state = chunk.getBlockState(position.set(cell.getX() & (CHUNK_WIDTH - 1), cell.getY(), cell.getZ() & (CHUNK_WIDTH - 1)));
|
||||||
|
assertTrue(state + " at " + cell, state.is(Blocks.LAVA));
|
||||||
|
lava++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println(LOG_PREFIX + " deep cave cells open " + open + ", lava band cells " + lava);
|
||||||
|
assertTrue("no deep cave cell opened", open > 0L);
|
||||||
|
assertTrue("no lava band cell opened", lava > 0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void overworldMaterialPassComputesTheCaveFieldOnce() throws Exception {
|
||||||
|
SortedMap<String, byte[]> files = overworldFiles();
|
||||||
|
try (WorldgenExportSampler sampler = WorldgenExportSampler.open(files, WorldgenExportMode.OVERWORLD, OVERWORLD_ID, SEED)) {
|
||||||
|
CompiledCaches caches = CompiledCaches.open(sampler.registries(), SEED);
|
||||||
|
int field = caches.rootId(caches.function(WorldgenExportRequest.DEFAULT_NAMESPACE + ":" + CAVE_FIELD));
|
||||||
|
List<OreVeinRule> veins = caches.oreVeins();
|
||||||
|
DensityFunction opening = veins.getFirst().density();
|
||||||
|
assertTrue(caches.ids(opening).contains(field));
|
||||||
|
assertTrue(caches.ids(caches.finalDensity()).contains(field));
|
||||||
|
Set<Integer> caveCaches = caches.ids(caches.function(WorldgenExportRequest.DEFAULT_NAMESPACE + ":" + CAVE_FIELD));
|
||||||
|
int shells = 0;
|
||||||
|
for (OreVeinRule vein : veins) {
|
||||||
|
if (!vein.oreBlock().isAir()) {
|
||||||
|
assertTrue("wall shell " + shells + " " + vein.oreBlock(), caches.ids(vein.density()).contains(field));
|
||||||
|
assertTrue("wall shell " + shells + " " + vein.oreBlock(), caveCaches.containsAll(caches.ids(vein.density())));
|
||||||
|
shells++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JsonObject shell = JsonParser.parseString(new String(files.get("data/" + WorldgenExportRequest.DEFAULT_NAMESPACE + "/" + DENSITY_FUNCTION_RESOURCE
|
||||||
|
+ "/" + MaterialProgramCompiler.WALL_SHELL + ".json"), StandardCharsets.UTF_8)).getAsJsonObject();
|
||||||
|
double band = shell.get("max_exclusive").getAsDouble();
|
||||||
|
System.out.println(LOG_PREFIX + " material pass: " + veins.size() + " ore vein rules, " + shells + " wall shells read cave field cache " + field
|
||||||
|
+ " within band " + band);
|
||||||
|
assertTrue(String.valueOf(shells), shells > 0);
|
||||||
|
assertTrue(String.valueOf(band), band > 0.0 && band < WALL_SHELL_BAND_LIMIT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void overworldSubterrainRoomsPlace() throws Exception {
|
||||||
|
SortedMap<String, byte[]> files = overworldFiles();
|
||||||
|
SortedMap<String, JsonObject> structures = new TreeMap<>();
|
||||||
|
for (Map.Entry<String, byte[]> file : files.entrySet()) {
|
||||||
|
if (file.getKey().startsWith(SUBTERRAIN_STRUCTURES) && file.getKey().endsWith(".json")) {
|
||||||
|
String path = file.getKey().substring(SUBTERRAIN_STRUCTURES.length(), file.getKey().length() - ".json".length());
|
||||||
|
structures.put(WorldgenExportRequest.DEFAULT_NAMESPACE + ":subterrain/" + path,
|
||||||
|
JsonParser.parseString(new String(file.getValue(), StandardCharsets.UTF_8)).getAsJsonObject());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
long templateBytes = 0L;
|
||||||
|
for (Map.Entry<String, byte[]> file : files.entrySet()) {
|
||||||
|
if (file.getKey().startsWith(SUBTERRAIN_TEMPLATES)) {
|
||||||
|
templateBytes += file.getValue().length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println(LOG_PREFIX + " subterrain structures " + structures.size() + ", structure sets "
|
||||||
|
+ count(files, "/worldgen/structure_set/subterrain/", ".json") + ", templates " + count(files, "/structure/subterrain/", ".nbt")
|
||||||
|
+ ", template bytes " + templateBytes);
|
||||||
|
assertFalse(structures.isEmpty());
|
||||||
|
try (WorldgenExportSampler sampler = WorldgenExportSampler.open(files, WorldgenExportMode.OVERWORLD, OVERWORLD_ID, SEED)) {
|
||||||
|
for (Map.Entry<String, JsonObject> entry : structures.entrySet()) {
|
||||||
|
Structure structure = sampler.registries().lookupOrThrow(Registries.STRUCTURE)
|
||||||
|
.getOrThrow(ResourceKey.create(Registries.STRUCTURE, Identifier.parse(entry.getKey()))).value();
|
||||||
|
JsonObject height = entry.getValue().getAsJsonObject("start_height");
|
||||||
|
int y = (height.getAsJsonObject("min_inclusive").get("absolute").getAsInt()
|
||||||
|
+ height.getAsJsonObject("max_inclusive").get("absolute").getAsInt()) / 2;
|
||||||
|
List<ChunkPos> matches = new ArrayList<>();
|
||||||
|
for (int xIndex = 0; xIndex < ROOM_GRID; xIndex++) {
|
||||||
|
for (int zIndex = 0; zIndex < ROOM_GRID; zIndex++) {
|
||||||
|
ChunkPos chunk = new ChunkPos((xIndex - ROOM_GRID / 2) * ROOM_STEP_CHUNKS, (zIndex - ROOM_GRID / 2) * ROOM_STEP_CHUNKS);
|
||||||
|
String biome = sampler.biome(chunk.getMinBlockX(), y, chunk.getMinBlockZ());
|
||||||
|
Holder<Biome> holder = sampler.registries().lookupOrThrow(Registries.BIOME)
|
||||||
|
.getOrThrow(ResourceKey.create(Registries.BIOME, Identifier.parse(biome)));
|
||||||
|
if (structure.biomes().contains(holder)) {
|
||||||
|
matches.add(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println(String.format(Locale.ROOT, "%s subterrain %s biome match %d/%d at y %d", LOG_PREFIX, entry.getKey(),
|
||||||
|
matches.size(), ROOM_GRID * ROOM_GRID, y));
|
||||||
|
assertFalse(entry.getKey() + " finds no biome at y " + y, matches.isEmpty());
|
||||||
|
StructureStart start = null;
|
||||||
|
for (ChunkPos chunk : matches.subList(0, Math.min(ROOM_ATTEMPTS, matches.size()))) {
|
||||||
|
StructureStart candidate = sampler.structureStart(entry.getKey(), chunk.x(), chunk.z());
|
||||||
|
if (candidate.isValid()) {
|
||||||
|
start = candidate;
|
||||||
|
BoundingBox box = start.getBoundingBox();
|
||||||
|
assertTrue(entry.getKey() + " " + box, chunk.getMinBlockX() - box.minX() <= ROOM_REACH && box.maxX() - chunk.getMinBlockX() <= ROOM_REACH);
|
||||||
|
assertTrue(entry.getKey() + " " + box, chunk.getMinBlockZ() - box.minZ() <= ROOM_REACH && box.maxZ() - chunk.getMinBlockZ() <= ROOM_REACH);
|
||||||
|
assertTrue(entry.getKey() + " " + box, box.minY() >= sampler.minY() && box.maxY() < sampler.minY() + sampler.height());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(entry.getKey() + " never starts", start != null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<BlockPos> openLavaBandCells(WorldgenExportSampler sampler, String field) {
|
||||||
|
List<BlockPos> cells = new ArrayList<>(LAVA_CELLS);
|
||||||
|
for (int x = -LAVA_SEARCH_RADIUS; x < LAVA_SEARCH_RADIUS; x += LAVA_SEARCH_STEP) {
|
||||||
|
for (int z = -LAVA_SEARCH_RADIUS; z < LAVA_SEARCH_RADIUS; z += LAVA_SEARCH_STEP) {
|
||||||
|
for (int y = sampler.minY(); y <= OVERWORLD_LAVA_Y; y++) {
|
||||||
|
if (sampler.density(field, x, y, z) > -OPEN_MARGIN) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cells.add(new BlockPos(x, y, z));
|
||||||
|
if (cells.size() == LAVA_CELLS) {
|
||||||
|
return cells;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cells;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static synchronized SortedMap<String, byte[]> overworldFiles() throws Exception {
|
||||||
|
String probePack = System.getProperty(PROBE_PACK);
|
||||||
|
Assume.assumeTrue("Set -P" + PROBE_PACK + " to export a real pack", probePack != null && !probePack.isBlank());
|
||||||
|
if (exportedOverworld != null) {
|
||||||
|
return exportedOverworld;
|
||||||
|
}
|
||||||
|
Path root = overworldFolder.newFolder("exports").toPath();
|
||||||
|
SortedMap<String, byte[]> untrimmed = new TreeMap<>();
|
||||||
|
AtomicReference<ExportIr> compiled = new AtomicReference<>();
|
||||||
|
WorldgenExportResult result;
|
||||||
|
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(new RealPackProbeSupport.WorkspaceOptions(
|
||||||
|
new File(probePack), OVERWORLD, LOG_PREFIX, new HeadlessNativeRuntime(), overworldFolder.newFolder("workspace").toPath()))) {
|
||||||
|
WorldgenExportRequest request = WorldgenExportRequest.builder()
|
||||||
|
.pack(workspace.pack().toPath())
|
||||||
|
.dimension(OVERWORLD)
|
||||||
|
.exportRoot(root)
|
||||||
|
.objects(true)
|
||||||
|
.build();
|
||||||
|
result = new WorldgenDatapackExporter(target -> ir -> {
|
||||||
|
compiled.set(ir);
|
||||||
|
SortedMap<String, byte[]> written = WorldgenJsonWriter.forTarget(target).write(ir);
|
||||||
|
untrimmed.putAll(written);
|
||||||
|
return written;
|
||||||
|
}, PackCompiler.create()).export(request, WorldgenExportProgress.NONE);
|
||||||
|
}
|
||||||
|
assertTrue(result.message() + " " + result.diagnostics(), result.isSuccess());
|
||||||
|
SortedMap<String, byte[]> files = zipEntries(result.output());
|
||||||
|
Path untrimmedZip = root.resolve("untrimmed.zip");
|
||||||
|
new AtomicDatapackPublisher().publish(untrimmed, untrimmedZip, DatapackExportFormat.ZIP, false, ".untrimmed-");
|
||||||
|
report(result, untrimmed, Files.size(untrimmedZip), files);
|
||||||
|
exportedOverworld = files;
|
||||||
|
exportedOverworldResult = result;
|
||||||
|
exportedOverworldIr = compiled.get();
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertGenerationBudget(WorldgenExportResult result) {
|
||||||
|
GenerationCost cost = result.cost();
|
||||||
|
Set<String> flagged = new TreeSet<>();
|
||||||
|
for (ExportDiagnostic diagnostic : result.diagnostics()) {
|
||||||
|
if (diagnostic.limit() == ExportLimit.GENERATION_COST) {
|
||||||
|
flagged.add(diagnostic.resource());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (GenerationCost.Root root : GenerationCost.Root.values()) {
|
||||||
|
GenerationCost.RootCost rootCost = cost.root(root).orElseThrow();
|
||||||
|
System.out.println(String.format(Locale.ROOT, "%s cost %s worst case %.3e expanded %.3e distinct %d", LOG_PREFIX, root.id(),
|
||||||
|
rootCost.worstCaseNodes(), rootCost.expandedNodes(), rootCost.distinctNodes()));
|
||||||
|
}
|
||||||
|
for (GenerationCost.Root root : GenerationCost.Root.values()) {
|
||||||
|
if (!root.climate()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double limit = GenerationBudget.worstCaseLimit(root).orElseThrow();
|
||||||
|
double nodes = cost.root(root).orElseThrow().worstCaseNodes();
|
||||||
|
String resource = "noise_settings/" + OVERWORLD + "/" + root.id();
|
||||||
|
assertTrue(root.id() + " evaluates " + nodes + " nodes, budget " + limit, nodes <= limit);
|
||||||
|
assertFalse(resource + " " + flagged, flagged.contains(resource));
|
||||||
|
}
|
||||||
|
long finalDensity = cost.root(GenerationCost.Root.FINAL_DENSITY).orElseThrow().distinctNodes();
|
||||||
|
String finalResource = "noise_settings/" + OVERWORLD + "/" + GenerationCost.Root.FINAL_DENSITY.id();
|
||||||
|
System.out.println(String.format(Locale.ROOT, "%s final_density distinct %d of %d, density function JSON %.1f of %.1f MB", LOG_PREFIX, finalDensity,
|
||||||
|
GenerationBudget.FINAL_DENSITY_DISTINCT_NODES, cost.densityFunctionBytes() / MEGABYTE, GenerationBudget.DENSITY_FUNCTION_JSON_BYTES / MEGABYTE));
|
||||||
|
assertEquals(finalResource + " " + flagged, finalDensity > GenerationBudget.FINAL_DENSITY_DISTINCT_NODES, flagged.contains(finalResource));
|
||||||
|
assertTrue(cost.densityFunctionBytes() + " bytes", cost.densityFunctionBytes() > 0L);
|
||||||
|
assertTrue(cost.densityFunctionBytes() + " bytes", cost.densityFunctionBytes() <= GenerationBudget.DENSITY_FUNCTION_JSON_BYTES);
|
||||||
|
assertFalse(flagged.toString(), flagged.contains(DENSITY_FUNCTION_RESOURCE));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertWallPalettesFallBack(WorldgenExportResult result) {
|
||||||
|
int fallbacks = 0;
|
||||||
|
for (ExportDiagnostic diagnostic : result.diagnostics()) {
|
||||||
|
if (diagnostic.limit() == ExportLimit.WALL_PALETTE && diagnostic.severity() == ExportDiagnostic.Severity.WARNING) {
|
||||||
|
assertTrue(diagnostic.resource() + " " + diagnostic.message(), diagnostic.message().contains(WALL_FALLBACK));
|
||||||
|
fallbacks++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println(LOG_PREFIX + " wall palettes over the ore vein budget " + fallbacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertNaturalHeightMatchesTheEvaluator(ExportIr ir, WorldgenExportSampler sampler) {
|
||||||
|
Field.Shared height = shared(ir.router().chunkSurfaceLevel(), HeightCompiler.HEIGHT);
|
||||||
|
Map<String, NoiseDef> noises = new HashMap<>();
|
||||||
|
for (NoiseDef noise : ir.noises()) {
|
||||||
|
noises.put(ir.namespace() + ":" + noise.id(), noise);
|
||||||
|
}
|
||||||
|
String heightId = ir.namespace() + ":" + HeightCompiler.HEIGHT;
|
||||||
|
int outliers = 0;
|
||||||
|
StringBuilder detail = new StringBuilder();
|
||||||
|
for (int xIndex = 0; xIndex < AGREEMENT_POINTS; xIndex++) {
|
||||||
|
for (int zIndex = 0; zIndex < AGREEMENT_POINTS; zIndex++) {
|
||||||
|
int x = (xIndex - AGREEMENT_POINTS / 2) * OVERWORLD_STEP;
|
||||||
|
int z = (zIndex - AGREEMENT_POINTS / 2) * OVERWORLD_STEP;
|
||||||
|
double evaluated = FieldEvaluator.sample(height.expr(), noises, SEED, DataVersion.V26_3, x, 0, z);
|
||||||
|
double exported = sampler.density(heightId, x, 0, z);
|
||||||
|
if (Math.abs(evaluated - exported) > AGREEMENT_TOLERANCE) {
|
||||||
|
outliers++;
|
||||||
|
detail.append(' ').append(x).append(',').append(z).append('=').append(evaluated).append('/').append(exported);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(outliers + " columns differ:" + detail, outliers <= AGREEMENT_OUTLIERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Field.Shared shared(Field root, String name) {
|
||||||
|
Set<Field> visited = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||||
|
Deque<Field> pending = new ArrayDeque<>();
|
||||||
|
pending.push(root);
|
||||||
|
while (!pending.isEmpty()) {
|
||||||
|
Field field = pending.pop();
|
||||||
|
if (!visited.add(field)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (field instanceof Field.Shared candidate && candidate.name().equals(name)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
for (Field child : FieldMacros.children(field)) {
|
||||||
|
pending.push(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new AssertionError("Router has no shared field " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertCompleteGzip(String path, byte[] bytes) throws IOException {
|
||||||
|
try (InputStream input = new GZIPInputStream(new ByteArrayInputStream(bytes))) {
|
||||||
|
input.readAllBytes();
|
||||||
|
assertEquals(path, -1, input.read());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void report(WorldgenExportResult result, SortedMap<String, byte[]> untrimmed, long untrimmedZipBytes,
|
||||||
|
SortedMap<String, byte[]> files) throws IOException {
|
||||||
|
SortedMap<ExportLimit, Integer> limits = new TreeMap<>();
|
||||||
|
for (ExportDiagnostic diagnostic : result.diagnostics()) {
|
||||||
|
limits.merge(diagnostic.limit(), 1, Integer::sum);
|
||||||
|
}
|
||||||
|
System.out.println(LOG_PREFIX + " untrimmed zip bytes " + untrimmedZipBytes + ", files " + untrimmed.size()
|
||||||
|
+ ", trimmed zip bytes " + Files.size(result.output()) + ", files " + files.size() + ", trim " + result.trim());
|
||||||
|
System.out.println(LOG_PREFIX + " diagnostics " + result.diagnostics().size() + " " + limits);
|
||||||
|
reportCounts("untrimmed", untrimmed);
|
||||||
|
reportCounts("trimmed", files);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void reportCounts(String label, SortedMap<String, byte[]> files) {
|
||||||
|
System.out.println(LOG_PREFIX + " " + label + " templates " + count(files, "/structure/", ".nbt")
|
||||||
|
+ ", structures " + count(files, "/worldgen/structure/", ".json")
|
||||||
|
+ ", structure sets " + count(files, "/worldgen/structure_set/", ".json")
|
||||||
|
+ ", placed features " + count(files, "/worldgen/placed_feature/", ".json")
|
||||||
|
+ ", features " + count(files, "/worldgen/feature/", ".json")
|
||||||
|
+ ", density functions " + count(files, "/worldgen/density_function/", ".json")
|
||||||
|
+ ", noises " + count(files, "/worldgen/noise/", ".json")
|
||||||
|
+ ", biomes " + count(files, "/worldgen/biome/", ".json"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int count(SortedMap<String, byte[]> files, String folder, String suffix) {
|
||||||
|
int count = 0;
|
||||||
|
for (String path : files.keySet()) {
|
||||||
|
if (path.contains(folder) && path.endsWith(suffix)) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SortedMap<String, byte[]> zipEntries(Path zip) throws IOException {
|
||||||
|
SortedMap<String, byte[]> files = new TreeMap<>();
|
||||||
|
try (ZipFile archive = new ZipFile(zip.toFile())) {
|
||||||
|
Enumeration<? extends ZipEntry> entries = archive.entries();
|
||||||
|
while (entries.hasMoreElements()) {
|
||||||
|
ZipEntry entry = entries.nextElement();
|
||||||
|
if (!entry.isDirectory()) {
|
||||||
|
files.put(entry.getName(), archive.getInputStream(entry).readAllBytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<String> exportedBiomes(SortedMap<String, byte[]> files, String prefix, String namespace) {
|
||||||
|
Set<String> biomes = new TreeSet<>();
|
||||||
|
for (String path : files.keySet()) {
|
||||||
|
if (path.startsWith(prefix)) {
|
||||||
|
biomes.add(namespace + ":" + path.substring(prefix.length(), path.length() - ".json".length()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return biomes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class MinMaxAudit implements DfRewriteRule {
|
||||||
|
private final Set<DensityFunction> visited = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||||
|
private final List<String> disjoint = new ArrayList<>();
|
||||||
|
private int checked;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DensityFunction rewrite(DensityFunction function) {
|
||||||
|
if (!visited.add(function)) {
|
||||||
|
return function;
|
||||||
|
}
|
||||||
|
if (function instanceof BinaryFunction binary && (binary.type() == BinaryFunction.Type.MIN || binary.type() == BinaryFunction.Type.MAX)) {
|
||||||
|
checked++;
|
||||||
|
Interval left = binary.left().range();
|
||||||
|
Interval right = binary.right().range();
|
||||||
|
if (left.max() < right.min() || right.max() < left.min()) {
|
||||||
|
disjoint.add(binary.type() + " " + left + " " + right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function.rewriteChildren(this);
|
||||||
|
return function;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int checked() {
|
||||||
|
return checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> disjoint() {
|
||||||
|
return disjoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+136
@@ -0,0 +1,136 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.ExportTestPacks;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenDatapackExporter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportProgress;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportResult;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.DensityFunction;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class WorldgenExportDeepCaves263Test {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
|
private static final long SEED = 1337L;
|
||||||
|
private static final String NAMESPACE = WorldgenExportRequest.DEFAULT_NAMESPACE;
|
||||||
|
private static final String DIMENSION_ID = NAMESPACE + ":" + ExportTestPacks.DIMENSION;
|
||||||
|
private static final int MIN_Y = -64;
|
||||||
|
private static final int LAVA_Y = -62;
|
||||||
|
private static final int LAVA_LINE = -54;
|
||||||
|
private static final int CAVE_TOP = -14;
|
||||||
|
private static final int CHUNK_WIDTH = 16;
|
||||||
|
private static final String CAVE_FIELD = "cave/field";
|
||||||
|
private static final String DIMENSION = """
|
||||||
|
{
|
||||||
|
"regions": ["region"],
|
||||||
|
"dimensionHeight": {"min": -64, "max": 192},
|
||||||
|
"fluidHeight": 63,
|
||||||
|
"caveLavaHeight": 2,
|
||||||
|
"caveProfile": {
|
||||||
|
"enabled": true,
|
||||||
|
"verticalRange": {"min": 0, "max": 50},
|
||||||
|
"verticalEdgeFade": 0,
|
||||||
|
"baseDensityStyle": {"style": "SIMPLEX", "zoom": 0.6},
|
||||||
|
"detailDensityStyle": {"style": "SIMPLEX", "zoom": 0.3},
|
||||||
|
"baseWeight": 1.0,
|
||||||
|
"detailWeight": 0.0,
|
||||||
|
"densityThreshold": {"min": 5.0, "max": 5.0, "style": {"style": "SIMPLEX"}},
|
||||||
|
"thresholdBias": 0.0,
|
||||||
|
"surfaceClearance": 0,
|
||||||
|
"allowSurfaceBreak": false,
|
||||||
|
"allowFluid": false,
|
||||||
|
"allowLava": true,
|
||||||
|
"modules": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public ExportTestPacks.BoundPlatform platform = new ExportTestPacks.BoundPlatform();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deepCavesOpenAsAirAboveTheIrisLavaBand() throws IOException {
|
||||||
|
WorldgenExportResult result = export();
|
||||||
|
|
||||||
|
assertTrue(result.message() + " " + result.diagnostics(), result.isSuccess());
|
||||||
|
JsonObject head = JsonParser.parseString(Files.readString(result.output().resolve("data/" + NAMESPACE + "/worldgen/material_rule/"
|
||||||
|
+ ExportTestPacks.DIMENSION + ".json"), StandardCharsets.UTF_8)).getAsJsonObject().getAsJsonArray("sequence").get(0).getAsJsonObject();
|
||||||
|
assertEquals("minecraft:ore_vein", head.get("type").getAsString());
|
||||||
|
String opening = head.get("density").getAsString();
|
||||||
|
assertTrue(opening, Files.isRegularFile(densityFunction(result.output(), opening)));
|
||||||
|
try (WorldgenExportSampler sampler = WorldgenExportSampler.open(result.output(), WorldgenExportMode.OVERWORLD, DIMENSION_ID, SEED)) {
|
||||||
|
ProtoChunk chunk = sampler.terrainChunk(0, 0);
|
||||||
|
for (int x = 0; x < CHUNK_WIDTH; x++) {
|
||||||
|
for (int z = 0; z < CHUNK_WIDTH; z++) {
|
||||||
|
for (int y = MIN_Y; y <= CAVE_TOP; y++) {
|
||||||
|
BlockState state = chunk.getBlockState(new BlockPos(x, y, z));
|
||||||
|
String where = state + " at " + x + "," + y + "," + z;
|
||||||
|
if (y <= LAVA_Y) {
|
||||||
|
assertTrue(where, state.is(Blocks.LAVA));
|
||||||
|
} else {
|
||||||
|
assertTrue(where, state.isAir());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(chunk.getBlockState(new BlockPos(0, LAVA_LINE - 1, 0)).isAir());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deepCaveRuleSharesTheCaveFieldCacheWithTheFinalDensity() throws IOException {
|
||||||
|
WorldgenExportResult result = export();
|
||||||
|
assertTrue(result.message() + " " + result.diagnostics(), result.isSuccess());
|
||||||
|
|
||||||
|
try (WorldgenExportSampler sampler = WorldgenExportSampler.open(result.output(), WorldgenExportMode.OVERWORLD, DIMENSION_ID, SEED)) {
|
||||||
|
CompiledCaches caches = CompiledCaches.open(sampler.registries(), SEED);
|
||||||
|
DensityFunction opening = caches.oreVeins().getFirst().density();
|
||||||
|
int field = caches.rootId(caches.function(NAMESPACE + ":" + CAVE_FIELD));
|
||||||
|
Set<Integer> finalDensity = caches.ids(caches.finalDensity());
|
||||||
|
|
||||||
|
assertTrue(caches.ids(opening).toString(), caches.ids(opening).contains(field));
|
||||||
|
assertTrue(finalDensity.toString(), finalDensity.contains(field));
|
||||||
|
assertTrue(finalDensity.toString(), finalDensity.contains(caches.rootId(opening)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private WorldgenExportResult export() throws IOException {
|
||||||
|
File pack = ExportTestPacks.withDimension(temporaryFolder.newFolder("pack"), DIMENSION);
|
||||||
|
WorldgenExportRequest request = WorldgenExportRequest.builder()
|
||||||
|
.pack(pack.toPath())
|
||||||
|
.dimension(ExportTestPacks.DIMENSION)
|
||||||
|
.exportRoot(temporaryFolder.newFolder("exports").toPath())
|
||||||
|
.format("directory")
|
||||||
|
.build();
|
||||||
|
return WorldgenDatapackExporter.create().export(request, WorldgenExportProgress.NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path densityFunction(Path datapack, String id) {
|
||||||
|
int separator = id.indexOf(':');
|
||||||
|
return datapack.resolve("data/" + id.substring(0, separator) + "/worldgen/density_function/" + id.substring(separator + 1) + ".json");
|
||||||
|
}
|
||||||
|
}
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.ExportTestPacks;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenDatapackExporter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportProgress;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportResult;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class WorldgenExportPipeline263Test {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
|
private static final long SEED = 1337L;
|
||||||
|
private static final String DIMENSION_ID = WorldgenExportRequest.DEFAULT_NAMESPACE + ":" + ExportTestPacks.DIMENSION;
|
||||||
|
private static final int[] SAMPLE_Y = {-64, 0, 64, 200};
|
||||||
|
private static final int LATTICE = 8;
|
||||||
|
private static final int LATTICE_STEP = 61;
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public ExportTestPacks.BoundPlatform platform = new ExportTestPacks.BoundPlatform();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void exportedPackLoadsAndSamples() throws IOException {
|
||||||
|
File pack = ExportTestPacks.basic(temporaryFolder.newFolder("pack"));
|
||||||
|
Path root = temporaryFolder.newFolder("exports").toPath();
|
||||||
|
WorldgenExportRequest request = WorldgenExportRequest.builder()
|
||||||
|
.pack(pack.toPath())
|
||||||
|
.dimension(ExportTestPacks.DIMENSION)
|
||||||
|
.exportRoot(root)
|
||||||
|
.format("directory")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
WorldgenExportResult result = WorldgenDatapackExporter.create().export(request, WorldgenExportProgress.NONE);
|
||||||
|
|
||||||
|
assertTrue(result.message() + " " + result.diagnostics(), result.isSuccess());
|
||||||
|
try (WorldgenExportSampler sampler = WorldgenExportSampler.open(result.output(), WorldgenExportMode.OVERWORLD, DIMENSION_ID, SEED)) {
|
||||||
|
assertFalse(sampler.densityFunctionIds().isEmpty());
|
||||||
|
for (String id : sampler.densityFunctionIds()) {
|
||||||
|
for (int y : SAMPLE_Y) {
|
||||||
|
for (int xIndex = 0; xIndex < LATTICE; xIndex++) {
|
||||||
|
for (int zIndex = 0; zIndex < LATTICE; zIndex++) {
|
||||||
|
int x = (xIndex - LATTICE / 2) * LATTICE_STEP;
|
||||||
|
int z = (zIndex - LATTICE / 2) * LATTICE_STEP;
|
||||||
|
float value = sampler.density(id, x, y, z);
|
||||||
|
assertTrue(id + " at " + x + "," + y + "," + z + " = " + value, Float.isFinite(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int xIndex = 0; xIndex < LATTICE; xIndex++) {
|
||||||
|
for (int zIndex = 0; zIndex < LATTICE; zIndex++) {
|
||||||
|
int x = (xIndex - LATTICE / 2) * LATTICE_STEP;
|
||||||
|
int z = (zIndex - LATTICE / 2) * LATTICE_STEP;
|
||||||
|
int height = sampler.baseHeight(x, z);
|
||||||
|
assertTrue("height " + height + " at " + x + "," + z, height >= sampler.minY() && height <= sampler.minY() + sampler.height());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.ExportTestPacks;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenDatapackExporter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportProgress;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportResult;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class WorldgenExportSamplerThreadsTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
|
private static final long SEED = 1337L;
|
||||||
|
private static final String DIMENSION_ID = WorldgenExportRequest.DEFAULT_NAMESPACE + ":" + ExportTestPacks.DIMENSION;
|
||||||
|
private static final String FINAL_DENSITY_ID = DIMENSION_ID + "/final_density";
|
||||||
|
private static final int THREADS = 8;
|
||||||
|
private static final int POINTS = 2048;
|
||||||
|
private static final int ROW = 64;
|
||||||
|
private static final int STEP = 37;
|
||||||
|
private static final int ROUNDS = 3;
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public ExportTestPacks.BoundPlatform platform = new ExportTestPacks.BoundPlatform();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void concurrentSamplingMatchesSequentialSampling() throws Exception {
|
||||||
|
File pack = ExportTestPacks.basic(temporaryFolder.newFolder("pack"));
|
||||||
|
WorldgenExportRequest request = WorldgenExportRequest.builder()
|
||||||
|
.pack(pack.toPath())
|
||||||
|
.dimension(ExportTestPacks.DIMENSION)
|
||||||
|
.exportRoot(temporaryFolder.newFolder("exports").toPath())
|
||||||
|
.format("directory")
|
||||||
|
.build();
|
||||||
|
WorldgenExportResult result = WorldgenDatapackExporter.create().export(request, WorldgenExportProgress.NONE);
|
||||||
|
assertTrue(result.message() + " " + result.diagnostics(), result.isSuccess());
|
||||||
|
try (WorldgenExportSampler sampler = WorldgenExportSampler.open(result.output(), WorldgenExportMode.OVERWORLD, DIMENSION_ID, SEED)) {
|
||||||
|
assertTrue(sampler.densityFunctionIds().toString(), sampler.densityFunctionIds().contains(FINAL_DENSITY_ID));
|
||||||
|
Sample expected = sweep(sampler, 0, 1).call();
|
||||||
|
ExecutorService executor = Executors.newFixedThreadPool(THREADS);
|
||||||
|
try {
|
||||||
|
List<Future<Sample>> futures = new ArrayList<>(THREADS);
|
||||||
|
for (int thread = 0; thread < THREADS; thread++) {
|
||||||
|
futures.add(executor.submit(sweep(sampler, thread * (POINTS / THREADS), ROUNDS)));
|
||||||
|
}
|
||||||
|
for (Future<Sample> future : futures) {
|
||||||
|
Sample sample = future.get();
|
||||||
|
assertArrayEquals(expected.density(), sample.density(), 0.0F);
|
||||||
|
assertEquals(List.of(expected.biome()), List.of(sample.biome()));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
executor.shutdownNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Callable<Sample> sweep(WorldgenExportSampler sampler, int offset, int rounds) {
|
||||||
|
return () -> {
|
||||||
|
float[] density = new float[POINTS];
|
||||||
|
String[] biome = new String[POINTS];
|
||||||
|
for (int round = 0; round < rounds; round++) {
|
||||||
|
for (int step = 0; step < POINTS; step++) {
|
||||||
|
int index = (offset + step) % POINTS;
|
||||||
|
int x = (index % ROW - ROW / 2) * STEP;
|
||||||
|
int y = (index * 7) % 256 - 64;
|
||||||
|
int z = (index / ROW - POINTS / ROW / 2) * STEP;
|
||||||
|
density[index] = sampler.density(FINAL_DENSITY_ID, x, y, z);
|
||||||
|
biome[index] = sampler.biome(x, y, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Sample(density, biome);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Sample(float[] density, String[] biome) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+90
@@ -0,0 +1,90 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.ExportTestPacks;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenDatapackExporter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportProgress;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportResult;
|
||||||
|
import art.arcane.iris.testsupport.CompatTestPlatform;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class WorldgenExportSeaLevel263Test {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
|
private static final long SEED = 1337L;
|
||||||
|
private static final String DIMENSION_ID = WorldgenExportRequest.DEFAULT_NAMESPACE + ":" + ExportTestPacks.DIMENSION;
|
||||||
|
private static final int FLUID_HEIGHT = 40;
|
||||||
|
private static final int SEA_FLOOR = 28;
|
||||||
|
private static final int CHUNK_WIDTH = 16;
|
||||||
|
private static final String DIMENSION = """
|
||||||
|
{
|
||||||
|
"regions": ["region"],
|
||||||
|
"dimensionHeight": {"min": -64, "max": 192},
|
||||||
|
"fluidHeight": 40,
|
||||||
|
"landChance": 0,
|
||||||
|
"carvingEnabled": false
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public ExportTestPacks.BoundPlatform platform = new ExportTestPacks.BoundPlatform();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void waterFillsThroughTheIrisFluidHeight() throws IOException {
|
||||||
|
WorldgenExportResult result = export();
|
||||||
|
|
||||||
|
assertTrue(result.message() + " " + result.diagnostics(), result.isSuccess());
|
||||||
|
try (WorldgenExportSampler sampler = WorldgenExportSampler.open(result.output(), WorldgenExportMode.OVERWORLD, DIMENSION_ID, SEED)) {
|
||||||
|
ProtoChunk chunk = sampler.terrainChunk(0, 0);
|
||||||
|
for (int x = 0; x < CHUNK_WIDTH; x++) {
|
||||||
|
for (int z = 0; z < CHUNK_WIDTH; z++) {
|
||||||
|
assertSolid(chunk.getBlockState(new BlockPos(x, SEA_FLOOR, z)), x, z);
|
||||||
|
for (int y = SEA_FLOOR + 1; y <= FLUID_HEIGHT; y++) {
|
||||||
|
BlockState state = chunk.getBlockState(new BlockPos(x, y, z));
|
||||||
|
assertTrue(state + " at " + x + "," + y + "," + z, state.is(Blocks.WATER));
|
||||||
|
}
|
||||||
|
BlockState above = chunk.getBlockState(new BlockPos(x, FLUID_HEIGHT + 1, z));
|
||||||
|
assertTrue(above + " above the fluid height at " + x + "," + z, above.isAir());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertEquals(FLUID_HEIGHT + 1, sampler.seaLevel());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertSolid(BlockState state, int x, int z) {
|
||||||
|
assertTrue(state + " sea floor at " + x + "," + z, !state.isAir() && state.getFluidState().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private WorldgenExportResult export() throws IOException {
|
||||||
|
File pack = ExportTestPacks.withDimension(temporaryFolder.newFolder("pack"), DIMENSION);
|
||||||
|
CompatTestPlatform.write(pack, "generators/flat.json", "{\"composite\":[]}");
|
||||||
|
CompatTestPlatform.write(pack, "biomes/ocean.json", """
|
||||||
|
{"name":"Ocean","generators":[{"generator":"flat","min":-12,"max":-12}]}
|
||||||
|
""");
|
||||||
|
WorldgenExportRequest request = WorldgenExportRequest.builder()
|
||||||
|
.pack(pack.toPath())
|
||||||
|
.dimension(ExportTestPacks.DIMENSION)
|
||||||
|
.exportRoot(temporaryFolder.newFolder("exports").toPath())
|
||||||
|
.format("directory")
|
||||||
|
.build();
|
||||||
|
return WorldgenDatapackExporter.create().export(request, WorldgenExportProgress.NONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
+165
@@ -0,0 +1,165 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.ExportTestPacks;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenDatapackExporter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportProgress;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportResult;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
import net.minecraft.nbt.NbtAccounter;
|
||||||
|
import net.minecraft.nbt.NbtIo;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.Rotation;
|
||||||
|
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class WorldgenExportSubterrain263Test {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
|
private static final long SEED = 1337L;
|
||||||
|
private static final String DIMENSION_ID = WorldgenExportRequest.DEFAULT_NAMESPACE + ":" + ExportTestPacks.DIMENSION;
|
||||||
|
private static final String POOLS = WorldgenExportRequest.DEFAULT_NAMESPACE + ":subterrain/pools";
|
||||||
|
private static final String TUBES = WorldgenExportRequest.DEFAULT_NAMESPACE + ":subterrain/tubes";
|
||||||
|
private static final String ANCHOR = WorldgenExportRequest.DEFAULT_NAMESPACE + ":subterrain/anchor";
|
||||||
|
private static final String TEMPLATES = "data/" + WorldgenExportRequest.DEFAULT_NAMESPACE + "/structure/subterrain/";
|
||||||
|
private static final int CHUNK_X = 3;
|
||||||
|
private static final int CHUNK_Z = -2;
|
||||||
|
private static final int POOLS_CENTER_Y = -49;
|
||||||
|
private static final int POOLS_BELOW_CENTER = 10;
|
||||||
|
private static final int POOLS_EXTENT = 10;
|
||||||
|
private static final int MAX_REACH = 128;
|
||||||
|
private static final String DIMENSION = """
|
||||||
|
{"regions": ["region"], "subterrainFeatures": [
|
||||||
|
{"id": "pools", "family": "CENOTE", "worldYRange": {"min": -60, "max": -40}, "spacing": 64, "probability": 1.0,
|
||||||
|
"radius": 8, "height": 12, "fluid": "WATER", "fluidDepth": 2, "shapeWarp": 0.5, "pillarSpacing": 0,
|
||||||
|
"formationFraction": 0.0, "solid": "minecraft:stone"},
|
||||||
|
{"id": "tubes", "family": "LAVA_TUBE", "allowedRegions": ["region"], "worldYRange": {"min": -60, "max": 40},
|
||||||
|
"spacing": 320, "probability": 0.5, "length": 300, "radius": 6, "height": 8, "fluidDepth": 2, "chimneyHeight": 0,
|
||||||
|
"shapeWarp": 0.0, "pillarSpacing": 0, "formationFraction": 0.0, "solid": "minecraft:stone"}]}
|
||||||
|
""";
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public ExportTestPacks.BoundPlatform platform = new ExportTestPacks.BoundPlatform();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void subterrainRoomsLoadAndPlaceThroughRealCodecs() throws IOException {
|
||||||
|
File pack = ExportTestPacks.withDimension(temporaryFolder.newFolder("pack"), DIMENSION);
|
||||||
|
WorldgenExportRequest request = WorldgenExportRequest.builder()
|
||||||
|
.pack(pack.toPath())
|
||||||
|
.dimension(ExportTestPacks.DIMENSION)
|
||||||
|
.exportRoot(temporaryFolder.newFolder("exports").toPath())
|
||||||
|
.format("directory")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
WorldgenExportResult result = WorldgenDatapackExporter.create().export(request, WorldgenExportProgress.NONE);
|
||||||
|
|
||||||
|
assertTrue(result.message() + " " + result.diagnostics(), result.isSuccess());
|
||||||
|
try (WorldgenExportSampler sampler = WorldgenExportSampler.open(result.output(), WorldgenExportMode.OVERWORLD, DIMENSION_ID, SEED)) {
|
||||||
|
assertRegistered(sampler, POOLS, 4, 2);
|
||||||
|
assertRegistered(sampler, TUBES, 20, 10);
|
||||||
|
int cornerX = CHUNK_X << 4;
|
||||||
|
int cornerZ = CHUNK_Z << 4;
|
||||||
|
|
||||||
|
BoundingBox pools = placedBox(sampler, POOLS);
|
||||||
|
assertEquals(cornerX - POOLS_EXTENT, pools.minX());
|
||||||
|
assertEquals(cornerX + POOLS_EXTENT, pools.maxX());
|
||||||
|
assertEquals(cornerZ - POOLS_EXTENT, pools.minZ());
|
||||||
|
assertEquals(cornerZ + POOLS_EXTENT, pools.maxZ());
|
||||||
|
assertEquals(POOLS_CENTER_Y - POOLS_BELOW_CENTER, pools.minY());
|
||||||
|
|
||||||
|
BoundingBox tubes = placedBox(sampler, TUBES);
|
||||||
|
assertEquals(2 * MAX_REACH + 1, Math.max(tubes.getXSpan(), tubes.getZSpan()));
|
||||||
|
assertTrue(tubes.toString(), tubes.minX() >= cornerX - MAX_REACH && tubes.maxX() <= cornerX + MAX_REACH);
|
||||||
|
assertTrue(tubes.toString(), tubes.minZ() >= cornerZ - MAX_REACH && tubes.maxZ() <= cornerZ + MAX_REACH);
|
||||||
|
assertTrue(tubes.toString(), tubes.minY() >= -60 && tubes.maxY() <= 40);
|
||||||
|
}
|
||||||
|
assertTemplatesResolve(result.output());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertRegistered(WorldgenExportSampler sampler, String id, int spacing, int separation) {
|
||||||
|
Identifier identifier = Identifier.parse(id);
|
||||||
|
Structure structure = sampler.registries().lookupOrThrow(Registries.STRUCTURE)
|
||||||
|
.getOrThrow(ResourceKey.create(Registries.STRUCTURE, identifier)).value();
|
||||||
|
assertTrue(id, structure instanceof JigsawStructure);
|
||||||
|
assertEquals(id, GenerationStep.Decoration.UNDERGROUND_STRUCTURES, structure.step());
|
||||||
|
StructureSet set = sampler.registries().lookupOrThrow(Registries.STRUCTURE_SET)
|
||||||
|
.getOrThrow(ResourceKey.create(Registries.STRUCTURE_SET, identifier)).value();
|
||||||
|
RandomSpreadStructurePlacement placement = (RandomSpreadStructurePlacement) set.placement();
|
||||||
|
assertEquals(id, spacing, placement.spacing());
|
||||||
|
assertEquals(id, separation, placement.separation());
|
||||||
|
assertTrue(id, sampler.registries().lookupOrThrow(Registries.TEMPLATE_POOL).containsKey(identifier));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BoundingBox placedBox(WorldgenExportSampler sampler, String id) throws IOException {
|
||||||
|
StructureStart start = sampler.structureStart(id, CHUNK_X, CHUNK_Z);
|
||||||
|
assertTrue(id, start.isValid());
|
||||||
|
assertEquals(id, 1, start.getPieces().size());
|
||||||
|
PoolElementStructurePiece piece = (PoolElementStructurePiece) start.getPieces().getFirst();
|
||||||
|
assertTrue(id + " " + piece.getElement(), piece.getElement() instanceof ListPoolElement);
|
||||||
|
return start.getBoundingBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertTemplatesResolve(Path datapack) throws IOException {
|
||||||
|
List<Path> templates;
|
||||||
|
try (Stream<Path> paths = Files.walk(datapack.resolve(TEMPLATES))) {
|
||||||
|
templates = paths.filter(path -> path.toString().endsWith(".nbt")).sorted().toList();
|
||||||
|
}
|
||||||
|
assertEquals(12, templates.size());
|
||||||
|
boolean lava = false;
|
||||||
|
for (Path path : templates) {
|
||||||
|
CompoundTag tag;
|
||||||
|
try (InputStream input = Files.newInputStream(path)) {
|
||||||
|
tag = NbtIo.readCompressed(input, NbtAccounter.unlimitedHeap());
|
||||||
|
}
|
||||||
|
StructureTemplate template = new StructureTemplate();
|
||||||
|
template.load(BuiltInRegistries.BLOCK, tag);
|
||||||
|
StructurePlaceSettings settings = new StructurePlaceSettings();
|
||||||
|
assertTrue(path.toString(), template.filterBlocks(BlockPos.ZERO, settings, Blocks.AIR).isEmpty());
|
||||||
|
if (path.getFileName().toString().endsWith("_core.nbt")) {
|
||||||
|
assertFalse(path.toString(), template.filterBlocks(BlockPos.ZERO, settings, Blocks.CAVE_AIR).isEmpty());
|
||||||
|
List<StructureTemplate.JigsawBlockInfo> jigsaws = template.getJigsaws(BlockPos.ZERO, Rotation.NONE);
|
||||||
|
assertEquals(path.toString(), 1, jigsaws.size());
|
||||||
|
assertEquals(ANCHOR, jigsaws.getFirst().name().toString());
|
||||||
|
lava |= !template.filterBlocks(BlockPos.ZERO, settings, Blocks.LAVA).isEmpty();
|
||||||
|
} else {
|
||||||
|
assertFalse(path.toString(), template.filterBlocks(BlockPos.ZERO, settings, Blocks.STONE).isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue("no lava tube template holds lava", lava);
|
||||||
|
}
|
||||||
|
}
|
||||||
+111
@@ -0,0 +1,111 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ExportIrFixtures;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
|
||||||
|
import art.arcane.iris.pack.datapack.export.emit.WorldgenJsonWriter;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class WorldgenStageBenchTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
|
private static final long SEED = 1337L;
|
||||||
|
private static final int SIDE = 2;
|
||||||
|
private static final WorldgenStageBench.CallBudget BUDGET = new WorldgenStageBench.CallBudget(32, 8, 0L);
|
||||||
|
|
||||||
|
private static HeadlessNativeRegistries loaded;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void loadFixture() throws IOException {
|
||||||
|
HeadlessNativeBootstrap.initialize();
|
||||||
|
SortedMap<String, byte[]> files = WorldgenJsonWriter.forTarget(DataVersion.V26_3)
|
||||||
|
.write(ExportIrFixtures.flatNoise(DataVersion.V26_3, WorldgenExportMode.PRESET));
|
||||||
|
Path directory = Files.createTempDirectory("iris-stage-bench-");
|
||||||
|
for (Map.Entry<String, byte[]> file : files.entrySet()) {
|
||||||
|
Path target = directory.resolve(file.getKey());
|
||||||
|
Files.createDirectories(target.getParent());
|
||||||
|
Files.write(target, file.getValue());
|
||||||
|
}
|
||||||
|
try (PaperRegistryScope scope = PaperRegistryScope.open()) {
|
||||||
|
loaded = HeadlessNativeRegistries.load(directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void closeFixture() throws IOException {
|
||||||
|
if (loaded != null) {
|
||||||
|
loaded.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void exportedFixtureRunsEveryStage() {
|
||||||
|
assertStages(bench("export", ExportIrFixtures.NAMESPACE + ":" + ExportIrFixtures.DIMENSION));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void vanillaOverworldRunsEveryStage() {
|
||||||
|
assertStages(bench("vanilla", DatapackCostProbe.VANILLA_PRESET));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertStages(WorldgenStageBench bench) {
|
||||||
|
WorldgenStageBench.ChunkSamples samples = new WorldgenStageBench.ChunkSamples();
|
||||||
|
bench.chunks(0, 0, SIDE, samples);
|
||||||
|
bench.chunks(SIDE + 4, 0, SIDE, samples);
|
||||||
|
WorldgenStageBench.ChunkTimings timings = samples.timings();
|
||||||
|
assertEquals(2 * SIDE * SIDE, timings.chunks());
|
||||||
|
assertEquals(2 * (SIDE + 2) * (SIDE + 2), timings.biomes().wall().count());
|
||||||
|
assertEquals(2 * SIDE * SIDE, timings.noise().cpu().count());
|
||||||
|
assertEquals(2 * SIDE * SIDE, timings.surface().wall().count());
|
||||||
|
assertEquals(2 * SIDE * SIDE, timings.carverBiome().wall().count());
|
||||||
|
assertEquals(0L, timings.biomeFallbacks());
|
||||||
|
assertTrue(Double.isFinite(timings.totalMean()));
|
||||||
|
assertTrue(Double.isFinite(timings.totalCpuMean()));
|
||||||
|
assertCalls(samples(bench, Call.UNCACHED_BIOME));
|
||||||
|
assertCalls(samples(bench, Call.BASE_HEIGHT));
|
||||||
|
assertCalls(samples(bench, Call.BASE_COLUMN));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertCalls(WorldgenStageBench.CallTiming timing) {
|
||||||
|
assertEquals(2 * BUDGET.minimumCalls(), timing.calls());
|
||||||
|
assertEquals(2 * BUDGET.minimumCalls(), timing.wall().count());
|
||||||
|
assertTrue(Double.isFinite(timing.cpuMean()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorldgenStageBench.CallTiming samples(WorldgenStageBench bench, Call call) {
|
||||||
|
WorldgenStageBench.CallSamples samples = new WorldgenStageBench.CallSamples();
|
||||||
|
for (int round = 0; round < 2; round++) {
|
||||||
|
switch (call) {
|
||||||
|
case UNCACHED_BIOME -> bench.uncachedBiome(BUDGET, samples);
|
||||||
|
case BASE_HEIGHT -> bench.baseHeight(BUDGET, Heightmap.Types.WORLD_SURFACE_WG, samples);
|
||||||
|
case BASE_COLUMN -> bench.baseColumn(BUDGET, samples);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return samples.timing();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorldgenStageBench bench(String label, String preset) {
|
||||||
|
return new WorldgenStageBench(new WorldgenStageBench.Options(label,
|
||||||
|
DatapackCostProbe.presetOverworld(loaded.registries(), preset), loaded.registries(), SEED));
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum Call {
|
||||||
|
UNCACHED_BIOME,
|
||||||
|
BASE_HEIGHT,
|
||||||
|
BASE_COLUMN
|
||||||
|
}
|
||||||
|
}
|
||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
package art.arcane.iris.probe.fidelity;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ExportIrFixtures;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
|
||||||
|
import art.arcane.iris.pack.datapack.export.emit.WorldgenJsonWriter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.AquiferDef;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Axis;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.BiomeDef;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ClimateLayout;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.DimensionDef;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportIr;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Field;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.MaterialProgram;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.RouterDef;
|
||||||
|
import art.arcane.iris.probe.PaperRegistryIsolation;
|
||||||
|
import art.arcane.iris.probe.WorldgenExportSampler;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class VanillaNoiseColumnSourceTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
|
private static final long SEED = 1337L;
|
||||||
|
private static final String DIMENSION_ID = ExportIrFixtures.NAMESPACE + ":" + ExportIrFixtures.DIMENSION;
|
||||||
|
private static final String BIOME_ID = ExportIrFixtures.NAMESPACE + ":" + ExportIrFixtures.BIOME;
|
||||||
|
private static final String LOAD_KEY = "Flat Plains";
|
||||||
|
private static final int LAND_HEIGHT = 80;
|
||||||
|
private static final int SEA_FLOOR_HEIGHT = 40;
|
||||||
|
private static final int MAX_Y = ExportIrFixtures.MIN_Y + ExportIrFixtures.HEIGHT;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void landColumnsReportTheTopSolidBlockAndStayDry() throws IOException {
|
||||||
|
try (VanillaNoiseColumnSource source = new VanillaNoiseColumnSource(open(LAND_HEIGHT), Map.of(BIOME_ID, LOAD_KEY))) {
|
||||||
|
assertEquals(ExportIrFixtures.MIN_Y, source.minY());
|
||||||
|
assertEquals(MAX_Y, source.maxY());
|
||||||
|
for (int x : new int[]{-37, 0, 113}) {
|
||||||
|
FidelityColumn column = source.column(x, -x);
|
||||||
|
assertEquals(new FidelityColumn(LOAD_KEY, LAND_HEIGHT - 1, false), column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void seaColumnsReportTheFloorUnderWater() throws IOException {
|
||||||
|
WorldgenExportSampler sampler = open(SEA_FLOOR_HEIGHT);
|
||||||
|
try (VanillaNoiseColumnSource source = new VanillaNoiseColumnSource(sampler, Map.of(BIOME_ID, LOAD_KEY))) {
|
||||||
|
FidelityStats stats = new FidelityStats(source.minY(), source.maxY());
|
||||||
|
for (int x : new int[]{-37, 0, 113}) {
|
||||||
|
FidelityColumn column = source.column(x, x);
|
||||||
|
assertEquals(new FidelityColumn(LOAD_KEY, sampler.floorHeight(x, x) - 1, true), column);
|
||||||
|
assertTrue(column.terrainY() + " under sea level", column.terrainY() < ExportIrFixtures.SEA_LEVEL - 1);
|
||||||
|
assertEquals(ExportIrFixtures.SEA_LEVEL, sampler.baseHeight(x, x));
|
||||||
|
stats.accept(column);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(0.0D, stats.landFraction(), 0.0D);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void exportedBiomeWithoutALoadKeyIsRejected() throws IOException {
|
||||||
|
try (VanillaNoiseColumnSource source = new VanillaNoiseColumnSource(open(LAND_HEIGHT), Map.of())) {
|
||||||
|
IllegalStateException failure = assertThrows(IllegalStateException.class, () -> source.column(0, 0));
|
||||||
|
|
||||||
|
assertTrue(failure.getMessage(), failure.getMessage().contains(BIOME_ID));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorldgenExportSampler open(int height) throws IOException {
|
||||||
|
return WorldgenExportSampler.open(flatExport(height), WorldgenExportMode.PRESET, DIMENSION_ID, SEED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SortedMap<String, byte[]> flatExport(int height) {
|
||||||
|
Field zero = new Field.Const(0.0);
|
||||||
|
Field level = new Field.Const(height);
|
||||||
|
Field y = new Field.Coord(Axis.Y, Field.Tiling.CLAMP, -4064.0, 4062.0, -4064.0, 4062.0);
|
||||||
|
Field finalDensity = new Field.Grid(new Field.Binary(Field.BinaryOp.MUL, new Field.Const(0.125),
|
||||||
|
new Field.Binary(Field.BinaryOp.SUB, level, y)), 4, 8);
|
||||||
|
RouterDef router = new RouterDef(zero, zero, zero, zero, zero, zero, new Field.Grid(level, 16, 1), finalDensity, AquiferDef.dry(level));
|
||||||
|
DimensionDef dimension = new DimensionDef(ExportIrFixtures.DIMENSION, ExportIrFixtures.MIN_Y, ExportIrFixtures.HEIGHT,
|
||||||
|
ExportIrFixtures.HEIGHT, ExportIrFixtures.FLUID_HEIGHT, "minecraft:stone", "minecraft:water", ExportIrFixtures.dimensionType());
|
||||||
|
ClimateLayout climate = new ClimateLayout(List.of(new ClimateLayout.ClimateEntry(BIOME_ID, ClimateLayout.ClimateAxis.TEMPERATURE, -2.0, 2.0)));
|
||||||
|
ExportIr ir = new ExportIr(ExportIrFixtures.NAMESPACE, DataVersion.V26_3, WorldgenExportMode.PRESET, dimension, List.of(), router, climate,
|
||||||
|
List.of(new BiomeDef(ExportIrFixtures.BIOME, ExportIrFixtures.biome())), MaterialProgram.rock("minecraft:stone"),
|
||||||
|
List.of(), List.of(), List.of());
|
||||||
|
return WorldgenJsonWriter.forTarget(DataVersion.V26_3).write(ir);
|
||||||
|
}
|
||||||
|
}
|
||||||
+189
@@ -0,0 +1,189 @@
|
|||||||
|
package art.arcane.iris.probe.fidelity;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ExportIrFixtures;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
|
||||||
|
import art.arcane.iris.pack.datapack.export.emit.WorldgenJsonWriter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.AquiferDef;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Axis;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.BiomeDef;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ClimateLayout;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.DimensionDef;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportIr;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Field;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.JsonResource;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.MaterialProgram;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.RouterDef;
|
||||||
|
import art.arcane.iris.probe.PaperRegistryIsolation;
|
||||||
|
import art.arcane.iris.probe.WorldgenExportSampler;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class VanillaNoiseFidelitySourceTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
|
private static final long SEED = 1337L;
|
||||||
|
private static final String DIMENSION_ID = ExportIrFixtures.NAMESPACE + ":" + ExportIrFixtures.DIMENSION;
|
||||||
|
private static final String BIOME_ID = ExportIrFixtures.NAMESPACE + ":" + ExportIrFixtures.BIOME;
|
||||||
|
private static final String LOAD_KEY = "Flat Plains";
|
||||||
|
private static final int LAND_HEIGHT = 80;
|
||||||
|
private static final int MAX_Y = ExportIrFixtures.MIN_Y + ExportIrFixtures.HEIGHT;
|
||||||
|
private static final String WALL_SHELL = ExportIrFixtures.NAMESPACE + ":cave/wall_shell";
|
||||||
|
private static final int WALL_MIN_Y = 10;
|
||||||
|
private static final int WALL_MAX_Y = 20;
|
||||||
|
private static final int CARVED_HEIGHT = 120;
|
||||||
|
private static final int VOID_MIN_Y = 96;
|
||||||
|
private static final int CAVE_MIN_Y = 64;
|
||||||
|
private static final Field BLOCK_Y = new Field.Coord(Axis.Y, Field.Tiling.CLAMP, -4064.0, 4062.0, -4064.0, 4062.0);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void flatExportReportsTheConstantSurface() throws IOException {
|
||||||
|
WorldgenExportSampler sampler = open(LAND_HEIGHT);
|
||||||
|
try (VanillaNoiseFidelitySource source = new VanillaNoiseFidelitySource(sampler, Map.of(BIOME_ID, LOAD_KEY), WALL_SHELL)) {
|
||||||
|
assertEquals(ExportIrFixtures.MIN_Y, source.minY());
|
||||||
|
assertEquals(MAX_Y, source.maxY());
|
||||||
|
for (int x : new int[]{-37, 0, 113}) {
|
||||||
|
assertEquals(LAND_HEIGHT, sampler.baseHeight(x, -x));
|
||||||
|
}
|
||||||
|
for (int[] chunkPosition : new int[][]{{0, 0}, {-3, 5}}) {
|
||||||
|
FidelityChunk chunk = source.sample(chunkPosition[0], chunkPosition[1]);
|
||||||
|
for (int x = 0; x < 16; x++) {
|
||||||
|
for (int z = 0; z < 16; z++) {
|
||||||
|
assertEquals(LAND_HEIGHT - 1, chunk.surfaceY(x, z));
|
||||||
|
assertEquals(LOAD_KEY, chunk.surfaceBiome(x, z));
|
||||||
|
assertTrue(chunk.open(x, LAND_HEIGHT, z));
|
||||||
|
assertNull(chunk.block(x, LAND_HEIGHT - 1, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int y = ExportIrFixtures.MIN_Y; y < LAND_HEIGHT; y++) {
|
||||||
|
assertFalse("y " + y, chunk.open(7, y, 9));
|
||||||
|
assertFalse("y " + y, chunk.fluid(7, y, 9));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FidelityStats stats = FidelityScanner.scan(source, 0, 0, 1, 4, 2);
|
||||||
|
|
||||||
|
assertEquals(144L, stats.heightColumns());
|
||||||
|
assertEquals(LAND_HEIGHT - 1, stats.heightQuantile(0.1D), 0.0D);
|
||||||
|
assertEquals(LAND_HEIGHT - 1, stats.heightQuantile(0.9D), 0.0D);
|
||||||
|
assertEquals(1.0D, stats.landFraction(), 0.0D);
|
||||||
|
assertEquals(Map.of(LOAD_KEY, 144L), stats.biomeColumns());
|
||||||
|
assertEquals(0L, stats.blockColumns());
|
||||||
|
assertEquals(0L, stats.wallSolidVoxels());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void wallShellDensityMarksWallVoxels() throws IOException {
|
||||||
|
WorldgenExportSampler sampler = WorldgenExportSampler.open(flatExport(LAND_HEIGHT, wallShell()), WorldgenExportMode.PRESET, DIMENSION_ID, SEED);
|
||||||
|
try (VanillaNoiseFidelitySource source = new VanillaNoiseFidelitySource(sampler, Map.of(BIOME_ID, LOAD_KEY), WALL_SHELL)) {
|
||||||
|
FidelityChunk chunk = source.sample(2, -1);
|
||||||
|
|
||||||
|
assertTrue(chunk.wallsKnown());
|
||||||
|
assertTrue(chunk.wall(7, WALL_MIN_Y, 9));
|
||||||
|
assertTrue(chunk.wall(7, WALL_MAX_Y - 1, 9));
|
||||||
|
assertFalse(chunk.wall(7, WALL_MIN_Y - 1, 9));
|
||||||
|
assertFalse(chunk.wall(7, WALL_MAX_Y, 9));
|
||||||
|
|
||||||
|
FidelityStats stats = FidelityScanner.scan(source, 0, 0, 1, 4, 2);
|
||||||
|
|
||||||
|
long interiorColumns = 9L * 9L;
|
||||||
|
assertEquals(interiorColumns * (LAND_HEIGHT - 1 - ExportIrFixtures.MIN_Y), stats.wallSolidVoxels());
|
||||||
|
assertEquals(interiorColumns * (WALL_MAX_Y - WALL_MIN_Y), stats.wallVoxels());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void openSpaceOutsideTheTerrainOperandIsATerrainVoid() throws IOException {
|
||||||
|
WorldgenExportSampler sampler = WorldgenExportSampler.open(carvedExport(), WorldgenExportMode.PRESET, DIMENSION_ID, SEED);
|
||||||
|
try (VanillaNoiseFidelitySource source = new VanillaNoiseFidelitySource(sampler, Map.of(BIOME_ID, LOAD_KEY), WALL_SHELL)) {
|
||||||
|
FidelityChunk chunk = source.sample(1, -2);
|
||||||
|
|
||||||
|
assertEquals(CARVED_HEIGHT - 1, chunk.surfaceY(7, 9));
|
||||||
|
assertTrue(chunk.open(7, VOID_MIN_Y, 9));
|
||||||
|
assertFalse(chunk.terrainMass(7, VOID_MIN_Y, 9));
|
||||||
|
assertTrue(chunk.open(7, CAVE_MIN_Y + 3, 9));
|
||||||
|
assertTrue(chunk.terrainMass(7, CAVE_MIN_Y + 3, 9));
|
||||||
|
assertFalse(chunk.open(7, VOID_MIN_Y - 4, 9));
|
||||||
|
assertTrue(chunk.terrainMass(7, VOID_MIN_Y - 4, 9));
|
||||||
|
|
||||||
|
FidelityStats stats = FidelityScanner.scan(source, 0, 0, 1, 4, 2);
|
||||||
|
|
||||||
|
int voidBand = (VOID_MIN_Y - ExportIrFixtures.MIN_Y) / 16;
|
||||||
|
int caveBand = (CAVE_MIN_Y - ExportIrFixtures.MIN_Y) / 16;
|
||||||
|
assertEquals(1.0D, stats.voidShare(voidBand), 0.0D);
|
||||||
|
assertEquals(0.0D, stats.caveShare(voidBand), 0.0D);
|
||||||
|
assertEquals(1.0D, stats.caveShare(caveBand), 0.0D);
|
||||||
|
assertEquals(0.0D, stats.voidShare(caveBand), 0.0D);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void exportWithoutAWallShellReportsNoWalls() throws IOException {
|
||||||
|
try (VanillaNoiseFidelitySource source = new VanillaNoiseFidelitySource(open(LAND_HEIGHT), Map.of(BIOME_ID, LOAD_KEY), WALL_SHELL)) {
|
||||||
|
assertFalse(source.sample(0, 0).wallsKnown());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void exportedBiomeWithoutALoadKeyIsRejected() throws IOException {
|
||||||
|
try (VanillaNoiseFidelitySource source = new VanillaNoiseFidelitySource(open(LAND_HEIGHT), Map.of(), WALL_SHELL)) {
|
||||||
|
FidelityChunk chunk = source.sample(0, 0);
|
||||||
|
|
||||||
|
IllegalStateException failure = assertThrows(IllegalStateException.class, () -> chunk.surfaceBiome(0, 0));
|
||||||
|
|
||||||
|
assertTrue(failure.getMessage(), failure.getMessage().contains(BIOME_ID));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorldgenExportSampler open(int height) throws IOException {
|
||||||
|
return WorldgenExportSampler.open(flatExport(height), WorldgenExportMode.PRESET, DIMENSION_ID, SEED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonResource wallShell() {
|
||||||
|
return new JsonResource("data/" + ExportIrFixtures.NAMESPACE + "/worldgen/density_function/cave/wall_shell.json", JsonParser.parseString(
|
||||||
|
"{\"type\":\"minecraft:range_choice\",\"input\":{\"type\":\"minecraft:gradient\",\"axis\":\"y\",\"tiling\":\"clamp_to_edge\","
|
||||||
|
+ "\"from_coordinate\":-4064,\"to_coordinate\":4062,\"from_value\":-4064.0,\"to_value\":4062.0},"
|
||||||
|
+ "\"min_inclusive\":" + WALL_MIN_Y + ".0,\"max_exclusive\":" + WALL_MAX_Y + ".0,\"when_in_range\":1.0,\"when_out_of_range\":0.0}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SortedMap<String, byte[]> flatExport(int height, JsonResource... resources) {
|
||||||
|
Field level = new Field.Const(height);
|
||||||
|
Field finalDensity = new Field.Grid(new Field.Binary(Field.BinaryOp.MUL, new Field.Const(0.125),
|
||||||
|
new Field.Binary(Field.BinaryOp.SUB, level, BLOCK_Y)), 4, 8);
|
||||||
|
return export(height, finalDensity, resources);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SortedMap<String, byte[]> carvedExport() {
|
||||||
|
Field ramp = new Field.Binary(Field.BinaryOp.MUL, new Field.Const(0.125), new Field.Binary(Field.BinaryOp.SUB, new Field.Const(CARVED_HEIGHT), BLOCK_Y));
|
||||||
|
Field terrain = new Field.RangeIf(BLOCK_Y, VOID_MIN_Y, VOID_MIN_Y + 16, new Field.Const(-1.0), ramp);
|
||||||
|
Field caves = new Field.RangeIf(BLOCK_Y, CAVE_MIN_Y, CAVE_MIN_Y + 16, new Field.Const(-1.0), new Field.Const(1.0));
|
||||||
|
return export(CARVED_HEIGHT, new Field.Binary(Field.BinaryOp.MIN, terrain, caves));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SortedMap<String, byte[]> export(int height, Field finalDensity, JsonResource... resources) {
|
||||||
|
Field zero = new Field.Const(0.0);
|
||||||
|
Field level = new Field.Const(height);
|
||||||
|
RouterDef router = new RouterDef(zero, zero, zero, zero, zero, zero, new Field.Grid(level, 16, 1), finalDensity, AquiferDef.dry(level));
|
||||||
|
DimensionDef dimension = new DimensionDef(ExportIrFixtures.DIMENSION, ExportIrFixtures.MIN_Y, ExportIrFixtures.HEIGHT,
|
||||||
|
ExportIrFixtures.HEIGHT, ExportIrFixtures.FLUID_HEIGHT, "minecraft:stone", "minecraft:water", ExportIrFixtures.dimensionType());
|
||||||
|
ClimateLayout climate = new ClimateLayout(List.of(new ClimateLayout.ClimateEntry(BIOME_ID, ClimateLayout.ClimateAxis.TEMPERATURE, -2.0, 2.0)));
|
||||||
|
ExportIr ir = new ExportIr(ExportIrFixtures.NAMESPACE, DataVersion.V26_3, WorldgenExportMode.PRESET, dimension, List.of(), router, climate,
|
||||||
|
List.of(new BiomeDef(ExportIrFixtures.BIOME, ExportIrFixtures.biome())), MaterialProgram.rock("minecraft:stone"),
|
||||||
|
List.of(), List.of(resources), List.of());
|
||||||
|
return WorldgenJsonWriter.forTarget(DataVersion.V26_3).write(ir);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ dependencies {
|
|||||||
// log4j-core Filter interface, so its constructor reference needs the type resolvable.
|
// log4j-core Filter interface, so its constructor reference needs the type resolvable.
|
||||||
compileOnly(libs.log4j.core)
|
compileOnly(libs.log4j.core)
|
||||||
testImplementation('junit:junit:4.13.2')
|
testImplementation('junit:junit:4.13.2')
|
||||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
testImplementation(libs.mockito.core)
|
||||||
testImplementation(libs.paper.api)
|
testImplementation(libs.paper.api)
|
||||||
testImplementation(libs.bstats)
|
testImplementation(libs.bstats)
|
||||||
testImplementation(project(':core'))
|
testImplementation(project(':core'))
|
||||||
@@ -28,6 +28,16 @@ dependencies {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String lwjglVersion = libs.versions.lwjgl.get()
|
||||||
|
List<String> mavenLibraries = [
|
||||||
|
"org.lwjgl:lwjgl:${lwjglVersion}",
|
||||||
|
"org.lwjgl:lwjgl:jar:natives-linux:${lwjglVersion}",
|
||||||
|
"org.lwjgl:lwjgl-opengl:${lwjglVersion}",
|
||||||
|
"org.lwjgl:lwjgl-opengl:jar:natives-linux:${lwjglVersion}",
|
||||||
|
"org.lwjgl:lwjgl-egl:${lwjglVersion}",
|
||||||
|
"org.lwjgl:lwjgl-jawt:${lwjglVersion}",
|
||||||
|
].collect { GString coordinate -> coordinate.toString() }
|
||||||
|
|
||||||
tasks.named('processResources').configure {
|
tasks.named('processResources').configure {
|
||||||
def pluginProperties = [
|
def pluginProperties = [
|
||||||
name : rootProject.name,
|
name : rootProject.name,
|
||||||
@@ -35,6 +45,7 @@ tasks.named('processResources').configure {
|
|||||||
apiVersion: apiVersion,
|
apiVersion: apiVersion,
|
||||||
main : mainClass,
|
main : mainClass,
|
||||||
bootstrapper: bootstrapperClass,
|
bootstrapper: bootstrapperClass,
|
||||||
|
libraries : mavenLibraries.collect { String coordinate -> "'${coordinate}'" }.join(', '),
|
||||||
]
|
]
|
||||||
inputs.properties(pluginProperties)
|
inputs.properties(pluginProperties)
|
||||||
filesMatching(['**/paper-plugin.yml', '**/plugin.yml']) {
|
filesMatching(['**/paper-plugin.yml', '**/plugin.yml']) {
|
||||||
|
|||||||
@@ -57,9 +57,12 @@ import art.arcane.iris.localization.IrisLanguage;
|
|||||||
import art.arcane.volmlib.util.director.help.DirectorMiniMenu;
|
import art.arcane.volmlib.util.director.help.DirectorMiniMenu;
|
||||||
import art.arcane.volmlib.util.localization.BukkitLanguageSwitcher;
|
import art.arcane.volmlib.util.localization.BukkitLanguageSwitcher;
|
||||||
import art.arcane.iris.integration.MultiverseCoreLink;
|
import art.arcane.iris.integration.MultiverseCoreLink;
|
||||||
|
import art.arcane.iris.platform.agent.Agent;
|
||||||
|
import art.arcane.iris.platform.agent.AttachSignal;
|
||||||
import art.arcane.iris.platform.bukkit.nms.INMS;
|
import art.arcane.iris.platform.bukkit.nms.INMS;
|
||||||
import art.arcane.volmlib.nativelib.terrain.ServerShutdownBoundary;
|
import art.arcane.volmlib.nativelib.terrain.ServerShutdownBoundary;
|
||||||
import art.arcane.iris.studio.view.BukkitGuiHost;
|
import art.arcane.iris.studio.view.BukkitGuiHost;
|
||||||
|
import art.arcane.iris.studio.view.GuiHost;
|
||||||
import art.arcane.iris.studio.view.PregeneratorJob;
|
import art.arcane.iris.studio.view.PregeneratorJob;
|
||||||
import art.arcane.iris.platform.bukkit.BoardSVC;
|
import art.arcane.iris.platform.bukkit.BoardSVC;
|
||||||
import art.arcane.iris.command.CommandSVC;
|
import art.arcane.iris.command.CommandSVC;
|
||||||
@@ -200,10 +203,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
private final AtomicBoolean postStopFinisherStarted = new AtomicBoolean(false);
|
private final AtomicBoolean postStopFinisherStarted = new AtomicBoolean(false);
|
||||||
private final AtomicBoolean serverStopTeardownDeferred = new AtomicBoolean(false);
|
private final AtomicBoolean serverStopTeardownDeferred = new AtomicBoolean(false);
|
||||||
private final AtomicBoolean servicesDisabled = new AtomicBoolean(false);
|
private final AtomicBoolean servicesDisabled = new AtomicBoolean(false);
|
||||||
|
private final AtomicBoolean runtimeTeardownFailed = new AtomicBoolean(false);
|
||||||
private final AtomicBoolean sharedRuntimeClosed = new AtomicBoolean(false);
|
private final AtomicBoolean sharedRuntimeClosed = new AtomicBoolean(false);
|
||||||
private final AtomicBoolean startupBoundaryRestart = new AtomicBoolean(false);
|
private final AtomicBoolean startupBoundaryRestart = new AtomicBoolean(false);
|
||||||
private final AtomicBoolean terminalCleanupCompleted = new AtomicBoolean(false);
|
private final AtomicBoolean terminalCleanupCompleted = new AtomicBoolean(false);
|
||||||
private final AtomicBoolean runtimeTeardownFailed = new AtomicBoolean(false);
|
|
||||||
private volatile boolean generatorDrainCompleted;
|
private volatile boolean generatorDrainCompleted;
|
||||||
private volatile boolean enableFailed;
|
private volatile boolean enableFailed;
|
||||||
private volatile PlaceholderRegistration papiRegistration;
|
private volatile PlaceholderRegistration papiRegistration;
|
||||||
@@ -224,6 +227,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
private BukkitDebugDump debugDump;
|
private BukkitDebugDump debugDump;
|
||||||
private volatile SettingsHotloadWatch settingsHotloadWatch;
|
private volatile SettingsHotloadWatch settingsHotloadWatch;
|
||||||
private volatile ServerShutdownBoundary serverShutdownBoundary;
|
private volatile ServerShutdownBoundary serverShutdownBoundary;
|
||||||
|
private boolean shutdownProtectionInitialized;
|
||||||
|
|
||||||
public static VolmitSender getSender() {
|
public static VolmitSender getSender() {
|
||||||
VolmitSender current = sender;
|
VolmitSender current = sender;
|
||||||
@@ -473,10 +477,16 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (debug) {
|
if (debug) {
|
||||||
String n = e.getClass().getCanonicalName() + "-" + e.getStackTrace()[0].getClassName() + "-" + e.getStackTrace()[0].getLineNumber();
|
String n = e.getClass().getName();
|
||||||
|
StackTraceElement[] trace = e.getStackTrace();
|
||||||
|
if (trace.length > 0) {
|
||||||
|
n += "-" + trace[0].getClassName() + "-" + trace[0].getLineNumber();
|
||||||
|
}
|
||||||
|
|
||||||
if (e.getCause() != null) {
|
if (e.getCause() != null) {
|
||||||
n += "-" + e.getCause().getStackTrace()[0].getClassName() + "-" + e.getCause().getStackTrace()[0].getLineNumber();
|
StackTraceElement[] causeTrace = e.getCause().getStackTrace();
|
||||||
|
n += "-" + (causeTrace.length == 0 ? e.getCause().getClass().getName()
|
||||||
|
: causeTrace[0].getClassName() + "-" + causeTrace[0].getLineNumber());
|
||||||
}
|
}
|
||||||
|
|
||||||
File f = instance.getDataFile("debug", "caught-exceptions", n + ".txt");
|
File f = instance.getDataFile("debug", "caught-exceptions", n + ".txt");
|
||||||
@@ -570,6 +580,17 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
this::getIrisVersion,
|
this::getIrisVersion,
|
||||||
this::getMCVersion));
|
this::getMCVersion));
|
||||||
SlimJar.load();
|
SlimJar.load();
|
||||||
|
restoreAttachSignal();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void restoreAttachSignal() {
|
||||||
|
try {
|
||||||
|
if (AttachSignal.restoreJvmHandler()) {
|
||||||
|
info("Restored the JVM SIGQUIT handler that the server console replaced");
|
||||||
|
}
|
||||||
|
} catch (RuntimeException failure) {
|
||||||
|
reportError("Could not restore the JVM SIGQUIT handler; the Iris agent may fail to attach.", failure);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void bridgeLog(LogLevel level, String message) {
|
private static void bridgeLog(LogLevel level, String message) {
|
||||||
@@ -626,10 +647,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
postStopFinisherStarted.set(false);
|
postStopFinisherStarted.set(false);
|
||||||
serverStopTeardownDeferred.set(false);
|
serverStopTeardownDeferred.set(false);
|
||||||
servicesDisabled.set(false);
|
servicesDisabled.set(false);
|
||||||
|
runtimeTeardownFailed.set(false);
|
||||||
sharedRuntimeClosed.set(false);
|
sharedRuntimeClosed.set(false);
|
||||||
startupBoundaryRestart.set(false);
|
startupBoundaryRestart.set(false);
|
||||||
terminalCleanupCompleted.set(false);
|
terminalCleanupCompleted.set(false);
|
||||||
runtimeTeardownFailed.set(false);
|
|
||||||
generatorDrainCompleted = false;
|
generatorDrainCompleted = false;
|
||||||
deferredShutdownGenerators.clear();
|
deferredShutdownGenerators.clear();
|
||||||
MultiBurst.burst.reopen();
|
MultiBurst.burst.reopen();
|
||||||
@@ -910,6 +931,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
|
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
enableFailed = false;
|
enableFailed = false;
|
||||||
|
shutdownProtectionInitialized = false;
|
||||||
IrisStartupValidation.begin();
|
IrisStartupValidation.begin();
|
||||||
Bukkit.getPluginManager().registerEvents(new IrisStartupAdmissionListener(), this);
|
Bukkit.getPluginManager().registerEvents(new IrisStartupAdmissionListener(), this);
|
||||||
try {
|
try {
|
||||||
@@ -921,13 +943,14 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
refuseUnsupportedServer();
|
refuseUnsupportedServer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
initializeShutdownProtection();
|
||||||
enable();
|
enable();
|
||||||
} catch (Throwable failure) {
|
} catch (Throwable failure) {
|
||||||
|
enableFailed = true;
|
||||||
if (!FailedEnableLock.engage(failure, FailedEnableLock.irisWorldsPresent(),
|
if (!FailedEnableLock.engage(failure, FailedEnableLock.irisWorldsPresent(),
|
||||||
generatorResolver.refusals().startupWorldsCreated())) {
|
generatorResolver.refusals().startupWorldsCreated())) {
|
||||||
throw failure;
|
throw failure;
|
||||||
}
|
}
|
||||||
enableFailed = true;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
BukkitGuiHost.install();
|
BukkitGuiHost.install();
|
||||||
@@ -945,6 +968,17 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
reportLockedRuntime();
|
reportLockedRuntime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void initializeShutdownProtection() {
|
||||||
|
if (!INMS.get().supportsIrisWorldGeneration()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!Agent.install()) {
|
||||||
|
throw new IllegalStateException("Iris Java agent is unavailable; shutdown protection cannot be installed.");
|
||||||
|
}
|
||||||
|
INMS.get().initializeLifecycle(getClass().getClassLoader());
|
||||||
|
shutdownProtectionInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
private void refuseUnsupportedServer() {
|
private void refuseUnsupportedServer() {
|
||||||
Throwable bindFailure = INMS.bindFailure();
|
Throwable bindFailure = INMS.bindFailure();
|
||||||
IllegalStateException failure = new IllegalStateException("Iris cannot start: " + (bindFailure == null
|
IllegalStateException failure = new IllegalStateException("Iris cannot start: " + (bindFailure == null
|
||||||
@@ -988,6 +1022,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void onDisable() {
|
public void onDisable() {
|
||||||
|
pendingWorldReplacements.awaitStagingForShutdown();
|
||||||
|
GuiHost.shutdown();
|
||||||
preservePendingBlockDrops();
|
preservePendingBlockDrops();
|
||||||
papiLifecycleActive = false;
|
papiLifecycleActive = false;
|
||||||
if (updateNotifier != null) {
|
if (updateNotifier != null) {
|
||||||
@@ -1004,7 +1040,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
}
|
}
|
||||||
IrisLanguage.shutdown();
|
IrisLanguage.shutdown();
|
||||||
teardownPapi();
|
teardownPapi();
|
||||||
boolean deferToServerStop = FailedEnableLock.defersTeardownToServerStop(enableFailed, IrisToolbelt.isServerStopping());
|
boolean deferToServerStop = FailedEnableLock.defersTeardownToServerStop(enableFailed, IrisToolbelt.isServerStopping())
|
||||||
|
&& tryDeferPluginClassLoaderClose();
|
||||||
boolean restartingAtStartupBoundary = startupBoundaryRestart.get();
|
boolean restartingAtStartupBoundary = startupBoundaryRestart.get();
|
||||||
if (restartingAtStartupBoundary) {
|
if (restartingAtStartupBoundary) {
|
||||||
teardownRuntime("startup-boundary-restart", 30L);
|
teardownRuntime("startup-boundary-restart", 30L);
|
||||||
@@ -1036,7 +1073,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
preservePendingBlockDrops();
|
preservePendingBlockDrops();
|
||||||
papiLifecycleActive = false;
|
papiLifecycleActive = false;
|
||||||
teardownPapi();
|
teardownPapi();
|
||||||
if (IrisToolbelt.isServerStopping()) {
|
if (IrisToolbelt.isServerStopping() && tryDeferPluginClassLoaderClose()) {
|
||||||
quiesceRuntimeForServerShutdown("pre-unload:" + reason);
|
quiesceRuntimeForServerShutdown("pre-unload:" + reason);
|
||||||
startPostStopFinisher();
|
startPostStopFinisher();
|
||||||
Iris.debug("Pre-unload hook deferred generator teardown until Paper closes its chunk schedulers.");
|
Iris.debug("Pre-unload hook deferred generator teardown until Paper closes its chunk schedulers.");
|
||||||
@@ -1051,6 +1088,19 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
return CompletableFuture.completedFuture(null);
|
return CompletableFuture.completedFuture(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean tryDeferPluginClassLoaderClose() {
|
||||||
|
if (!shutdownProtectionInitialized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
INMS.get().deferPluginClassLoaderClose();
|
||||||
|
return true;
|
||||||
|
} catch (RuntimeException | LinkageError failure) {
|
||||||
|
Iris.reportError("Failed to retain the Iris plugin class loader; draining shutdown inline.", failure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drains the world generators exactly once. Serialized against the JVM shutdown hook so a
|
* Drains the world generators exactly once. Serialized against the JVM shutdown hook so a
|
||||||
* second caller cannot rip the pools or services out from under an in-flight drain.
|
* second caller cannot rip the pools or services out from under an in-flight drain.
|
||||||
@@ -1097,19 +1147,16 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
try {
|
try {
|
||||||
MultiBurst.burst.close();
|
MultiBurst.burst.close();
|
||||||
} catch (Throwable failure) {
|
} catch (Throwable failure) {
|
||||||
runtimeTeardownFailed.set(true);
|
|
||||||
Iris.reportError("Failed to close Iris generation workers.", failure);
|
Iris.reportError("Failed to close Iris generation workers.", failure);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
MultiBurst.hydrology.close();
|
MultiBurst.hydrology.close();
|
||||||
} catch (Throwable failure) {
|
} catch (Throwable failure) {
|
||||||
runtimeTeardownFailed.set(true);
|
|
||||||
Iris.reportError("Failed to close Iris hydrology workers.", failure);
|
Iris.reportError("Failed to close Iris hydrology workers.", failure);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
MultiBurst.ioBurst.close();
|
MultiBurst.ioBurst.close();
|
||||||
} catch (Throwable failure) {
|
} catch (Throwable failure) {
|
||||||
runtimeTeardownFailed.set(true);
|
|
||||||
Iris.reportError("Failed to close Iris I/O workers.", failure);
|
Iris.reportError("Failed to close Iris I/O workers.", failure);
|
||||||
}
|
}
|
||||||
clearQueues();
|
clearQueues();
|
||||||
@@ -1128,7 +1175,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
|
|
||||||
private void quiesceRuntimeForServerShutdown(String reason) {
|
private void quiesceRuntimeForServerShutdown(String reason) {
|
||||||
serverStopTeardownDeferred.set(true);
|
serverStopTeardownDeferred.set(true);
|
||||||
INMS.get().deferPluginClassLoaderClose();
|
|
||||||
JigsawStudioService jigsawStudioService = IrisServices.getOrNull(JigsawStudioService.class);
|
JigsawStudioService jigsawStudioService = IrisServices.getOrNull(JigsawStudioService.class);
|
||||||
if (jigsawStudioService != null) {
|
if (jigsawStudioService != null) {
|
||||||
try {
|
try {
|
||||||
@@ -1226,30 +1272,37 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
if (!terminalCleanupCompleted.compareAndSet(false, true)) {
|
if (!terminalCleanupCompleted.compareAndSet(false, true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
J.attempt(() -> INMS.get().uninjectBukkit());
|
try {
|
||||||
|
if (INMS.isBound()) {
|
||||||
|
INMS.get().uninjectBukkit();
|
||||||
|
}
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
Iris.reportError("Failed to remove Iris world-generation hooks.", failure);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
runPostShutdown();
|
runPostShutdown();
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
runtimeTeardownFailed.set(true);
|
runtimeTeardownFailed.set(true);
|
||||||
Iris.reportError("Failed to run Iris post-shutdown cleanup.", e);
|
Iris.reportError("Failed to run Iris post-shutdown cleanup.", e);
|
||||||
} finally {
|
} finally {
|
||||||
if (serverStopTeardownDeferred.get() && hasActiveShutdownResources()) {
|
boolean activeResources = serverStopTeardownDeferred.get()
|
||||||
runtimeTeardownFailed.set(true);
|
&& (runtimeTeardownFailed.get() || hasActiveShutdownResources());
|
||||||
SHUTDOWN_ERRORS.println("[Iris] Owned background work remains active after cleanup; retaining the plugin class loader until JVM exit.");
|
|
||||||
}
|
|
||||||
IrisPlatforms.unbind();
|
IrisPlatforms.unbind();
|
||||||
if (serverStopTeardownDeferred.get() && runtimeTeardownFailed.get()) {
|
BukkitPlatform.releaseHost();
|
||||||
SHUTDOWN_ERRORS.println("[Iris] Runtime cleanup failed; retaining the plugin class loader until JVM exit.");
|
if (activeResources) {
|
||||||
|
SHUTDOWN_ERRORS.println("[Iris] Owned background work has not completed cleanup; retaining the plugin class loader until JVM exit.");
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
|
if (shutdownProtectionInitialized) {
|
||||||
INMS.get().releasePluginClassLoaderClose();
|
INMS.get().releasePluginClassLoaderClose();
|
||||||
|
} else {
|
||||||
SlimJar.closeNativeProviders();
|
SlimJar.closeNativeProviders();
|
||||||
|
}
|
||||||
} catch (Throwable failure) {
|
} catch (Throwable failure) {
|
||||||
SHUTDOWN_ERRORS.println("[Iris] Failed to release the plugin class loader after runtime cleanup.");
|
SHUTDOWN_ERRORS.println("[Iris] Failed to release the plugin class loader after runtime cleanup.");
|
||||||
failure.printStackTrace(SHUTDOWN_ERRORS);
|
failure.printStackTrace(SHUTDOWN_ERRORS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BukkitPlatform.releaseHost();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,22 @@ import io.papermc.paper.plugin.bootstrap.PluginProviderContext;
|
|||||||
import io.papermc.paper.plugin.loader.PluginClasspathBuilder;
|
import io.papermc.paper.plugin.loader.PluginClasspathBuilder;
|
||||||
import io.papermc.paper.plugin.loader.PluginLoader;
|
import io.papermc.paper.plugin.loader.PluginLoader;
|
||||||
import io.papermc.paper.plugin.loader.library.impl.JarLibrary;
|
import io.papermc.paper.plugin.loader.library.impl.JarLibrary;
|
||||||
|
import io.papermc.paper.plugin.loader.library.impl.MavenLibraryResolver;
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
|
import org.eclipse.aether.artifact.DefaultArtifact;
|
||||||
|
import org.eclipse.aether.graph.Dependency;
|
||||||
|
import org.eclipse.aether.repository.RemoteRepository;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipFile;
|
||||||
|
|
||||||
@SuppressWarnings("UnstableApiUsage")
|
@SuppressWarnings("UnstableApiUsage")
|
||||||
public final class IrisPluginLoader implements PluginLoader {
|
public final class IrisPluginLoader implements PluginLoader {
|
||||||
@@ -40,6 +50,21 @@ public final class IrisPluginLoader implements PluginLoader {
|
|||||||
classpathBuilder.addLibrary(new JarLibrary(library));
|
classpathBuilder.addLibrary(new JarLibrary(library));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
classpathBuilder.addLibrary(mavenResolver(mavenLibraries(context.getPluginSource())));
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<String> mavenLibraries(Path pluginJar) {
|
||||||
|
try (ZipFile jar = new ZipFile(pluginJar.toFile())) {
|
||||||
|
ZipEntry descriptor = jar.getEntry("plugin.yml");
|
||||||
|
if (descriptor == null) {
|
||||||
|
throw new IllegalStateException("The Iris jar " + pluginJar + " has no plugin.yml library list.");
|
||||||
|
}
|
||||||
|
try (Reader reader = new InputStreamReader(jar.getInputStream(descriptor), StandardCharsets.UTF_8)) {
|
||||||
|
return YamlConfiguration.loadConfiguration(reader).getStringList("libraries");
|
||||||
|
}
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw new IllegalStateException("Unable to read the Iris library list from " + pluginJar + ".", failure);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<Path> relocatedLibraries(Path libraryRoot) {
|
static List<Path> relocatedLibraries(Path libraryRoot) {
|
||||||
@@ -59,6 +84,16 @@ public final class IrisPluginLoader implements PluginLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static MavenLibraryResolver mavenResolver(List<String> coordinates) {
|
||||||
|
MavenLibraryResolver resolver = new MavenLibraryResolver();
|
||||||
|
resolver.addRepository(new RemoteRepository.Builder("central", "default",
|
||||||
|
MavenLibraryResolver.MAVEN_CENTRAL_DEFAULT_MIRROR).build());
|
||||||
|
for (String coordinate : coordinates) {
|
||||||
|
resolver.addDependency(new Dependency(new DefaultArtifact(coordinate), null));
|
||||||
|
}
|
||||||
|
return resolver;
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean isRelocatedLibrary(Path path) {
|
private static boolean isRelocatedLibrary(Path path) {
|
||||||
int count = path.getNameCount();
|
int count = path.getNameCount();
|
||||||
for (int i = 0; i < count - 2; i++) {
|
for (int i = 0; i < count - 2; i++) {
|
||||||
|
|||||||
@@ -18,21 +18,41 @@
|
|||||||
|
|
||||||
package art.arcane.iris.command;
|
package art.arcane.iris.command;
|
||||||
|
|
||||||
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
import art.arcane.iris.Iris;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.iris.command.handlers.DimensionHandler;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
|
import art.arcane.iris.localization.BukkitCommandMessages;
|
||||||
import art.arcane.iris.localization.C;
|
import art.arcane.iris.localization.C;
|
||||||
|
import art.arcane.iris.localization.DirectorCommandMessages;
|
||||||
|
import art.arcane.iris.localization.IrisLanguage;
|
||||||
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
|
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
||||||
|
import art.arcane.iris.pack.datapack.export.DatapackExportDestinations;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportProgress;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportResult;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportService;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportServices;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportTargets;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
|
||||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
import art.arcane.iris.world.task.J;
|
import art.arcane.iris.world.task.J;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import art.arcane.volmlib.util.director.annotations.Director;
|
import art.arcane.volmlib.util.director.annotations.Director;
|
||||||
import art.arcane.volmlib.util.director.annotations.Param;
|
import art.arcane.volmlib.util.director.annotations.Param;
|
||||||
|
import art.arcane.volmlib.util.localization.LanguageAudience;
|
||||||
|
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||||
|
import art.arcane.volmlib.util.localization.TextKey;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
import art.arcane.iris.localization.IrisLanguage;
|
|
||||||
import art.arcane.iris.localization.BukkitCommandMessages;
|
|
||||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
|
||||||
@Director(name = "datapack", aliases = {"datapacks", "dp"}, description = "Download & manage external datapack imports", descriptionKey = "iris.director.commanddatapack.director.download_manage_external_datapack_imports_modrinth")
|
@Director(name = "datapack", aliases = {"datapacks", "dp"}, description = "Download & manage external datapack imports", descriptionKey = "iris.director.commanddatapack.director.download_manage_external_datapack_imports_modrinth")
|
||||||
public class CommandDatapack implements DirectorExecutor {
|
public class CommandDatapack implements DirectorExecutor {
|
||||||
|
private static final int RESOURCE_REPORTS = 4;
|
||||||
|
|
||||||
@Director(description = "Download/update every datapack listed in a pack dimension's 'datapackImports' and install it into the world so its structures register like vanilla", descriptionKey = "iris.director.commanddatapack.director.download_update_every_datapack_listed_pack_dimension_s_datapackimports_install_it_into", aliases = {"pull"})
|
@Director(description = "Download/update every datapack listed in a pack dimension's 'datapackImports' and install it into the world so its structures register like vanilla", descriptionKey = "iris.director.commanddatapack.director.download_update_every_datapack_listed_pack_dimension_s_datapackimports_install_it_into", aliases = {"pull"})
|
||||||
public void ingest(
|
public void ingest(
|
||||||
@Param(description = "Restart the server when new datapacks are installed (required for new structures to register and generate)", descriptionKey = "iris.director.commanddatapack.param.restart_server_when_new_datapacks_are_installed_required_new_structures_register_generate", defaultValue = "false")
|
@Param(description = "Restart the server when new datapacks are installed (required for new structures to register and generate)", descriptionKey = "iris.director.commanddatapack.param.restart_server_when_new_datapacks_are_installed_required_new_structures_register_generate", defaultValue = "false")
|
||||||
@@ -71,4 +91,128 @@ public class CommandDatapack implements DirectorExecutor {
|
|||||||
) {
|
) {
|
||||||
DatapackIngestService.remove(sender(), id);
|
DatapackIngestService.remove(sender(), id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Director(description = "Export a dimension as a vanilla worldgen datapack", descriptionKey = "iris.director.commanddatapack.director.export_dimension_vanilla_worldgen_datapack")
|
||||||
|
public void export(
|
||||||
|
@Param(name = "dimension", description = "The dimension to export", descriptionKey = "iris.director.commanddatapack.param.export_dimension", customHandler = DimensionHandler.class)
|
||||||
|
IrisDimension dimension,
|
||||||
|
@Param(name = "version", description = "Target Minecraft version, 26.3 or later (blank uses the latest)", descriptionKey = "iris.director.commanddatapack.param.export_version", defaultValue = "")
|
||||||
|
String version,
|
||||||
|
@Param(name = "mode", description = "Output mode: preset, dimension, or overworld", descriptionKey = "iris.director.commanddatapack.param.export_mode", defaultValue = WorldgenExportRequest.DEFAULT_MODE)
|
||||||
|
String mode,
|
||||||
|
@Param(name = "namespace", description = "Namespace for the exported resources", descriptionKey = "iris.director.commanddatapack.param.export_namespace", defaultValue = WorldgenExportRequest.DEFAULT_NAMESPACE)
|
||||||
|
String namespace,
|
||||||
|
@Param(name = "output", description = "Output name in the datapack exports folder (blank uses the dimension and version)", descriptionKey = "iris.director.commanddatapack.param.export_output", defaultValue = "")
|
||||||
|
String output,
|
||||||
|
@Param(name = "format", description = "zip or directory", descriptionKey = "iris.director.commanddatapack.param.export_format", defaultValue = WorldgenExportRequest.DEFAULT_FORMAT)
|
||||||
|
String format,
|
||||||
|
@Param(name = "objects", description = "Include objects as structure templates", descriptionKey = "iris.director.commanddatapack.param.export_objects", defaultValue = "true")
|
||||||
|
boolean objects,
|
||||||
|
@Param(name = "replace", description = "Replace an existing export with the same name", descriptionKey = "iris.director.commanddatapack.param.export_replace", defaultValue = "false")
|
||||||
|
boolean replace
|
||||||
|
) {
|
||||||
|
VolmitSender sender = sender();
|
||||||
|
WorldgenExportRequest request = WorldgenExportRequest.builder()
|
||||||
|
.pack(dimension.getLoader().getDataFolder().toPath())
|
||||||
|
.dimension(dimension.getLoadKey())
|
||||||
|
.version(version)
|
||||||
|
.mode(mode)
|
||||||
|
.namespace(namespace)
|
||||||
|
.output(output)
|
||||||
|
.format(format)
|
||||||
|
.objects(objects)
|
||||||
|
.replace(replace)
|
||||||
|
.exportRoot(IrisPlatforms.get().dataFolder("exports", "datapacks").toPath())
|
||||||
|
.build();
|
||||||
|
DataVersion target;
|
||||||
|
try {
|
||||||
|
target = validateExport(request);
|
||||||
|
} catch (IllegalArgumentException invalid) {
|
||||||
|
sender.sendMessage(C.RED + IrisLanguage.text(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_INVALID, MessageArgument.untrusted("reason", invalid.getMessage())));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
WorldgenExportService service = WorldgenExportServices.get();
|
||||||
|
sender.sendMessage(C.GRAY + IrisLanguage.text(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_STARTED, MessageArgument.untrusted("dimension", request.dimension()), MessageArgument.untrusted("version", target.getVersion())));
|
||||||
|
UUID audience = LanguageAudience.current();
|
||||||
|
ExportFeedback feedback = new ExportFeedback(sender);
|
||||||
|
J.a(() -> LanguageAudience.run(audience, () -> runExport(service, request, feedback)));
|
||||||
|
}
|
||||||
|
|
||||||
|
static DataVersion validateExport(WorldgenExportRequest request) {
|
||||||
|
DataVersion target = WorldgenExportTargets.resolve(request.version());
|
||||||
|
WorldgenExportMode.parse(request.mode());
|
||||||
|
DatapackExportDestinations.parseFormat(request.format());
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void runExport(WorldgenExportService service, WorldgenExportRequest request, ExportFeedback feedback) {
|
||||||
|
WorldgenExportResult result;
|
||||||
|
try {
|
||||||
|
result = service.export(request, feedback);
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
Iris.reportError("Datapack export failed for '" + request.dimension() + "'.", failure);
|
||||||
|
feedback.send(C.RED, IrisLanguage.text(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_FAILED, MessageArgument.untrusted("reason", IrisLanguage.errorDetail(failure))));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reportExport(feedback, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void reportExport(ExportFeedback feedback, WorldgenExportResult result) {
|
||||||
|
for (ExportDiagnostic diagnostic : result.diagnostics()) {
|
||||||
|
feedback.send(diagnosticColor(diagnostic.severity()), IrisLanguage.text(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_DIAGNOSTIC, MessageArgument.untrusted("key", diagnosticLabel(diagnostic)), MessageArgument.untrusted("message", diagnostic.message())));
|
||||||
|
}
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
feedback.send(C.GREEN, IrisLanguage.text(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_EXPORTED, MessageArgument.untrusted("path", result.output())));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TextKey outcome = result.status() == WorldgenExportResult.Status.REJECTED
|
||||||
|
? DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_REJECTED
|
||||||
|
: DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_FAILED;
|
||||||
|
feedback.send(C.RED, IrisLanguage.text(outcome, MessageArgument.untrusted("reason", result.message())));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static C diagnosticColor(ExportDiagnostic.Severity severity) {
|
||||||
|
return switch (severity) {
|
||||||
|
case ERROR -> C.RED;
|
||||||
|
case WARNING -> C.YELLOW;
|
||||||
|
case INFO -> C.GRAY;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String diagnosticLabel(ExportDiagnostic diagnostic) {
|
||||||
|
return diagnostic.resource().isEmpty()
|
||||||
|
? diagnostic.limit().name()
|
||||||
|
: diagnostic.limit().name() + " " + diagnostic.resource();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ExportFeedback implements WorldgenExportProgress {
|
||||||
|
private final VolmitSender sender;
|
||||||
|
|
||||||
|
private ExportFeedback(VolmitSender sender) {
|
||||||
|
this.sender = sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stage(String stage) {
|
||||||
|
send(C.GRAY, IrisLanguage.text(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_STAGE, MessageArgument.untrusted("stage", stage)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void resources(int done, int total) {
|
||||||
|
int stride = Math.max(1, total / RESOURCE_REPORTS);
|
||||||
|
if (done != total && done % stride != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
send(C.GRAY, IrisLanguage.text(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_RESOURCES, MessageArgument.untrusted("done", done), MessageArgument.untrusted("total", total)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void send(C color, String message) {
|
||||||
|
String line = color + message;
|
||||||
|
if (sender.isPlayer()) {
|
||||||
|
J.runEntity(sender.player(), () -> sender.sendMessage(line));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sender.sendMessage(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -31,8 +31,7 @@ import art.arcane.iris.structure.placement.IrisStructureLocator;
|
|||||||
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
|
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
|
||||||
import art.arcane.iris.structure.placement.StructureReachability;
|
import art.arcane.iris.structure.placement.StructureReachability;
|
||||||
import art.arcane.iris.world.history.GenerationSemanticQueries;
|
import art.arcane.iris.world.history.GenerationSemanticQueries;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureQuery;
|
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
|
||||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
|
|
||||||
import art.arcane.iris.generation.biome.IrisBiome;
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
|
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
|
||||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||||
@@ -196,9 +195,9 @@ public class CommandFind implements DirectorExecutor {
|
|||||||
EngineBukkitOps.gotoPOI(e, type, player(), teleport);
|
EngineBukkitOps.gotoPOI(e, type, player(), teleport);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Director(description = "Find an accepted hydrology feature", sync = true)
|
@Director(description = "Find a hydrology water feature", sync = true)
|
||||||
public void river(
|
public void river(
|
||||||
@Param(description = "Feature type: surface, waterfall, underground, grotto, mouth, or a deep-fluid id", customHandler = HydrologyTypeHandler.class)
|
@Param(description = "Feature type: stream, river, major_river, cascade, waterfall, mouth, pond, lake, dam, sea_inlet or cave_pool", customHandler = HydrologyTypeHandler.class)
|
||||||
String type,
|
String type,
|
||||||
@Param(description = "Should you be teleported", defaultValue = "true")
|
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||||
boolean teleport
|
boolean teleport
|
||||||
@@ -212,42 +211,32 @@ public class CommandFind implements DirectorExecutor {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
IrisHydrologyRuntime runtime = activeEngine.getComplex().getHydrologyRuntime();
|
HydrologyFeatureType featureType;
|
||||||
HydrologyFeatureQuery query;
|
|
||||||
try {
|
try {
|
||||||
query = HydrologyFeatureQuery.parse(type);
|
featureType = HydrologyFeatureType.parse(type);
|
||||||
} catch (IllegalArgumentException error) {
|
} catch (IllegalArgumentException error) {
|
||||||
commandSender.sendMessage(C.RED + error.getMessage());
|
commandSender.sendMessage(C.RED + error.getMessage());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
String label = featureType.key().replace('_', ' ');
|
||||||
Location origin = target.getLocation();
|
Location origin = target.getLocation();
|
||||||
World originWorld = origin.getWorld();
|
World originWorld = origin.getWorld();
|
||||||
int requestedDistance = runtime == null
|
int maximumDistance = GenerationSemanticQueries.RIVER_SEARCH_DISTANCE;
|
||||||
? 8192
|
commandSender.sendMessage(C.GRAY + GenerationSemanticQueries.riverSearchMessage(featureType, maximumDistance));
|
||||||
: Math.min(8192, runtime.settings().routing().tileSize() * 15);
|
|
||||||
int maximumDistance = runtime == null
|
|
||||||
? requestedDistance
|
|
||||||
: runtime.maximumFeatureSearchDistance(
|
|
||||||
origin.getBlockX(), origin.getBlockZ(), requestedDistance);
|
|
||||||
commandSender.sendMessage(C.GRAY + "Searching accepted hydrology plans for " + type + "...");
|
|
||||||
J.a(() -> {
|
J.a(() -> {
|
||||||
try {
|
try {
|
||||||
GenerationSemanticQueries.RiverResult feature = GenerationSemanticQueries.nearestRiver(
|
GenerationSemanticQueries.RiverResult feature = GenerationSemanticQueries.nearestRiver(
|
||||||
activeEngine,
|
activeEngine,
|
||||||
query,
|
featureType,
|
||||||
origin.getBlockX(),
|
origin.getBlockX(),
|
||||||
origin.getBlockZ(),
|
origin.getBlockZ(),
|
||||||
maximumDistance,
|
maximumDistance
|
||||||
(int visited) -> sendStructureMessage(target, commandSender,
|
|
||||||
C.GRAY + "Searched " + visited + " hydrology tiles for " + type + "...")
|
|
||||||
).orElse(null);
|
).orElse(null);
|
||||||
if (feature == null) {
|
if (feature == null) {
|
||||||
sendStructureMessage(target, commandSender,
|
sendStructureMessage(target, commandSender,
|
||||||
C.YELLOW + "No accepted " + type + " hydrology feature found within "
|
C.YELLOW + GenerationSemanticQueries.riverNotFoundMessage(featureType, maximumDistance));
|
||||||
+ maximumDistance + " blocks.");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String label = type + " hydrology feature";
|
|
||||||
if (!teleport) {
|
if (!teleport) {
|
||||||
sendStructureMessage(target, commandSender,
|
sendStructureMessage(target, commandSender,
|
||||||
C.GREEN + "Found " + label + " at " + feature.x() + ", " + feature.y() + ", " + feature.z() + ".");
|
C.GREEN + "Found " + label + " at " + feature.x() + ", " + feature.y() + ", " + feature.z() + ".");
|
||||||
@@ -259,8 +248,9 @@ public class CommandFind implements DirectorExecutor {
|
|||||||
target, originWorld, commandSender, label, destination, true);
|
target, originWorld, commandSender, label, destination, true);
|
||||||
} catch (Throwable error) {
|
} catch (Throwable error) {
|
||||||
sendStructureMessage(target, commandSender,
|
sendStructureMessage(target, commandSender,
|
||||||
C.RED + "Could not locate " + type + " hydrology: " + error.getClass().getSimpleName());
|
C.RED + "Could not search for " + label + ": " + error.getClass().getSimpleName()
|
||||||
Iris.reportError("Could not locate accepted hydrology feature '" + type + "'.", error);
|
+ (error.getMessage() == null ? "" : " - " + error.getMessage()));
|
||||||
|
Iris.reportError("Could not locate hydrology feature '" + featureType.key() + "'.", error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ import art.arcane.iris.structure.StructureImporter;
|
|||||||
import art.arcane.iris.structure.VillageImporter;
|
import art.arcane.iris.structure.VillageImporter;
|
||||||
import art.arcane.iris.structure.authoring.StructureKey;
|
import art.arcane.iris.structure.authoring.StructureKey;
|
||||||
import art.arcane.iris.structure.authoring.StructureWriteResult;
|
import art.arcane.iris.structure.authoring.StructureWriteResult;
|
||||||
import art.arcane.iris.structure.export.VanillaJigsawExportFormat;
|
import art.arcane.iris.pack.datapack.export.DatapackExportDestinations;
|
||||||
|
import art.arcane.iris.pack.datapack.export.DatapackExportFormat;
|
||||||
import art.arcane.iris.structure.export.VanillaJigsawExportRequest;
|
import art.arcane.iris.structure.export.VanillaJigsawExportRequest;
|
||||||
|
import art.arcane.iris.structure.export.VanillaJigsawExportSettings;
|
||||||
import art.arcane.iris.structure.export.VanillaJigsawExportSource;
|
import art.arcane.iris.structure.export.VanillaJigsawExportSource;
|
||||||
import art.arcane.iris.structure.graph.PlanarJigsawWorkcellResolver;
|
import art.arcane.iris.structure.graph.PlanarJigsawWorkcellResolver;
|
||||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
@@ -419,9 +421,9 @@ public class CommandJigsaw implements DirectorExecutor {
|
|||||||
sendError("Wait for the pending autosave or discard the edits before exporting the on-disk graph.");
|
sendError("Wait for the pending autosave or discard the edits before exporting the on-disk graph.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
VanillaJigsawExportFormat exportFormat;
|
DatapackExportFormat exportFormat;
|
||||||
try {
|
try {
|
||||||
exportFormat = CommandJigsawExports.parseFormat(format);
|
exportFormat = DatapackExportDestinations.parseFormat(format);
|
||||||
} catch (IllegalArgumentException exception) {
|
} catch (IllegalArgumentException exception) {
|
||||||
sendError(exception.getMessage());
|
sendError(exception.getMessage());
|
||||||
return;
|
return;
|
||||||
@@ -430,20 +432,21 @@ public class CommandJigsaw implements DirectorExecutor {
|
|||||||
.toPath().toAbsolutePath().normalize();
|
.toPath().toAbsolutePath().normalize();
|
||||||
Path destination;
|
Path destination;
|
||||||
try {
|
try {
|
||||||
destination = CommandJigsawExports.resolveExportDestination(exportRoot, output, exportFormat);
|
destination = DatapackExportDestinations.resolve(exportRoot, output, exportFormat);
|
||||||
} catch (IllegalArgumentException exception) {
|
} catch (IllegalArgumentException exception) {
|
||||||
sendError(exception.getMessage());
|
sendError(exception.getMessage());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
VanillaJigsawExportRequest request;
|
VanillaJigsawExportRequest request;
|
||||||
try {
|
try {
|
||||||
request = VanillaJigsawExportRequest.builder(
|
VanillaJigsawExportSource source = VanillaJigsawExportSource.forData(
|
||||||
VanillaJigsawExportSource.forData(
|
context.request().source(), context.request().structureKey());
|
||||||
context.request().source(), context.request().structureKey()), destination)
|
request = VanillaJigsawExportRequest.builder(source, destination)
|
||||||
.namespace(namespace)
|
.namespace(namespace)
|
||||||
.resourcePath(context.request().structureKey())
|
.resourcePath(context.request().structureKey())
|
||||||
.format(exportFormat)
|
.format(exportFormat)
|
||||||
.replaceExisting(replace)
|
.replaceExisting(replace)
|
||||||
|
.settings(VanillaJigsawExportSettings.runtimeDefaults(source.loadStructure()))
|
||||||
.build();
|
.build();
|
||||||
} catch (IllegalArgumentException exception) {
|
} catch (IllegalArgumentException exception) {
|
||||||
sendError(exception.getMessage());
|
sendError(exception.getMessage());
|
||||||
|
|||||||
+4
-37
@@ -4,7 +4,7 @@ import art.arcane.iris.Iris;
|
|||||||
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
|
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
|
||||||
import art.arcane.iris.structure.export.VanillaJigsawDatapackExporter;
|
import art.arcane.iris.structure.export.VanillaJigsawDatapackExporter;
|
||||||
import art.arcane.iris.structure.export.VanillaJigsawExportDiagnostic;
|
import art.arcane.iris.structure.export.VanillaJigsawExportDiagnostic;
|
||||||
import art.arcane.iris.structure.export.VanillaJigsawExportFormat;
|
import art.arcane.iris.pack.datapack.export.DatapackExportDestinations;
|
||||||
import art.arcane.iris.structure.export.VanillaJigsawExportRequest;
|
import art.arcane.iris.structure.export.VanillaJigsawExportRequest;
|
||||||
import art.arcane.iris.structure.export.VanillaJigsawExportResult;
|
import art.arcane.iris.structure.export.VanillaJigsawExportResult;
|
||||||
import art.arcane.iris.localization.C;
|
import art.arcane.iris.localization.C;
|
||||||
@@ -13,18 +13,14 @@ import art.arcane.iris.world.task.J;
|
|||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
final class CommandJigsawExports {
|
final class CommandJigsawExports {
|
||||||
private static final Pattern EXPORT_OUTPUT_NAME = Pattern.compile("[a-zA-Z0-9_-][a-zA-Z0-9._-]{0,127}");
|
|
||||||
private static final Set<UUID> EXPORTING_PLAYERS = ConcurrentHashMap.newKeySet();
|
private static final Set<UUID> EXPORTING_PLAYERS = ConcurrentHashMap.newKeySet();
|
||||||
private static final Set<Path> EXPORT_DESTINATIONS = ConcurrentHashMap.newKeySet();
|
|
||||||
|
|
||||||
private CommandJigsawExports() {
|
private CommandJigsawExports() {
|
||||||
}
|
}
|
||||||
@@ -45,11 +41,11 @@ final class CommandJigsawExports {
|
|||||||
|
|
||||||
static boolean beginExport(UUID playerId, Path destination) {
|
static boolean beginExport(UUID playerId, Path destination) {
|
||||||
Objects.requireNonNull(playerId);
|
Objects.requireNonNull(playerId);
|
||||||
Path normalizedDestination = Objects.requireNonNull(destination).toAbsolutePath().normalize();
|
Objects.requireNonNull(destination);
|
||||||
if (!EXPORTING_PLAYERS.add(playerId)) {
|
if (!EXPORTING_PLAYERS.add(playerId)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (EXPORT_DESTINATIONS.add(normalizedDestination)) {
|
if (DatapackExportDestinations.claim(destination)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
EXPORTING_PLAYERS.remove(playerId);
|
EXPORTING_PLAYERS.remove(playerId);
|
||||||
@@ -58,7 +54,7 @@ final class CommandJigsawExports {
|
|||||||
|
|
||||||
static void finishExport(UUID playerId, Path destination) {
|
static void finishExport(UUID playerId, Path destination) {
|
||||||
EXPORTING_PLAYERS.remove(Objects.requireNonNull(playerId));
|
EXPORTING_PLAYERS.remove(Objects.requireNonNull(playerId));
|
||||||
EXPORT_DESTINATIONS.remove(Objects.requireNonNull(destination).toAbsolutePath().normalize());
|
DatapackExportDestinations.release(destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void runExport(ExportOperation operation) {
|
static void runExport(ExportOperation operation) {
|
||||||
@@ -102,35 +98,6 @@ final class CommandJigsawExports {
|
|||||||
+ result.status() + ": " + result.output());
|
+ result.status() + ": " + result.output());
|
||||||
}
|
}
|
||||||
|
|
||||||
static VanillaJigsawExportFormat parseFormat(String value) {
|
|
||||||
return switch (value.trim().toLowerCase(Locale.ROOT)) {
|
|
||||||
case "zip" -> VanillaJigsawExportFormat.ZIP;
|
|
||||||
case "directory", "dir", "folder" -> VanillaJigsawExportFormat.DIRECTORY;
|
|
||||||
default -> throw new IllegalArgumentException("Export format must be directory or zip.");
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static Path resolveExportDestination(
|
|
||||||
Path exportRoot,
|
|
||||||
String output,
|
|
||||||
VanillaJigsawExportFormat format
|
|
||||||
) {
|
|
||||||
Path root = Objects.requireNonNull(exportRoot, "Jigsaw export root").toAbsolutePath().normalize();
|
|
||||||
String requested = Objects.requireNonNull(output, "Jigsaw export output name");
|
|
||||||
String name = requested.trim();
|
|
||||||
if (!name.equals(requested) || !EXPORT_OUTPUT_NAME.matcher(name).matches()) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Export output must be one file name using letters, numbers, '.', '_', or '-'.");
|
|
||||||
}
|
|
||||||
String fileName = format == VanillaJigsawExportFormat.ZIP && !name.endsWith(".zip")
|
|
||||||
? name + ".zip" : name;
|
|
||||||
Path destination = root.resolve(fileName).normalize();
|
|
||||||
if (destination.equals(root) || !root.equals(destination.getParent())) {
|
|
||||||
throw new IllegalArgumentException("Export output must be one artifact inside the Studio exports folder.");
|
|
||||||
}
|
|
||||||
return destination;
|
|
||||||
}
|
|
||||||
|
|
||||||
record ExportOperation(
|
record ExportOperation(
|
||||||
Player player,
|
Player player,
|
||||||
VolmitSender sender,
|
VolmitSender sender,
|
||||||
|
|||||||
@@ -21,10 +21,9 @@ package art.arcane.iris.command;
|
|||||||
import art.arcane.iris.Iris;
|
import art.arcane.iris.Iris;
|
||||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||||
import art.arcane.iris.configuration.IrisSettings;
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
|
import art.arcane.iris.studio.map.MapWindow;
|
||||||
|
import art.arcane.iris.studio.map.PaletteExplorerStart;
|
||||||
import art.arcane.iris.studio.view.GuiHost;
|
import art.arcane.iris.studio.view.GuiHost;
|
||||||
import art.arcane.iris.studio.view.ImageMapStudioGUI;
|
|
||||||
import art.arcane.iris.studio.view.NoiseExplorerGUI;
|
|
||||||
import art.arcane.iris.studio.view.VisionGUI;
|
|
||||||
import art.arcane.iris.pack.loading.IrisData;
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
import art.arcane.iris.studio.workspace.IrisProject;
|
import art.arcane.iris.studio.workspace.IrisProject;
|
||||||
import art.arcane.iris.studio.workspace.IrisCodeWorkspace;
|
import art.arcane.iris.studio.workspace.IrisCodeWorkspace;
|
||||||
@@ -98,11 +97,14 @@ import java.util.Objects;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import art.arcane.iris.localization.IrisLanguage;
|
import art.arcane.iris.localization.IrisLanguage;
|
||||||
import art.arcane.iris.localization.BukkitCommandMessages;
|
import art.arcane.iris.localization.BukkitCommandMessages;
|
||||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||||
|
import art.arcane.volmlib.util.localization.LanguageAudience;
|
||||||
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
|
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
|
||||||
@Director(name = "studio", aliases = {"std", "s"}, description = "Studio Commands", descriptionKey = "iris.director.commandstudio.director.studio_commands")
|
@Director(name = "studio", aliases = {"std", "s"}, description = "Studio Commands", descriptionKey = "iris.director.commandstudio.director.studio_commands")
|
||||||
public class CommandStudio implements DirectorExecutor {
|
public class CommandStudio implements DirectorExecutor {
|
||||||
@@ -202,6 +204,22 @@ public class CommandStudio implements DirectorExecutor {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Director(description = "Hotload the active Studio pack", descriptionKey = "iris.director.commandstudio.director.hotload_active_studio_pack", aliases = {"refresh"}, origin = DirectorOrigin.BOTH)
|
||||||
|
public void hotload() {
|
||||||
|
VolmitSender commandSender = sender();
|
||||||
|
IrisProject project = Iris.service(StudioSVC.class).getActiveProject();
|
||||||
|
PlatformChunkGenerator generator = project == null ? null : project.getActiveProvider();
|
||||||
|
if (generator == null) {
|
||||||
|
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_NO_OPEN_STUDIO_PROJECTS));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UUID audience = LanguageAudience.current();
|
||||||
|
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_HOTLOADING));
|
||||||
|
generator.hotloadAsync().whenComplete((ignored, failure) ->
|
||||||
|
LanguageAudience.run(audience, () -> sendHotloadResult(commandSender, failure)));
|
||||||
|
}
|
||||||
|
|
||||||
@Director(description = "Toggle your Studio debug scoreboard", descriptionKey = "iris.director.commandstudio.director.toggle_your_studio_debug_scoreboard", aliases = {"board", "sidebar", "sb"}, origin = DirectorOrigin.PLAYER)
|
@Director(description = "Toggle your Studio debug scoreboard", descriptionKey = "iris.director.commandstudio.director.toggle_your_studio_debug_scoreboard", aliases = {"board", "sidebar", "sb"}, origin = DirectorOrigin.PLAYER)
|
||||||
public void scoreboard() {
|
public void scoreboard() {
|
||||||
Player target = player();
|
Player target = player();
|
||||||
@@ -255,15 +273,10 @@ public class CommandStudio implements DirectorExecutor {
|
|||||||
long seed
|
long seed
|
||||||
) {
|
) {
|
||||||
if (noGUI()) return;
|
if (noGUI()) return;
|
||||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_NOISE_EXPLORER));
|
VolmitSender commandSender = sender();
|
||||||
|
UUID openerId = commandSender.isPlayer() ? commandSender.player().getUniqueId() : null;
|
||||||
if (generator == null) {
|
MapWindow.NoiseSelection selection = new MapWindow.NoiseSelection(generator == null ? null : generator.getLoadKey(), generator, seed);
|
||||||
NoiseExplorerGUI.launch();
|
withPlayerContext(opener -> openNoiseAtPlayer(commandSender, openerId, opener, selection));
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String generatorKey = generator.getLoadKey();
|
|
||||||
NoiseExplorerGUI.launchGeneratorKey(generatorKey, generator, seed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Director(description = "Open the image-map studio (External GUI)", descriptionKey = "iris.director.commandstudio.director.open_image_map_studio_external_gui", aliases = {"imap"})
|
@Director(description = "Open the image-map studio (External GUI)", descriptionKey = "iris.director.commandstudio.director.open_image_map_studio_external_gui", aliases = {"imap"})
|
||||||
@@ -279,8 +292,7 @@ public class CommandStudio implements DirectorExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Engine activeEngine = IrisToolbelt.access(world).getEngine();
|
requestMapStart(world, MapWindow::openImageMaps);
|
||||||
ImageMapStudioGUI.launch(activeEngine);
|
|
||||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_IMAGE_MAP_STUDIO));
|
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_IMAGE_MAP_STUDIO));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,11 +432,61 @@ public class CommandStudio implements DirectorExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
UUID openerId = sender().isPlayer() ? player().getUniqueId() : null;
|
requestMapStart(world, MapWindow::openMap);
|
||||||
VisionGUI.launch(IrisToolbelt.access(world).getEngine(), openerId);
|
|
||||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_MAP));
|
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_MAP));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The map opens where the player stands when they are in that world, otherwise at the world origin.
|
||||||
|
private PaletteExplorerStart mapStartIn(World world, Player opener, UUID openerId) {
|
||||||
|
Location center = opener != null && opener.getWorld().equals(world) ? opener.getLocation() : null;
|
||||||
|
return new PaletteExplorerStart(
|
||||||
|
IrisToolbelt.access(world).getEngine(),
|
||||||
|
world.getUID(),
|
||||||
|
openerId,
|
||||||
|
null,
|
||||||
|
center == null ? 0D : center.getX(),
|
||||||
|
center == null ? 0D : center.getZ()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestMapStart(World world, Consumer<PaletteExplorerStart> launch) {
|
||||||
|
UUID openerId = sender().isPlayer() ? player().getUniqueId() : null;
|
||||||
|
withPlayerContext(opener -> launch.accept(mapStartIn(world, opener, openerId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openNoiseAtPlayer(VolmitSender commandSender, UUID openerId, Player opener, MapWindow.NoiseSelection selection) {
|
||||||
|
World world = opener == null ? null : opener.getWorld();
|
||||||
|
PlatformChunkGenerator worldGenerator = world == null ? null : IrisToolbelt.access(world);
|
||||||
|
Engine engine = worldGenerator == null ? GuiHost.get().findActiveEngine() : worldGenerator.getEngine();
|
||||||
|
if (engine == null) {
|
||||||
|
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_NEED_BE_SPECIFY_IRIS_GENERATED_WORLD));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PaletteExplorerStart start = worldGenerator == null
|
||||||
|
? new PaletteExplorerStart(engine, null, openerId, null, 0D, 0D)
|
||||||
|
: mapStartIn(world, opener, openerId);
|
||||||
|
MapWindow.openNoise(start, selection);
|
||||||
|
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_NOISE_EXPLORER));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void withPlayerContext(Consumer<Player> action) {
|
||||||
|
Player opener = sender().isPlayer() ? player() : null;
|
||||||
|
if (opener == null) {
|
||||||
|
action.accept(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AtomicBoolean completed = new AtomicBoolean();
|
||||||
|
Consumer<Player> complete = player -> {
|
||||||
|
if (completed.compareAndSet(false, true)) {
|
||||||
|
action.accept(player);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Runnable retired = () -> complete.accept(null);
|
||||||
|
if (!J.runEntity(opener, () -> complete.accept(opener.isOnline() ? opener : null), 0, retired)) {
|
||||||
|
retired.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Director(description = "Profiles the performance of a dimension", descriptionKey = "iris.director.commandstudio.director.profiles_performance_dimension", origin = DirectorOrigin.PLAYER)
|
@Director(description = "Profiles the performance of a dimension", descriptionKey = "iris.director.commandstudio.director.profiles_performance_dimension", origin = DirectorOrigin.PLAYER)
|
||||||
public void profile(
|
public void profile(
|
||||||
@Param(description = "The dimension to profile", descriptionKey = "iris.director.commandstudio.param.dimension_profile", contextual = true, contextualOverride = true, defaultValue = "default", customHandler = DimensionHandler.class)
|
@Param(description = "The dimension to profile", descriptionKey = "iris.director.commandstudio.param.dimension_profile", contextual = true, contextualOverride = true, defaultValue = "default", customHandler = DimensionHandler.class)
|
||||||
@@ -597,6 +659,16 @@ public class CommandStudio implements DirectorExecutor {
|
|||||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_DONE, MessageArgument.untrusted("value", report.getPath())));
|
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_DONE, MessageArgument.untrusted("value", report.getPath())));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void sendHotloadResult(VolmitSender commandSender, Throwable failure) {
|
||||||
|
if (failure != null) {
|
||||||
|
Iris.reportError(failure);
|
||||||
|
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_HOTLOAD_FAILED,
|
||||||
|
MessageArgument.untrusted("value", String.valueOf(failure.getMessage()))));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_HOTLOADED));
|
||||||
|
}
|
||||||
|
|
||||||
private PlatformChunkGenerator resolveProfileGenerator(IrisDimension dimension) {
|
private PlatformChunkGenerator resolveProfileGenerator(IrisDimension dimension) {
|
||||||
StudioSVC studioService = Iris.service(StudioSVC.class);
|
StudioSVC studioService = Iris.service(StudioSVC.class);
|
||||||
if (studioService != null && studioService.isProjectOpen()) {
|
if (studioService != null && studioService.isProjectOpen()) {
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package art.arcane.iris.platform.agent;
|
||||||
|
|
||||||
|
import java.lang.foreign.Arena;
|
||||||
|
import java.lang.foreign.FunctionDescriptor;
|
||||||
|
import java.lang.foreign.Linker;
|
||||||
|
import java.lang.foreign.MemorySegment;
|
||||||
|
import java.lang.foreign.ValueLayout;
|
||||||
|
import java.lang.invoke.MethodHandle;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public final class AttachSignal {
|
||||||
|
private static final int SIGQUIT = 3;
|
||||||
|
private static final int SIGTERM = 15;
|
||||||
|
private static final long ACTION_BYTES = 256;
|
||||||
|
private static final long SIG_IGN = 1L;
|
||||||
|
|
||||||
|
private AttachSignal() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean restoreJvmHandler() {
|
||||||
|
if (System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("win")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Linker linker = Linker.nativeLinker();
|
||||||
|
MethodHandle sigaction = linker.downcallHandle(
|
||||||
|
linker.defaultLookup().find("sigaction")
|
||||||
|
.orElseThrow(() -> new IllegalStateException("sigaction is unavailable")),
|
||||||
|
FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS));
|
||||||
|
try (Arena arena = Arena.ofConfined()) {
|
||||||
|
MemorySegment jvm = arena.allocate(ACTION_BYTES);
|
||||||
|
MemorySegment quit = arena.allocate(ACTION_BYTES);
|
||||||
|
if ((int) sigaction.invokeExact(SIGTERM, MemorySegment.NULL, jvm) != 0
|
||||||
|
|| (int) sigaction.invokeExact(SIGQUIT, MemorySegment.NULL, quit) != 0) {
|
||||||
|
throw new IllegalStateException("sigaction could not read the SIGTERM and SIGQUIT handlers");
|
||||||
|
}
|
||||||
|
long handler = jvm.get(ValueLayout.JAVA_LONG, 0);
|
||||||
|
if (handler == 0L || handler == SIG_IGN || quit.get(ValueLayout.JAVA_LONG, 0) == handler) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ((int) sigaction.invokeExact(SIGQUIT, jvm, MemorySegment.NULL) != 0) {
|
||||||
|
throw new IllegalStateException("sigaction could not restore the JVM SIGQUIT handler");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (RuntimeException | Error failure) {
|
||||||
|
throw failure;
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
throw new IllegalStateException("Unable to restore the JVM SIGQUIT handler", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
-34
@@ -17,8 +17,7 @@ import art.arcane.iris.platform.bukkit.terrain.IrisSampleLimits;
|
|||||||
import art.arcane.iris.platform.bukkit.terrain.IrisSurfaceClassifier;
|
import art.arcane.iris.platform.bukkit.terrain.IrisSurfaceClassifier;
|
||||||
import art.arcane.iris.platform.bukkit.terrain.IrisWorldInfoFactory;
|
import art.arcane.iris.platform.bukkit.terrain.IrisWorldInfoFactory;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
|
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
|
||||||
import art.arcane.iris.generation.terrain.InferredType;
|
import art.arcane.iris.generation.terrain.InferredType;
|
||||||
import art.arcane.iris.generation.biome.IrisBiome;
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||||
@@ -108,15 +107,15 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
int surface = engine.getHeight(blockX, blockZ);
|
int surface = engine.getHeight(blockX, blockZ);
|
||||||
HydrologyColumnSample hydrology = hydrologySample(engine, blockX, blockZ);
|
WaterRole role = engine.getComplex().getWaterRoleStream().get(blockX, blockZ);
|
||||||
int fluid = (int) Math.round(
|
int fluid = (int) Math.round(
|
||||||
engine.getComplex().getRiverWaterSurfaceStream().get(blockX, blockZ));
|
engine.getComplex().getWaterHeadStream().get(blockX, blockZ));
|
||||||
InferredType inferredType = null;
|
InferredType inferredType = null;
|
||||||
if (IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid)) {
|
if (IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid)) {
|
||||||
IrisBiome biome = engine.getSurfaceBiome(blockX, blockZ);
|
IrisBiome biome = engine.getSurfaceBiome(blockX, blockZ);
|
||||||
inferredType = biome == null ? null : biome.getInferredType();
|
inferredType = biome == null ? null : biome.getInferredType();
|
||||||
}
|
}
|
||||||
return IrisSurfaceClassifier.classify(surface, fluid, inferredType, hydrology);
|
return IrisSurfaceClassifier.classify(surface, fluid, inferredType, role);
|
||||||
} catch (Throwable error) {
|
} catch (Throwable error) {
|
||||||
reportQueryFault("surfaceKind", world, error);
|
reportQueryFault("surfaceKind", world, error);
|
||||||
return IrisSurfaceKind.UNKNOWN;
|
return IrisSurfaceKind.UNKNOWN;
|
||||||
@@ -266,12 +265,12 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
HydrologyColumnSample hydrology = wantRiver
|
WaterRole role = wantRiver
|
||||||
? hydrologySample(engine, blockX, blockZ)
|
? engine.getComplex().getWaterRoleStream().get(blockX, blockZ)
|
||||||
: null;
|
: WaterRole.NONE;
|
||||||
int surface = wantHeight || wantKind ? engine.getHeight(blockX, blockZ) : 0;
|
int surface = wantHeight || wantKind ? engine.getHeight(blockX, blockZ) : 0;
|
||||||
int fluid = wantRiver
|
int fluid = wantRiver
|
||||||
? (int) Math.round(engine.getComplex().getRiverWaterSurfaceStream().get(blockX, blockZ))
|
? (int) Math.round(engine.getComplex().getWaterHeadStream().get(blockX, blockZ))
|
||||||
: engine.getDimension().getFluidHeight();
|
: engine.getDimension().getFluidHeight();
|
||||||
boolean needsBiome = wantBiome
|
boolean needsBiome = wantBiome
|
||||||
|| (wantKind && IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid));
|
|| (wantKind && IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid));
|
||||||
@@ -281,29 +280,23 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
|||||||
surface,
|
surface,
|
||||||
fluid,
|
fluid,
|
||||||
biome == null ? null : biome.getInferredType(),
|
biome == null ? null : biome.getInferredType(),
|
||||||
hydrology
|
role
|
||||||
)
|
)
|
||||||
: IrisSurfaceKind.UNKNOWN;
|
: IrisSurfaceKind.UNKNOWN;
|
||||||
String biomeKey = wantBiome && biome != null ? biome.getLoadKey() : null;
|
String biomeKey = wantBiome && biome != null ? biome.getLoadKey() : null;
|
||||||
int natural = wantNaturalHeight
|
int natural = wantNaturalHeight
|
||||||
? (int) Math.round(engine.getComplex().getNaturalHeightStream().get(blockX, blockZ)) + minHeight
|
? (int) Math.round(engine.getComplex().getNaturalHeightStream().get(blockX, blockZ)) + minHeight
|
||||||
: IrisColumnSample.UNAVAILABLE_HEIGHT;
|
: IrisColumnSample.UNAVAILABLE_HEIGHT;
|
||||||
HydrologyColumnLayer surfaceLayer = hydrology == null
|
|
||||||
? null
|
|
||||||
: hydrology.primarySurfaceLayer().orElse(null);
|
|
||||||
boolean riverPresent = surfaceLayer != null;
|
|
||||||
IrisRiverState riverState = wantRiverState
|
IrisRiverState riverState = wantRiverState
|
||||||
? riverState(hydrology)
|
? riverState(role)
|
||||||
: IrisRiverState.NONE;
|
: IrisRiverState.NONE;
|
||||||
double riverDistance = wantRiverDistance && riverPresent
|
double riverDistance = wantRiverDistance
|
||||||
? engine.getComplex().getRiverDistanceStream().get(blockX, blockZ)
|
? riverDistance(role)
|
||||||
: IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE;
|
: IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE;
|
||||||
int riverFlow = wantRiverFlow && riverPresent
|
int riverFlow = wantRiverFlow
|
||||||
? (int) Math.round(engine.getComplex().getRiverFlowStream().get(blockX, blockZ))
|
? riverFlow(role)
|
||||||
: IrisColumnSample.UNAVAILABLE_RIVER_FLOW;
|
: IrisColumnSample.UNAVAILABLE_RIVER_FLOW;
|
||||||
int riverWaterSurfaceY = wantRiverWaterSurface
|
int riverWaterSurfaceY = wantRiverWaterSurface && role.wet()
|
||||||
&& surfaceLayer != null
|
|
||||||
&& surfaceLayer.connectedFluid()
|
|
||||||
? fluid + minHeight
|
? fluid + minHeight
|
||||||
: IrisColumnSample.UNAVAILABLE_HEIGHT;
|
: IrisColumnSample.UNAVAILABLE_HEIGHT;
|
||||||
sink.accept(new IrisColumnSample(
|
sink.accept(new IrisColumnSample(
|
||||||
@@ -327,21 +320,25 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static IrisRiverState riverState(HydrologyColumnSample hydrology) {
|
static IrisRiverState riverState(WaterRole role) {
|
||||||
HydrologyColumnLayer layer = hydrology == null
|
return role.wet() ? IrisRiverState.WET : IrisRiverState.NONE;
|
||||||
? null
|
|
||||||
: hydrology.primarySurfaceLayer().orElse(null);
|
|
||||||
if (layer == null || !layer.channel()) {
|
|
||||||
return IrisRiverState.NONE;
|
|
||||||
}
|
|
||||||
return layer.connectedFluid() ? IrisRiverState.WET : IrisRiverState.DRY;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HydrologyColumnSample hydrologySample(Engine engine, int blockX, int blockZ) {
|
static double riverDistance(WaterRole role) {
|
||||||
if (engine.getComplex().getHydrologyRuntime() == null) {
|
return switch (role) {
|
||||||
return null;
|
case NONE -> IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE;
|
||||||
|
case CHANNEL, POND, MOUTH, CASCADE, WATERFALL, INLET -> 0D;
|
||||||
|
case SHORE, POND_SHORE -> 1D;
|
||||||
|
case BANK, DAM -> 2D;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return engine.getComplex().getHydrologyRuntime().sample(blockX, blockZ).orElse(null);
|
|
||||||
|
static int riverFlow(WaterRole role) {
|
||||||
|
return switch (role) {
|
||||||
|
case NONE, SHORE, POND_SHORE, BANK, DAM -> IrisColumnSample.UNAVAILABLE_RIVER_FLOW;
|
||||||
|
case CHANNEL, MOUTH, CASCADE, WATERFALL -> 1;
|
||||||
|
case POND, INLET -> 0;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Optional<String> key(IrisBiome biome) {
|
private static Optional<String> key(IrisBiome biome) {
|
||||||
|
|||||||
+8
-18
@@ -1,8 +1,7 @@
|
|||||||
package art.arcane.iris.platform.bukkit.terrain;
|
package art.arcane.iris.platform.bukkit.terrain;
|
||||||
|
|
||||||
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
|
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
|
||||||
import art.arcane.iris.generation.terrain.InferredType;
|
import art.arcane.iris.generation.terrain.InferredType;
|
||||||
|
|
||||||
public final class IrisSurfaceClassifier {
|
public final class IrisSurfaceClassifier {
|
||||||
@@ -29,25 +28,16 @@ public final class IrisSurfaceClassifier {
|
|||||||
int engineSurfaceHeight,
|
int engineSurfaceHeight,
|
||||||
int engineFluidHeight,
|
int engineFluidHeight,
|
||||||
InferredType inferredType,
|
InferredType inferredType,
|
||||||
HydrologyColumnSample hydrology
|
WaterRole role
|
||||||
) {
|
) {
|
||||||
if (engineSurfaceHeight <= 0) {
|
if (engineSurfaceHeight <= 0) {
|
||||||
return IrisSurfaceKind.VOID;
|
return IrisSurfaceKind.VOID;
|
||||||
}
|
}
|
||||||
HydrologyColumnLayer layer = hydrology == null
|
return switch (role) {
|
||||||
? null
|
case NONE -> classify(engineSurfaceHeight, engineFluidHeight, inferredType);
|
||||||
: hydrology.primarySurfaceLayer().orElse(null);
|
case SHORE, POND_SHORE -> IrisSurfaceKind.RIVER_SHORE;
|
||||||
if (layer != null) {
|
case BANK, DAM -> IrisSurfaceKind.LAND;
|
||||||
if (layer.shore()) {
|
case CHANNEL, POND, MOUTH, CASCADE, WATERFALL, INLET -> IrisSurfaceKind.RIVER;
|
||||||
return IrisSurfaceKind.RIVER_SHORE;
|
};
|
||||||
}
|
|
||||||
if (layer.channel()) {
|
|
||||||
return layer.connectedFluid() ? IrisSurfaceKind.RIVER : IrisSurfaceKind.DRY_CHANNEL;
|
|
||||||
}
|
|
||||||
if (layer.grading()) {
|
|
||||||
return IrisSurfaceKind.LAND;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return classify(engineSurfaceHeight, engineFluidHeight, inferredType);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+39
-3
@@ -23,6 +23,8 @@ import art.arcane.iris.world.event.IrisEngineHotloadEvent;
|
|||||||
import art.arcane.iris.world.IrisToolbelt;
|
import art.arcane.iris.world.IrisToolbelt;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||||
|
import art.arcane.iris.studio.StudioSVC;
|
||||||
|
import art.arcane.iris.studio.workspace.IrisProject;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.event.EventHandler;
|
import org.bukkit.event.EventHandler;
|
||||||
@@ -40,10 +42,47 @@ public final class BukkitGuiHost implements GuiHost.Provider {
|
|||||||
GuiHost.set(new BukkitGuiHost());
|
GuiHost.set(new BukkitGuiHost());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The engine of the open studio project, which reads the pack folder being edited, or else the first
|
||||||
|
* Iris world. A normal Iris world runs on its own copy of the pack, made when the world was created.
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Engine findActiveEngine() {
|
public Engine findActiveEngine() {
|
||||||
|
Engine studioEngine = findStudioEngine();
|
||||||
|
if (studioEngine != null) {
|
||||||
|
return studioEngine;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
for (World world : new ArrayList<>(Bukkit.getWorlds())) {
|
for (World world : new ArrayList<>(Bukkit.getWorlds())) {
|
||||||
|
Engine engine = engineOf(world);
|
||||||
|
if (engine != null) {
|
||||||
|
return engine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Engine findEngine(UUID worldId) {
|
||||||
|
World world = worldId == null ? null : Bukkit.getWorld(worldId);
|
||||||
|
return world == null ? null : engineOf(world);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Engine findStudioEngine() {
|
||||||
|
try {
|
||||||
|
StudioSVC studio = Iris.service(StudioSVC.class);
|
||||||
|
IrisProject project = studio == null || !studio.isProjectOpen() ? null : studio.getActiveProject();
|
||||||
|
PlatformChunkGenerator provider = project == null ? null : project.getActiveProvider();
|
||||||
|
Engine engine = provider == null ? null : provider.getEngine();
|
||||||
|
return engine == null || engine.isClosed() ? null : engine;
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Engine engineOf(World world) {
|
||||||
try {
|
try {
|
||||||
PlatformChunkGenerator access = IrisToolbelt.access(world);
|
PlatformChunkGenerator access = IrisToolbelt.access(world);
|
||||||
if (access != null && access.getEngine() != null && !access.getEngine().isClosed()) {
|
if (access != null && access.getEngine() != null && !access.getEngine().isClosed()) {
|
||||||
@@ -51,9 +90,6 @@ public final class BukkitGuiHost implements GuiHost.Provider {
|
|||||||
}
|
}
|
||||||
} catch (Throwable ignored) {
|
} catch (Throwable ignored) {
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+83
-40
@@ -18,23 +18,19 @@
|
|||||||
|
|
||||||
package art.arcane.iris.studio.view;
|
package art.arcane.iris.studio.view;
|
||||||
|
|
||||||
import art.arcane.volmlib.nativelib.view.WorldMarker;
|
|
||||||
|
|
||||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.iris.studio.render.RenderType;
|
|
||||||
import art.arcane.iris.world.IrisWorld;
|
import art.arcane.iris.world.IrisWorld;
|
||||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.world.task.J;
|
import art.arcane.iris.world.task.J;
|
||||||
import art.arcane.volmlib.util.format.Form;
|
import art.arcane.volmlib.util.format.Form;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.entity.LivingEntity;
|
import org.bukkit.entity.LivingEntity;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -56,7 +52,7 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
|||||||
private final AtomicBoolean playerRefreshQueued = new AtomicBoolean();
|
private final AtomicBoolean playerRefreshQueued = new AtomicBoolean();
|
||||||
private final AtomicLong teleportSequence = new AtomicLong();
|
private final AtomicLong teleportSequence = new AtomicLong();
|
||||||
private final AtomicReference<VisionTeleportRequest> latestTeleport = new AtomicReference<>();
|
private final AtomicReference<VisionTeleportRequest> latestTeleport = new AtomicReference<>();
|
||||||
private volatile List<WorldMarker> playerMarkers = List.of();
|
private volatile List<GuiMarker> playerMarkers = List.of();
|
||||||
|
|
||||||
public BukkitVisionOverlay(Engine engine, UUID openerId) {
|
public BukkitVisionOverlay(Engine engine, UUID openerId) {
|
||||||
this.engine = engine;
|
this.engine = engine;
|
||||||
@@ -68,7 +64,7 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
|||||||
* built by a server thread.
|
* built by a server thread.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public List<WorldMarker> players() {
|
public List<GuiMarker> players() {
|
||||||
queuePlayerRefresh();
|
queuePlayerRefresh();
|
||||||
return playerMarkers;
|
return playerMarkers;
|
||||||
}
|
}
|
||||||
@@ -78,26 +74,48 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean scheduled = J.runGlobal(() -> {
|
boolean scheduled = J.runGlobal(this::refreshPlayers);
|
||||||
try {
|
|
||||||
List<WorldMarker> markers = new ArrayList<>();
|
|
||||||
for (Player player : BukkitWorldBinding.players(engine.getWorld())) {
|
|
||||||
Location at = player.getLocation();
|
|
||||||
markers.add(WorldMarker.player(player.getName(), at.getX(), at.getZ()));
|
|
||||||
}
|
|
||||||
playerMarkers = List.copyOf(markers);
|
|
||||||
} finally {
|
|
||||||
playerRefreshQueued.set(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!scheduled) {
|
if (!scheduled) {
|
||||||
playerRefreshQueued.set(false);
|
playerRefreshQueued.set(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void refreshPlayers() {
|
||||||
|
World world = BukkitWorldBinding.world(engine.getWorld());
|
||||||
|
List<Player> players = BukkitWorldBinding.players(engine.getWorld());
|
||||||
|
if (players.isEmpty() || world == null) {
|
||||||
|
playerMarkers = List.of();
|
||||||
|
playerRefreshQueued.set(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<GuiMarker> markers = Collections.synchronizedList(new ArrayList<>(players.size()));
|
||||||
|
AtomicInteger pending = new AtomicInteger(players.size());
|
||||||
|
for (Player player : players) {
|
||||||
|
AtomicBoolean completed = new AtomicBoolean();
|
||||||
|
Runnable complete = () -> {
|
||||||
|
if (completed.compareAndSet(false, true) && pending.decrementAndGet() == 0) {
|
||||||
|
playerMarkers = List.copyOf(markers);
|
||||||
|
playerRefreshQueued.set(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (!J.runEntity(player, () -> {
|
||||||
|
try {
|
||||||
|
if (player.isOnline() && player.getWorld() == world) {
|
||||||
|
Location at = player.getLocation();
|
||||||
|
markers.add(GuiMarker.player(player.getName(), at.getX(), at.getZ(), at.getYaw()));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
complete.run();
|
||||||
|
}
|
||||||
|
}, 0, complete)) {
|
||||||
|
complete.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void requestEntities(Consumer<List<WorldMarker>> sink) {
|
public void requestEntities(Consumer<List<GuiMarker>> sink) {
|
||||||
J.runGlobal(() -> {
|
J.runGlobal(() -> {
|
||||||
IrisWorld target = engine.getWorld();
|
IrisWorld target = engine.getWorld();
|
||||||
World world = BukkitWorldBinding.world(target);
|
World world = BukkitWorldBinding.world(target);
|
||||||
@@ -118,7 +136,7 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<WorldMarker> collected = Collections.synchronizedList(new ArrayList<>(living.size()));
|
List<GuiMarker> collected = Collections.synchronizedList(new ArrayList<>(living.size()));
|
||||||
AtomicInteger pending = new AtomicInteger(living.size());
|
AtomicInteger pending = new AtomicInteger(living.size());
|
||||||
Runnable complete = () -> {
|
Runnable complete = () -> {
|
||||||
if (pending.decrementAndGet() == 0) {
|
if (pending.decrementAndGet() == 0) {
|
||||||
@@ -144,22 +162,22 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private WorldMarker marker(LivingEntity entity, Location at) {
|
private GuiMarker marker(LivingEntity entity, Location at) {
|
||||||
String label = Form.capitalizeWords(entity.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " "));
|
String label = Form.capitalizeWords(entity.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " "));
|
||||||
double maxHealth = 0;
|
double maxHealth = 0;
|
||||||
try {
|
try {
|
||||||
maxHealth = entity.getAttribute(MAX_HEALTH).getValue();
|
maxHealth = entity.getAttribute(MAX_HEALTH).getValue();
|
||||||
} catch (Throwable ignored) {
|
} catch (Throwable ignored) {
|
||||||
}
|
}
|
||||||
return WorldMarker.entity(label, at.getX(), at.getY(), at.getZ(), entity.getHealth(), maxHealth);
|
return GuiMarker.entity(label, at.getX(), at.getY(), at.getZ(), entity.getHealth(), maxHealth);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void teleport(double worldX, double worldZ) {
|
public void teleport(double worldX, double worldZ) {
|
||||||
VisionTeleportRequest request = new VisionTeleportRequest(
|
VisionTeleportRequest request = new VisionTeleportRequest(
|
||||||
teleportSequence.incrementAndGet(),
|
teleportSequence.incrementAndGet(),
|
||||||
VisionGUI.floorWorldCoordinate(worldX),
|
(int) StrictMath.floor(worldX),
|
||||||
VisionGUI.floorWorldCoordinate(worldZ));
|
(int) StrictMath.floor(worldZ));
|
||||||
latestTeleport.set(request);
|
latestTeleport.set(request);
|
||||||
startTeleport(request);
|
startTeleport(request);
|
||||||
}
|
}
|
||||||
@@ -212,6 +230,46 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void requestOpenerPosition(Consumer<OpenerPosition> sink) {
|
||||||
|
AtomicBoolean completed = new AtomicBoolean();
|
||||||
|
Consumer<OpenerPosition> complete = position -> {
|
||||||
|
if (completed.compareAndSet(false, true)) {
|
||||||
|
sink.accept(position);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (openerId == null) {
|
||||||
|
complete.accept(OpenerPosition.OFFLINE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean scheduled = J.runGlobal(() -> {
|
||||||
|
Player player = Bukkit.getPlayer(openerId);
|
||||||
|
if (player == null) {
|
||||||
|
complete.accept(OpenerPosition.OFFLINE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Runnable retired = () -> complete.accept(OpenerPosition.OFFLINE);
|
||||||
|
if (!J.runEntity(player, () -> complete.accept(positionOf(player,
|
||||||
|
BukkitWorldBinding.world(engine.getWorld()))), 0, retired)) {
|
||||||
|
retired.run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!scheduled) {
|
||||||
|
complete.accept(OpenerPosition.OFFLINE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OpenerPosition positionOf(Player player, World world) {
|
||||||
|
if (!player.isOnline()) {
|
||||||
|
return OpenerPosition.OFFLINE;
|
||||||
|
}
|
||||||
|
if (world == null || player.getWorld() != world) {
|
||||||
|
return OpenerPosition.IN_OTHER_WORLD;
|
||||||
|
}
|
||||||
|
Location at = player.getLocation();
|
||||||
|
return OpenerPosition.inWorld(at.getX(), at.getY(), at.getZ());
|
||||||
|
}
|
||||||
|
|
||||||
private Player selectPlayer(List<Player> players) {
|
private Player selectPlayer(List<Player> players) {
|
||||||
if (openerId == null) {
|
if (openerId == null) {
|
||||||
return players.isEmpty() ? null : players.get(0);
|
return players.isEmpty() ? null : players.get(0);
|
||||||
@@ -316,21 +374,6 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
|||||||
+ blockX + "," + blockZ + ".", failure);
|
+ blockX + "," + blockZ + ".", failure);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public String openInEditor(double worldX, double worldZ, RenderType type) {
|
|
||||||
IrisComplex complex = engine.getComplex();
|
|
||||||
File file = switch (type) {
|
|
||||||
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT, RIVER ->
|
|
||||||
complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode();
|
|
||||||
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
|
|
||||||
case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode();
|
|
||||||
case REGION -> complex.getRegionStream().get(worldX, worldZ).openInVSCode();
|
|
||||||
case CAVE_LAND -> complex.getCaveBiomeStream().get(worldX, worldZ).openInVSCode();
|
|
||||||
default -> null;
|
|
||||||
};
|
|
||||||
return file == null ? null : file.getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class VisionTeleportRequest {
|
private static final class VisionTeleportRequest {
|
||||||
private final long sequence;
|
private final long sequence;
|
||||||
private final int blockX;
|
private final int blockX;
|
||||||
|
|||||||
+59
-1
@@ -58,6 +58,7 @@ import java.util.Set;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
@@ -78,6 +79,9 @@ public final class PendingWorldReplacementManager implements Listener {
|
|||||||
private volatile UUID overworldEntryAwaitingVerification;
|
private volatile UUID overworldEntryAwaitingVerification;
|
||||||
private volatile World overworldEntryWorld;
|
private volatile World overworldEntryWorld;
|
||||||
private volatile boolean paperEntryListenerRegistered;
|
private volatile boolean paperEntryListenerRegistered;
|
||||||
|
private final Object stagingMonitor = new Object();
|
||||||
|
private boolean stagingAdmissionOpen = true;
|
||||||
|
private StagingAdmission activeStaging;
|
||||||
|
|
||||||
public PendingWorldReplacementManager(Iris plugin) {
|
public PendingWorldReplacementManager(Iris plugin) {
|
||||||
this.plugin = Objects.requireNonNull(plugin, "plugin");
|
this.plugin = Objects.requireNonNull(plugin, "plugin");
|
||||||
@@ -135,7 +139,8 @@ public final class PendingWorldReplacementManager implements Listener {
|
|||||||
LifecycleOperationCoordinator.Domain.WORLD_MUTATION,
|
LifecycleOperationCoordinator.Domain.WORLD_MUTATION,
|
||||||
LifecycleOperationCoordinator.OperationKind.WORLD_REPLACE,
|
LifecycleOperationCoordinator.OperationKind.WORLD_REPLACE,
|
||||||
requiredWorldKey.toString()
|
requiredWorldKey.toString()
|
||||||
); WorldReplacementProgress progress = WorldReplacementProgress.start(
|
); StagingAdmission ignoredAdmission = admitStaging(requiredWorldKey);
|
||||||
|
WorldReplacementProgress progress = WorldReplacementProgress.start(
|
||||||
requiredSender, requiredWorldKey.toString())) {
|
requiredSender, requiredWorldKey.toString())) {
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
IrisStartupValidation.requireWorldReplacementStagingReady();
|
IrisStartupValidation.requireWorldReplacementStagingReady();
|
||||||
@@ -271,6 +276,29 @@ public final class PendingWorldReplacementManager implements Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void awaitStagingForShutdown() {
|
||||||
|
StagingAdmission staging;
|
||||||
|
synchronized (stagingMonitor) {
|
||||||
|
stagingAdmissionOpen = false;
|
||||||
|
staging = activeStaging;
|
||||||
|
}
|
||||||
|
if (staging == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Iris.warn("Waiting for the replacement of " + staging.worldKey + " to finish staging before Iris shuts down.");
|
||||||
|
boolean interrupted = false;
|
||||||
|
while (staging.finished.getCount() > 0L) {
|
||||||
|
try {
|
||||||
|
staging.finished.await();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
interrupted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (interrupted) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public synchronized void processPendingStartupReplacements() {
|
public synchronized void processPendingStartupReplacements() {
|
||||||
ArrayList<String> failures = new ArrayList<>();
|
ArrayList<String> failures = new ArrayList<>();
|
||||||
ArrayList<String> restartBoundaries = new ArrayList<>();
|
ArrayList<String> restartBoundaries = new ArrayList<>();
|
||||||
@@ -1048,6 +1076,17 @@ public final class PendingWorldReplacementManager implements Listener {
|
|||||||
return plugin.getDataFolder().toPath().toAbsolutePath().normalize();
|
return plugin.getDataFolder().toPath().toAbsolutePath().normalize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private StagingAdmission admitStaging(NamespacedKey worldKey) throws IOException {
|
||||||
|
synchronized (stagingMonitor) {
|
||||||
|
if (!stagingAdmissionOpen) {
|
||||||
|
throw new IOException("Iris is shutting down. Run the replacement again after the server starts.");
|
||||||
|
}
|
||||||
|
StagingAdmission staging = new StagingAdmission(worldKey);
|
||||||
|
activeStaging = staging;
|
||||||
|
return staging;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private synchronized void loadOverworldEntryGuard() throws IOException {
|
private synchronized void loadOverworldEntryGuard() throws IOException {
|
||||||
ExactWorldSlotPathPolicy.Target target = resolveTarget(OVERWORLD_KEY);
|
ExactWorldSlotPathPolicy.Target target = resolveTarget(OVERWORLD_KEY);
|
||||||
Optional<WorldReplacementEntryGuard.Entry> loaded = WorldReplacementEntryGuard.load(target.worldDirectory());
|
Optional<WorldReplacementEntryGuard.Entry> loaded = WorldReplacementEntryGuard.load(target.worldDirectory());
|
||||||
@@ -1218,6 +1257,25 @@ public final class PendingWorldReplacementManager implements Listener {
|
|||||||
record SavedLocationInspection(boolean safe, Throwable failure) {
|
record SavedLocationInspection(boolean safe, Throwable failure) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class StagingAdmission implements AutoCloseable {
|
||||||
|
private final NamespacedKey worldKey;
|
||||||
|
private final CountDownLatch finished = new CountDownLatch(1);
|
||||||
|
|
||||||
|
private StagingAdmission(NamespacedKey worldKey) {
|
||||||
|
this.worldKey = worldKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
synchronized (stagingMonitor) {
|
||||||
|
if (activeStaging == this) {
|
||||||
|
activeStaging = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finished.countDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final class RestartBoundaryRequired extends IOException {
|
private static final class RestartBoundaryRequired extends IOException {
|
||||||
private RestartBoundaryRequired(String message) {
|
private RestartBoundaryRequired(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
|
|||||||
@@ -27,3 +27,4 @@ permissions:
|
|||||||
description: Allows survival players to fell Iris-managed trees with an axe.
|
description: Allows survival players to fell Iris-managed trees with an axe.
|
||||||
default: op
|
default: op
|
||||||
api-version: '${apiVersion}'
|
api-version: '${apiVersion}'
|
||||||
|
libraries: [ ${libraries} ]
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { readFile, writeFile } from 'node:fs/promises'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { setup, openStudio, closeStudio, sampleTerrain, readJson, writeJson,
|
||||||
|
logPosition, logSince } from './studio-support.mjs'
|
||||||
|
|
||||||
|
async function hotload(context, fixture, active, command) {
|
||||||
|
const before = await readJson(fixture.manifestPath(active.key))
|
||||||
|
const response = await context.command(command, /^Studio hotloaded\.$|^Studio hotload failed:/, 90000)
|
||||||
|
context.expect(response === 'Studio hotloaded.', 'The manual Studio command did not complete successfully', { command, response })
|
||||||
|
const after = await readJson(fixture.manifestPath(active.key))
|
||||||
|
context.expect(after.pendingActivationId === null, 'The manual hotload completed with a pending activation')
|
||||||
|
context.report.iris.manualHotloads.push({ command, response,
|
||||||
|
before: before.activeActivationId, after: after.activeActivationId })
|
||||||
|
return after
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'iris-studio-manual-hotload',
|
||||||
|
description: 'Refresh an open Studio through hotload and its refresh alias, verify terrain, and reject commands without a Studio.',
|
||||||
|
async run(context) {
|
||||||
|
const fixture = await setup(context)
|
||||||
|
const biomeFile = path.join(fixture.pack, 'biomes', 'flat.json')
|
||||||
|
const original = await readFile(biomeFile, 'utf8')
|
||||||
|
const logOffset = await logPosition(fixture)
|
||||||
|
const processId = (await context.observe()).processId
|
||||||
|
context.report.iris.manualHotloads = []
|
||||||
|
let active
|
||||||
|
try {
|
||||||
|
await context.step('Reject hotload without an open Studio', async () => {
|
||||||
|
await context.command('/iris studio hotload', /^No open studio projects\.$/, 10000)
|
||||||
|
})
|
||||||
|
await context.step('Open Studio and hotload an unchanged pack', async () => {
|
||||||
|
active = await openStudio(context, fixture)
|
||||||
|
await sampleTerrain(context, fixture, 0, fixture.state.material)
|
||||||
|
await hotload(context, fixture, active, '/iris studio hotload')
|
||||||
|
await sampleTerrain(context, fixture, 4096, fixture.state.material)
|
||||||
|
})
|
||||||
|
await context.step('Refresh an edited pack and inspect newly generated terrain', async () => {
|
||||||
|
const before = await readJson(fixture.manifestPath(active.key))
|
||||||
|
const biome = await readJson(biomeFile)
|
||||||
|
biome.name = 'Manual hotload terrain'
|
||||||
|
biome.layers[0].palette[0].block = 'minecraft:gold_block'
|
||||||
|
await writeJson(biomeFile, biome)
|
||||||
|
await hotload(context, fixture, active, '/iris studio refresh')
|
||||||
|
const after = await fixture.wait(async () => {
|
||||||
|
const manifest = await readJson(fixture.manifestPath(active.key))
|
||||||
|
return manifest.activeActivationId !== before.activeActivationId && manifest.pendingActivationId === null ? manifest : false
|
||||||
|
}, 'The edited Studio pack did not become active', 90000)
|
||||||
|
context.report.iris.activations.push({ before: before.activeActivationId,
|
||||||
|
after: after.activeActivationId, material: 'gold_block' })
|
||||||
|
await sampleTerrain(context, fixture, 16384, 'gold_block')
|
||||||
|
await sampleTerrain(context, fixture, 0, fixture.state.material)
|
||||||
|
})
|
||||||
|
await context.step('Close Studio and reject another refresh', async () => {
|
||||||
|
await closeStudio(context, fixture, active)
|
||||||
|
active = undefined
|
||||||
|
await context.command('/iris studio refresh', /^No open studio projects\.$/, 10000)
|
||||||
|
await context.command('/minecraft:seed', /Seed:/i, 10000)
|
||||||
|
})
|
||||||
|
context.expect((await context.observe()).processId === processId, 'The server restarted during manual Studio hotload')
|
||||||
|
const log = await logSince(fixture, logOffset)
|
||||||
|
context.expect(!/Studio generation update failed|Studio hotload failed|Unable to (capture terrain|save the terrain boundary)|ClosedChannelException|Chunk system error|server has stopped responding|could not acquire its engine runtime|Failed to list files/i.test(log),
|
||||||
|
'Manual Studio hotload emitted a generation, snapshot, storage, or watchdog failure')
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
if (active) await closeStudio(context, fixture, active)
|
||||||
|
} finally {
|
||||||
|
await writeFile(biomeFile, original)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -148,6 +148,9 @@ public class FailedEnableLockTest {
|
|||||||
try (EnableFixture fixture = new EnableFixture(false)) {
|
try (EnableFixture fixture = new EnableFixture(false)) {
|
||||||
IllegalStateException failure = assertThrows(IllegalStateException.class, plugin::onEnable);
|
IllegalStateException failure = assertThrows(IllegalStateException.class, plugin::onEnable);
|
||||||
assertTrue(failure.getMessage(), failure.getMessage().contains("already bound"));
|
assertTrue(failure.getMessage(), failure.getMessage().contains("already bound"));
|
||||||
|
Field failed = Iris.class.getDeclaredField("enableFailed");
|
||||||
|
failed.setAccessible(true);
|
||||||
|
assertTrue(failed.getBoolean(plugin));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package art.arcane.iris;
|
||||||
|
|
||||||
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.PrintStream;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class IrisErrorReportingTest {
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void debugReportingPreservesStacklessFailures() throws Exception {
|
||||||
|
ArrayIndexOutOfBoundsException failure = new ArrayIndexOutOfBoundsException("raster index");
|
||||||
|
failure.setStackTrace(new StackTraceElement[0]);
|
||||||
|
|
||||||
|
String output = reportWithDebug(failure);
|
||||||
|
|
||||||
|
assertEquals(1L, output.lines().filter(line -> line.contains("raster index")).count());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void debugReportingPreservesFailureWithStacklessCause() throws Exception {
|
||||||
|
ArrayIndexOutOfBoundsException cause = new ArrayIndexOutOfBoundsException("raster index");
|
||||||
|
cause.setStackTrace(new StackTraceElement[0]);
|
||||||
|
IllegalStateException failure = new IllegalStateException("Hydrology failed", cause);
|
||||||
|
|
||||||
|
String output = reportWithDebug(failure);
|
||||||
|
|
||||||
|
assertEquals(1L, output.lines().filter(line -> line.contains("Hydrology failed")).count());
|
||||||
|
assertTrue(output.contains("Caused by: java.lang.ArrayIndexOutOfBoundsException: raster index"));
|
||||||
|
assertTrue(output.contains("IrisErrorReportingTest.debugReportingPreservesFailureWithStacklessCause"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String reportWithDebug(Throwable failure) throws Exception {
|
||||||
|
Iris previousPlugin = Iris.instance;
|
||||||
|
IrisSettings previousSettings = IrisSettings.settings;
|
||||||
|
PrintStream previousError = System.err;
|
||||||
|
Iris plugin = mock(Iris.class);
|
||||||
|
Field deferred = Iris.class.getDeclaredField("serverStopTeardownDeferred");
|
||||||
|
deferred.setAccessible(true);
|
||||||
|
deferred.set(plugin, new AtomicBoolean());
|
||||||
|
File report = new File(temporaryFolder.getRoot(), "failure.txt");
|
||||||
|
when(plugin.getDataFile(any(String[].class))).thenReturn(report);
|
||||||
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||||
|
try (PrintStream errors = new PrintStream(output, true, StandardCharsets.UTF_8);
|
||||||
|
MockedStatic<Iris> iris = mockStatic(Iris.class)) {
|
||||||
|
Iris.instance = plugin;
|
||||||
|
IrisSettings.settings = new IrisSettings();
|
||||||
|
IrisSettings.settings.getGeneral().setDebug(true);
|
||||||
|
System.setErr(errors);
|
||||||
|
iris.when(() -> Iris.reportError(failure)).thenCallRealMethod();
|
||||||
|
|
||||||
|
Iris.reportError(failure);
|
||||||
|
|
||||||
|
assertTrue(Files.readString(report.toPath()).contains(failure.toString()));
|
||||||
|
return output.toString(StandardCharsets.UTF_8);
|
||||||
|
} finally {
|
||||||
|
System.setErr(previousError);
|
||||||
|
IrisSettings.settings = previousSettings;
|
||||||
|
Iris.instance = previousPlugin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,9 +4,13 @@ import org.junit.Rule;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
@@ -31,4 +35,19 @@ public class IrisPluginLoaderTest {
|
|||||||
|
|
||||||
assertEquals(List.of(relocatedCaffeine, relocatedGson), libraries);
|
assertEquals(List.of(relocatedCaffeine, relocatedGson), libraries);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void readsMavenLibrariesFromTheSpigotDescriptorInThePluginJar() throws Exception {
|
||||||
|
Path jar = temporaryFolder.getRoot().toPath().resolve("Iris.jar");
|
||||||
|
try (OutputStream file = Files.newOutputStream(jar); ZipOutputStream zip = new ZipOutputStream(file)) {
|
||||||
|
zip.putNextEntry(new ZipEntry("plugin.yml"));
|
||||||
|
zip.write("name: Iris\nlibraries: [ 'org.lwjgl:lwjgl:3.4.3', 'org.lwjgl:lwjgl:jar:natives-linux:3.4.3' ]\n"
|
||||||
|
.getBytes(StandardCharsets.UTF_8));
|
||||||
|
zip.closeEntry();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> libraries = IrisPluginLoader.mavenLibraries(jar);
|
||||||
|
|
||||||
|
assertEquals(List.of("org.lwjgl:lwjgl:3.4.3", "org.lwjgl:lwjgl:jar:natives-linux:3.4.3"), libraries);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+269
-2
@@ -1,7 +1,17 @@
|
|||||||
package art.arcane.iris;
|
package art.arcane.iris;
|
||||||
|
|
||||||
import art.arcane.iris.configuration.IrisSettings;
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
|
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.platform.agent.Agent;
|
||||||
|
import art.arcane.iris.platform.bootstrap.SlimJar;
|
||||||
|
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||||
|
import art.arcane.iris.platform.bukkit.plugin.IrisService;
|
||||||
|
import art.arcane.iris.platform.bukkit.plugin.VolmitPlugin;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
|
import art.arcane.iris.world.lifecycle.IrisRuntimeStatics;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import art.arcane.volmlib.util.collection.KMap;
|
||||||
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
||||||
import art.arcane.iris.generation.runtime.GenerationSessionManager;
|
import art.arcane.iris.generation.runtime.GenerationSessionManager;
|
||||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||||
@@ -17,6 +27,7 @@ import art.arcane.iris.world.WorldMaintenance;
|
|||||||
import art.arcane.iris.world.runtime.BukkitEnginePlatformHooks;
|
import art.arcane.iris.world.runtime.BukkitEnginePlatformHooks;
|
||||||
import art.arcane.iris.world.task.J;
|
import art.arcane.iris.world.task.J;
|
||||||
import art.arcane.volmlib.nativelib.terrain.ServerShutdownBoundary;
|
import art.arcane.volmlib.nativelib.terrain.ServerShutdownBoundary;
|
||||||
|
import art.arcane.volmlib.integration.ReloadAware;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.NamespacedKey;
|
import org.bukkit.NamespacedKey;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
@@ -26,10 +37,13 @@ import org.mockito.MockedStatic;
|
|||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
@@ -37,10 +51,16 @@ import java.util.concurrent.atomic.AtomicReference;
|
|||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyLong;
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.doCallRealMethod;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
import static org.mockito.Mockito.mockStatic;
|
import static org.mockito.Mockito.mockStatic;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
@@ -109,8 +129,6 @@ public class IrisShutdownMaintenanceTest {
|
|||||||
}
|
}
|
||||||
verify(fixture.generator).quiesceForServerShutdown();
|
verify(fixture.generator).quiesceForServerShutdown();
|
||||||
verify(fixture.generator, never()).closeAsync();
|
verify(fixture.generator, never()).closeAsync();
|
||||||
fixture.nms.verify(INMS::get);
|
|
||||||
verify(fixture.binding).deferPluginClassLoaderClose();
|
|
||||||
verify(fixture.binding, never()).releasePluginClassLoaderClose();
|
verify(fixture.binding, never()).releasePluginClassLoaderClose();
|
||||||
}
|
}
|
||||||
assertEquals(0, sessions.activeLeases());
|
assertEquals(0, sessions.activeLeases());
|
||||||
@@ -151,6 +169,229 @@ public class IrisShutdownMaintenanceTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void startupEnsuresAgentReadinessBeforeLifecycleProtectionWithoutWorldInjection() throws Exception {
|
||||||
|
try (Fixture fixture = new Fixture(); MockedStatic<Agent> agent = mockStatic(Agent.class)) {
|
||||||
|
when(fixture.binding.supportsIrisWorldGeneration()).thenReturn(true);
|
||||||
|
agent.when(Agent::install).thenReturn(true);
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
agent.verify(Agent::install);
|
||||||
|
assertSame(fixture.plugin.getClass().getClassLoader(), invocation.getArgument(0));
|
||||||
|
return null;
|
||||||
|
}).when(fixture.binding).initializeLifecycle(fixture.plugin.getClass().getClassLoader());
|
||||||
|
invoke(fixture.plugin, "initializeShutdownProtection");
|
||||||
|
verify(fixture.binding).initializeLifecycle(fixture.plugin.getClass().getClassLoader());
|
||||||
|
verify(fixture.binding, never()).injectBukkit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unavailableAgentStopsLifecycleInitialization() throws Exception {
|
||||||
|
try (Fixture fixture = new Fixture(); MockedStatic<Agent> agent = mockStatic(Agent.class)) {
|
||||||
|
when(fixture.binding.supportsIrisWorldGeneration()).thenReturn(true);
|
||||||
|
InvocationTargetException failure = assertThrows(InvocationTargetException.class,
|
||||||
|
() -> invoke(fixture.plugin, "initializeShutdownProtection"));
|
||||||
|
assertTrue(failure.getCause() instanceof IllegalStateException);
|
||||||
|
verify(fixture.binding, never()).initializeLifecycle(fixture.plugin.getClass().getClassLoader());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void worldHookTeardownFailureReleasesProtectionAfterServicesAndWorkersStop() throws Exception {
|
||||||
|
try (Fixture fixture = new Fixture();
|
||||||
|
MockedStatic<Iris> iris = mockStatic(Iris.class);
|
||||||
|
MockedStatic<IrisRuntimeStatics> statics = mockStatic(IrisRuntimeStatics.class);
|
||||||
|
MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class);
|
||||||
|
MockedStatic<BukkitPlatform> bukkitPlatform = mockStatic(BukkitPlatform.class);
|
||||||
|
MockedStatic<SlimJar> providers = mockStatic(SlimJar.class)) {
|
||||||
|
IrisService service = mock(IrisService.class);
|
||||||
|
IllegalStateException failure = new IllegalStateException("world hook teardown failed");
|
||||||
|
fixture.nms.when(INMS::isBound).thenReturn(true);
|
||||||
|
doThrow(failure).when(fixture.binding).uninjectBukkit();
|
||||||
|
setField(fixture.plugin, "services", new KMap<Class<? extends IrisService>, IrisService>());
|
||||||
|
setField(fixture.plugin, "enabledServices", List.of(service));
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
platforms.verify(IrisPlatforms::unbind);
|
||||||
|
bukkitPlatform.verify(BukkitPlatform::releaseHost);
|
||||||
|
assertTrue(MultiBurst.burst.isTerminated());
|
||||||
|
assertTrue(MultiBurst.hydrology.isTerminated());
|
||||||
|
assertTrue(MultiBurst.ioBurst.isTerminated());
|
||||||
|
return null;
|
||||||
|
}).when(fixture.binding).releasePluginClassLoaderClose();
|
||||||
|
fixture.quiesce();
|
||||||
|
try {
|
||||||
|
fixture.finish(5L);
|
||||||
|
fixture.finish(5L);
|
||||||
|
verify(service).onDisable();
|
||||||
|
iris.verify(() -> Iris.reportError("Failed to remove Iris world-generation hooks.", failure));
|
||||||
|
assertTrue(MultiBurst.burst.isTerminated());
|
||||||
|
assertTrue(MultiBurst.hydrology.isTerminated());
|
||||||
|
assertTrue(MultiBurst.ioBurst.isTerminated());
|
||||||
|
verify(fixture.binding).releasePluginClassLoaderClose();
|
||||||
|
platforms.verify(IrisPlatforms::unbind);
|
||||||
|
bukkitPlatform.verify(BukkitPlatform::releaseHost);
|
||||||
|
} finally {
|
||||||
|
MultiBurst.burst.reopen();
|
||||||
|
MultiBurst.hydrology.reopen();
|
||||||
|
MultiBurst.ioBurst.reopen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failedServiceWithAnOwnedExecutorRetainsTheClassLoader() throws Exception {
|
||||||
|
ExecutorService ownedExecutor = Executors.newSingleThreadExecutor();
|
||||||
|
CountDownLatch started = new CountDownLatch(1);
|
||||||
|
CountDownLatch finish = new CountDownLatch(1);
|
||||||
|
ownedExecutor.submit(() -> {
|
||||||
|
started.countDown();
|
||||||
|
finish.await();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
try (Fixture fixture = new Fixture();
|
||||||
|
MockedStatic<Iris> iris = mockStatic(Iris.class);
|
||||||
|
MockedStatic<IrisRuntimeStatics> statics = mockStatic(IrisRuntimeStatics.class);
|
||||||
|
MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class);
|
||||||
|
MockedStatic<BukkitPlatform> bukkitPlatform = mockStatic(BukkitPlatform.class)) {
|
||||||
|
assertTrue(started.await(5, TimeUnit.SECONDS));
|
||||||
|
IrisService service = mock(IrisService.class);
|
||||||
|
IllegalStateException failure = new IllegalStateException("service teardown failed");
|
||||||
|
doThrow(failure).when(service).onDisable();
|
||||||
|
setField(fixture.plugin, "services", new KMap<Class<? extends IrisService>, IrisService>());
|
||||||
|
setField(fixture.plugin, "enabledServices", List.of(service));
|
||||||
|
fixture.quiesce();
|
||||||
|
fixture.finish(5L);
|
||||||
|
verify(service).onDisable();
|
||||||
|
iris.verify(() -> Iris.reportError("Failed to disable " + service.getClass().getSimpleName() + ".", failure));
|
||||||
|
assertFalse(ownedExecutor.isTerminated());
|
||||||
|
verify(fixture.binding, never()).releasePluginClassLoaderClose();
|
||||||
|
} finally {
|
||||||
|
finish.countDown();
|
||||||
|
ownedExecutor.shutdownNow();
|
||||||
|
assertTrue(ownedExecutor.awaitTermination(5, TimeUnit.SECONDS));
|
||||||
|
MultiBurst.burst.reopen();
|
||||||
|
MultiBurst.hydrology.reopen();
|
||||||
|
MultiBurst.ioBurst.reopen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failedPostShutdownCallbackRetainsTheClassLoaderForSkippedCleanup() throws Exception {
|
||||||
|
ExecutorService ownedExecutor = Executors.newSingleThreadExecutor();
|
||||||
|
CountDownLatch started = new CountDownLatch(1);
|
||||||
|
CountDownLatch finish = new CountDownLatch(1);
|
||||||
|
ownedExecutor.submit(() -> {
|
||||||
|
started.countDown();
|
||||||
|
finish.await();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
try (Fixture fixture = new Fixture();
|
||||||
|
MockedStatic<Iris> iris = mockStatic(Iris.class);
|
||||||
|
MockedStatic<IrisRuntimeStatics> statics = mockStatic(IrisRuntimeStatics.class);
|
||||||
|
MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class);
|
||||||
|
MockedStatic<BukkitPlatform> bukkitPlatform = mockStatic(BukkitPlatform.class)) {
|
||||||
|
assertTrue(started.await(5, TimeUnit.SECONDS));
|
||||||
|
IllegalStateException failure = new IllegalStateException("post-shutdown callback failed");
|
||||||
|
AtomicBoolean cleanupAttempted = new AtomicBoolean();
|
||||||
|
KList<Runnable> callbacks = new KList<>();
|
||||||
|
callbacks.add(() -> { throw failure; });
|
||||||
|
callbacks.add(() -> {
|
||||||
|
cleanupAttempted.set(true);
|
||||||
|
ownedExecutor.shutdown();
|
||||||
|
});
|
||||||
|
Field callbacksField = VolmitPlugin.class.getDeclaredField("postShutdown");
|
||||||
|
callbacksField.setAccessible(true);
|
||||||
|
callbacksField.set(fixture.plugin, callbacks);
|
||||||
|
Method runCallbacks = VolmitPlugin.class.getDeclaredMethod("runPostShutdown");
|
||||||
|
runCallbacks.setAccessible(true);
|
||||||
|
runCallbacks.invoke(doCallRealMethod().when(fixture.plugin));
|
||||||
|
fixture.quiesce();
|
||||||
|
fixture.finish(5L);
|
||||||
|
iris.verify(() -> Iris.reportError("Failed to run Iris post-shutdown cleanup.", failure));
|
||||||
|
assertFalse(cleanupAttempted.get());
|
||||||
|
assertFalse(ownedExecutor.isShutdown());
|
||||||
|
assertFalse(ownedExecutor.isTerminated());
|
||||||
|
verify(fixture.binding, never()).releasePluginClassLoaderClose();
|
||||||
|
} finally {
|
||||||
|
finish.countDown();
|
||||||
|
ownedExecutor.shutdownNow();
|
||||||
|
assertTrue(ownedExecutor.awaitTermination(5, TimeUnit.SECONDS));
|
||||||
|
MultiBurst.burst.reopen();
|
||||||
|
MultiBurst.hydrology.reopen();
|
||||||
|
MultiBurst.ioBurst.reopen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void incompleteDrainRetainsServicesWorkersAndClassLoader() throws Exception {
|
||||||
|
try (Fixture fixture = new Fixture(); MockedStatic<Iris> iris = mockStatic(Iris.class)) {
|
||||||
|
IrisService service = mock(IrisService.class);
|
||||||
|
setField(fixture.plugin, "services", new KMap<Class<? extends IrisService>, IrisService>());
|
||||||
|
setField(fixture.plugin, "enabledServices", List.of(service));
|
||||||
|
when(fixture.generator.closeAsync()).thenReturn(new CompletableFuture<>());
|
||||||
|
fixture.quiesce();
|
||||||
|
fixture.finish(0L);
|
||||||
|
verify(service, never()).onDisable();
|
||||||
|
verify(fixture.binding, never()).releasePluginClassLoaderClose();
|
||||||
|
verify(fixture.binding, never()).uninjectBukkit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void serverStopPreUnloadDefersTeardownWhileTheClassLoaderIsHeld() throws Exception {
|
||||||
|
try (Fixture fixture = new Fixture()) {
|
||||||
|
setField(fixture.plugin, "postStopFinisherStarted", new AtomicBoolean(true));
|
||||||
|
try {
|
||||||
|
fixture.commitReloadDuringServerStop();
|
||||||
|
verify(fixture.binding).deferPluginClassLoaderClose();
|
||||||
|
verify(fixture.generator).quiesceForServerShutdown();
|
||||||
|
verify(fixture.generator, never()).closeAsync();
|
||||||
|
assertTrue(fixture.serverStopTeardownDeferred());
|
||||||
|
} finally {
|
||||||
|
WorldMaintenance.endWorldMaintenance(fixture.identity, "pre-unload:HOT_UNLOAD");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void serverStopPreUnloadDrainsInlineWhenTheClassLoaderCannotBeHeld() throws Exception {
|
||||||
|
try (Fixture fixture = new Fixture(); MockedStatic<Iris> iris = mockStatic(Iris.class)) {
|
||||||
|
IllegalStateException failure = new IllegalStateException("class loader hold failed");
|
||||||
|
doThrow(failure).when(fixture.binding).deferPluginClassLoaderClose();
|
||||||
|
try {
|
||||||
|
fixture.commitReloadDuringServerStop();
|
||||||
|
verify(fixture.generator).closeAsync();
|
||||||
|
verify(fixture.generator, never()).quiesceForServerShutdown();
|
||||||
|
assertFalse(fixture.serverStopTeardownDeferred());
|
||||||
|
iris.verify(() -> Iris.reportError("Failed to retain the Iris plugin class loader; draining shutdown inline.", failure));
|
||||||
|
} finally {
|
||||||
|
WorldMaintenance.endWorldMaintenance(fixture.identity, "pre-unload:HOT_UNLOAD", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void serverStopPreUnloadDrainsInlineWithoutInitializedProtection() throws Exception {
|
||||||
|
try (Fixture fixture = new Fixture()) {
|
||||||
|
setField(fixture.plugin, "shutdownProtectionInitialized", false);
|
||||||
|
try {
|
||||||
|
fixture.commitReloadDuringServerStop();
|
||||||
|
verify(fixture.binding, never()).deferPluginClassLoaderClose();
|
||||||
|
verify(fixture.generator).closeAsync();
|
||||||
|
verify(fixture.generator, never()).quiesceForServerShutdown();
|
||||||
|
assertFalse(fixture.serverStopTeardownDeferred());
|
||||||
|
} finally {
|
||||||
|
WorldMaintenance.endWorldMaintenance(fixture.identity, "pre-unload:HOT_UNLOAD", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void invoke(Iris plugin, String name) throws ReflectiveOperationException {
|
||||||
|
Method method = Iris.class.getDeclaredMethod(name);
|
||||||
|
method.setAccessible(true);
|
||||||
|
method.invoke(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
private static void setField(Iris plugin, String name, Object value) throws ReflectiveOperationException {
|
private static void setField(Iris plugin, String name, Object value) throws ReflectiveOperationException {
|
||||||
Field field = Iris.class.getDeclaredField(name);
|
Field field = Iris.class.getDeclaredField(name);
|
||||||
field.setAccessible(true);
|
field.setAccessible(true);
|
||||||
@@ -178,6 +419,12 @@ public class IrisShutdownMaintenanceTest {
|
|||||||
|
|
||||||
private Fixture() throws ReflectiveOperationException {
|
private Fixture() throws ReflectiveOperationException {
|
||||||
IrisSettings.settings = new IrisSettings();
|
IrisSettings.settings = new IrisSettings();
|
||||||
|
setField(plugin, "shutdownProtectionInitialized", true);
|
||||||
|
setField(plugin, "alreadyDrained", new AtomicBoolean());
|
||||||
|
setField(plugin, "servicesDisabled", new AtomicBoolean());
|
||||||
|
setField(plugin, "runtimeTeardownFailed", new AtomicBoolean());
|
||||||
|
setField(plugin, "sharedRuntimeClosed", new AtomicBoolean());
|
||||||
|
setField(plugin, "terminalCleanupCompleted", new AtomicBoolean());
|
||||||
setField(plugin, "serverStopTeardownDeferred", new AtomicBoolean());
|
setField(plugin, "serverStopTeardownDeferred", new AtomicBoolean());
|
||||||
setField(plugin, "postStopFinisherStarted", new AtomicBoolean());
|
setField(plugin, "postStopFinisherStarted", new AtomicBoolean());
|
||||||
setField(plugin, "deferredShutdownGenerators", new CopyOnWriteArrayList<PlatformChunkGenerator>());
|
setField(plugin, "deferredShutdownGenerators", new CopyOnWriteArrayList<PlatformChunkGenerator>());
|
||||||
@@ -206,6 +453,26 @@ public class IrisShutdownMaintenanceTest {
|
|||||||
method.invoke(plugin, "manual_disable", 5L);
|
method.invoke(plugin, "manual_disable", 5L);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void commitReloadDuringServerStop() throws ReflectiveOperationException {
|
||||||
|
setField(plugin, "alreadyDrained", new AtomicBoolean());
|
||||||
|
toolbelt.when(IrisToolbelt::isServerStopping).thenReturn(true);
|
||||||
|
doCallRealMethod().when(plugin).commitReload(any());
|
||||||
|
plugin.commitReload(ReloadAware.PreUnloadReason.HOT_UNLOAD);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean serverStopTeardownDeferred() throws ReflectiveOperationException {
|
||||||
|
Field field = Iris.class.getDeclaredField("serverStopTeardownDeferred");
|
||||||
|
field.setAccessible(true);
|
||||||
|
return ((AtomicBoolean) field.get(plugin)).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void finish(long timeoutSeconds) throws ReflectiveOperationException {
|
||||||
|
Method method = Iris.class.getDeclaredMethod("finishDeferredRuntimeTeardown", String.class, long.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
method.invoke(plugin, "test_shutdown", timeoutSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
private void startFinisher() throws ReflectiveOperationException {
|
private void startFinisher() throws ReflectiveOperationException {
|
||||||
Method method = Iris.class.getDeclaredMethod("startPostStopFinisher");
|
Method method = Iris.class.getDeclaredMethod("startPostStopFinisher");
|
||||||
method.setAccessible(true);
|
method.setAccessible(true);
|
||||||
|
|||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
package art.arcane.iris.command;
|
||||||
|
|
||||||
|
import art.arcane.iris.command.handlers.DimensionHandler;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
|
import art.arcane.iris.localization.IrisLanguage;
|
||||||
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
|
||||||
|
import art.arcane.volmlib.util.director.annotations.Director;
|
||||||
|
import art.arcane.volmlib.util.director.annotations.Param;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.Parameter;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
|
||||||
|
public class CommandDatapackExportContractTest {
|
||||||
|
private static final List<String> NAMES = List.of(
|
||||||
|
"dimension", "version", "mode", "namespace", "output", "format", "objects", "replace");
|
||||||
|
private static final List<String> DEFAULTS = List.of(
|
||||||
|
Param.NO_DEFAULT, "", "overworld", "iris_export", "", "zip", "true", "false");
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void exportParametersAreNamedInOrder() throws Exception {
|
||||||
|
List<String> names = new ArrayList<>();
|
||||||
|
for (Parameter parameter : exportMethod().getParameters()) {
|
||||||
|
names.add(parameter.getAnnotation(Param.class).name());
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(NAMES, names);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void dimensionIsPositionalAndEveryOtherParameterIsKeyedWithItsDefault() throws Exception {
|
||||||
|
Parameter[] parameters = exportMethod().getParameters();
|
||||||
|
List<String> defaults = new ArrayList<>();
|
||||||
|
for (Parameter parameter : parameters) {
|
||||||
|
defaults.add(parameter.getAnnotation(Param.class).defaultValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(DEFAULTS, defaults);
|
||||||
|
assertEquals(DimensionHandler.class, parameters[0].getAnnotation(Param.class).customHandler());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void exportCommandAndParametersUseRegisteredDescriptions() throws Exception {
|
||||||
|
Method export = exportMethod();
|
||||||
|
Director director = export.getAnnotation(Director.class);
|
||||||
|
|
||||||
|
assertNotNull(director);
|
||||||
|
assertEquals(0L, Arrays.stream(director.aliases()).filter((String alias) -> !alias.isBlank()).count());
|
||||||
|
assertNotNull(director.descriptionKey(), IrisLanguage.catalog().key(director.descriptionKey()));
|
||||||
|
for (Parameter parameter : export.getParameters()) {
|
||||||
|
String key = parameter.getAnnotation(Param.class).descriptionKey();
|
||||||
|
assertNotNull(key, IrisLanguage.catalog().key(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validRequestResolvesItsTarget() {
|
||||||
|
assertSame(DataVersion.V26_3, CommandDatapack.validateExport(request().build()));
|
||||||
|
assertSame(DataVersion.V26_3, CommandDatapack.validateExport(request().version("26.3").build()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unsupportedArgumentsAreRejectedWithSharedMessages() {
|
||||||
|
assertEquals("Unsupported datapack export version '1.21.1'. Supported: 26.3",
|
||||||
|
reason(request().version("1.21.1").build()));
|
||||||
|
assertEquals("Unsupported datapack export version '26.2'. Supported: 26.3",
|
||||||
|
reason(request().version("26.2").build()));
|
||||||
|
assertEquals("Unknown export mode 'nether'. Expected: preset, dimension, overworld",
|
||||||
|
reason(request().mode("nether").build()));
|
||||||
|
assertEquals("Export format must be directory or zip.", reason(request().format("rar").build()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private WorldgenExportRequest.Builder request() {
|
||||||
|
Path root = temporaryFolder.getRoot().toPath();
|
||||||
|
return WorldgenExportRequest.builder()
|
||||||
|
.pack(root.resolve("packs").resolve("overworld"))
|
||||||
|
.dimension("overworld")
|
||||||
|
.exportRoot(root.resolve("exports"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String reason(WorldgenExportRequest request) {
|
||||||
|
return assertThrows(IllegalArgumentException.class, () -> CommandDatapack.validateExport(request)).getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Method exportMethod() throws NoSuchMethodException {
|
||||||
|
return CommandDatapack.class.getDeclaredMethod("export", IrisDimension.class, String.class, String.class,
|
||||||
|
String.class, String.class, String.class, boolean.class, boolean.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
+77
-10
@@ -1,7 +1,7 @@
|
|||||||
package art.arcane.iris.command;
|
package art.arcane.iris.command;
|
||||||
|
|
||||||
import art.arcane.iris.Iris;
|
import art.arcane.iris.Iris;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
|
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||||
import art.arcane.iris.localization.IrisLanguage;
|
import art.arcane.iris.localization.IrisLanguage;
|
||||||
@@ -22,6 +22,7 @@ import java.util.OptionalLong;
|
|||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
@@ -45,7 +46,7 @@ public class CommandFindHydrologyTest {
|
|||||||
@Test
|
@Test
|
||||||
public void worldChangeDuringSearchDoesNotLoadOrTeleportIntoTheNewWorld() {
|
public void worldChangeDuringSearchDoesNotLoadOrTeleportIntoTheNewWorld() {
|
||||||
try (Fixture fixture = new Fixture()) {
|
try (Fixture fixture = new Fixture()) {
|
||||||
fixture.command.river("surface", true);
|
fixture.command.river("river", true);
|
||||||
when(fixture.player.getWorld()).thenReturn(mock(World.class));
|
when(fixture.player.getWorld()).thenReturn(mock(World.class));
|
||||||
fixture.search.get().run();
|
fixture.search.get().run();
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ public class CommandFindHydrologyTest {
|
|||||||
@Test
|
@Test
|
||||||
public void disconnectDuringSearchDoesNotLoadTheDestination() {
|
public void disconnectDuringSearchDoesNotLoadTheDestination() {
|
||||||
try (Fixture fixture = new Fixture()) {
|
try (Fixture fixture = new Fixture()) {
|
||||||
fixture.command.river("surface", true);
|
fixture.command.river("river", true);
|
||||||
when(fixture.player.isOnline()).thenReturn(false);
|
when(fixture.player.isOnline()).thenReturn(false);
|
||||||
fixture.search.get().run();
|
fixture.search.get().run();
|
||||||
|
|
||||||
@@ -69,7 +70,7 @@ public class CommandFindHydrologyTest {
|
|||||||
@Test
|
@Test
|
||||||
public void worldChangeWhileLoadingTheDestinationCancelsTeleport() {
|
public void worldChangeWhileLoadingTheDestinationCancelsTeleport() {
|
||||||
try (Fixture fixture = new Fixture()) {
|
try (Fixture fixture = new Fixture()) {
|
||||||
fixture.command.river("surface", true);
|
fixture.command.river("river", true);
|
||||||
fixture.search.get().run();
|
fixture.search.get().run();
|
||||||
when(fixture.player.getWorld()).thenReturn(mock(World.class));
|
when(fixture.player.getWorld()).thenReturn(mock(World.class));
|
||||||
fixture.chunk.complete(mock(Chunk.class));
|
fixture.chunk.complete(mock(Chunk.class));
|
||||||
@@ -82,7 +83,7 @@ public class CommandFindHydrologyTest {
|
|||||||
@Test
|
@Test
|
||||||
public void successIsReportedOnlyAfterTeleportCompletes() {
|
public void successIsReportedOnlyAfterTeleportCompletes() {
|
||||||
try (Fixture fixture = new Fixture()) {
|
try (Fixture fixture = new Fixture()) {
|
||||||
fixture.command.river("surface", true);
|
fixture.command.river("river", true);
|
||||||
fixture.search.get().run();
|
fixture.search.get().run();
|
||||||
fixture.chunk.complete(mock(Chunk.class));
|
fixture.chunk.complete(mock(Chunk.class));
|
||||||
|
|
||||||
@@ -95,7 +96,7 @@ public class CommandFindHydrologyTest {
|
|||||||
@Test
|
@Test
|
||||||
public void rejectedTeleportReportsFailureWithoutSuccess() {
|
public void rejectedTeleportReportsFailureWithoutSuccess() {
|
||||||
try (Fixture fixture = new Fixture()) {
|
try (Fixture fixture = new Fixture()) {
|
||||||
fixture.command.river("surface", true);
|
fixture.command.river("river", true);
|
||||||
fixture.search.get().run();
|
fixture.search.get().run();
|
||||||
fixture.chunk.complete(mock(Chunk.class));
|
fixture.chunk.complete(mock(Chunk.class));
|
||||||
fixture.teleport.complete(false);
|
fixture.teleport.complete(false);
|
||||||
@@ -109,7 +110,7 @@ public class CommandFindHydrologyTest {
|
|||||||
public void failedTeleportReportsTheFullCauseWithoutSuccess() {
|
public void failedTeleportReportsTheFullCauseWithoutSuccess() {
|
||||||
try (Fixture fixture = new Fixture()) {
|
try (Fixture fixture = new Fixture()) {
|
||||||
IllegalStateException failure = new IllegalStateException("destination unloaded");
|
IllegalStateException failure = new IllegalStateException("destination unloaded");
|
||||||
fixture.command.river("surface", true);
|
fixture.command.river("river", true);
|
||||||
fixture.search.get().run();
|
fixture.search.get().run();
|
||||||
fixture.chunk.complete(mock(Chunk.class));
|
fixture.chunk.complete(mock(Chunk.class));
|
||||||
fixture.teleport.completeExceptionally(failure);
|
fixture.teleport.completeExceptionally(failure);
|
||||||
@@ -120,6 +121,72 @@ public class CommandFindHydrologyTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void everyFeatureTypeIsSearchedOffTheCommandThreadAtTheRuntimeRadius() {
|
||||||
|
for (HydrologyFeatureType type : HydrologyFeatureType.values()) {
|
||||||
|
try (Fixture fixture = new Fixture()) {
|
||||||
|
fixture.command.river(type.key(), false);
|
||||||
|
fixture.queries.verify(() -> GenerationSemanticQueries.nearestRiver(any(), any(), anyInt(), anyInt(), anyInt()), never());
|
||||||
|
fixture.search.get().run();
|
||||||
|
|
||||||
|
fixture.queries.verify(() -> GenerationSemanticQueries.nearestRiver(fixture.engine, type, 0, 0,
|
||||||
|
GenerationSemanticQueries.RIVER_SEARCH_DISTANCE));
|
||||||
|
verify(fixture.sender).sendMessage(contains("Found " + type.key().replace('_', ' ') + " at 32, 63, 32."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unknownTypeIsRejectedWithTheAcceptedTypesBeforeAnySearch() {
|
||||||
|
try (Fixture fixture = new Fixture()) {
|
||||||
|
fixture.command.river("surface", true);
|
||||||
|
|
||||||
|
verify(fixture.sender).sendMessage(contains("Use one of: stream, river, major_river, cascade, waterfall, mouth, pond, lake, dam, sea_inlet, cave_pool."));
|
||||||
|
assertNull(fixture.search.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void missingCavePoolIsReportedAsNotFoundInGeneratedChunks() {
|
||||||
|
try (Fixture fixture = new Fixture()) {
|
||||||
|
fixture.queries.when(() -> GenerationSemanticQueries.nearestRiver(eq(fixture.engine), any(), eq(0), eq(0), anyInt()))
|
||||||
|
.thenReturn(Optional.empty());
|
||||||
|
fixture.command.river("cave_pool", true);
|
||||||
|
fixture.search.get().run();
|
||||||
|
|
||||||
|
verify(fixture.sender).sendMessage(contains("Searching generated chunks for the nearest cave pool within 7168 blocks"));
|
||||||
|
verify(fixture.sender).sendMessage(contains("No cave pool found in generated chunks within 7168 blocks."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void missingFeatureIsReportedAsNotFound() {
|
||||||
|
try (Fixture fixture = new Fixture()) {
|
||||||
|
fixture.queries.when(() -> GenerationSemanticQueries.nearestRiver(eq(fixture.engine), any(), eq(0), eq(0), anyInt()))
|
||||||
|
.thenReturn(Optional.empty());
|
||||||
|
fixture.command.river("sea_inlet", true);
|
||||||
|
fixture.search.get().run();
|
||||||
|
|
||||||
|
verify(fixture.sender).sendMessage(contains("No sea inlet found within 7168 blocks."));
|
||||||
|
fixture.platform.verify(() -> BukkitPlatform.chunkAtAsync(any(World.class), anyInt(), anyInt(), eq(true)), never());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void plannerFailureIsReportedAsAnErrorNotAsNotFound() {
|
||||||
|
try (Fixture fixture = new Fixture()) {
|
||||||
|
IllegalStateException failure = new IllegalStateException("window planner exploded");
|
||||||
|
fixture.queries.when(() -> GenerationSemanticQueries.nearestRiver(eq(fixture.engine), any(), eq(0), eq(0), anyInt()))
|
||||||
|
.thenThrow(failure);
|
||||||
|
fixture.command.river("lake", true);
|
||||||
|
fixture.search.get().run();
|
||||||
|
|
||||||
|
verify(fixture.sender).sendMessage(contains("Could not search for lake: IllegalStateException - window planner exploded"));
|
||||||
|
verify(fixture.sender, never()).sendMessage(contains("No lake found"));
|
||||||
|
fixture.logging.verify(() -> Iris.reportError(any(String.class), eq(failure)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final class Fixture implements AutoCloseable {
|
private static final class Fixture implements AutoCloseable {
|
||||||
private final CommandFind command = mock(CommandFind.class, CALLS_REAL_METHODS);
|
private final CommandFind command = mock(CommandFind.class, CALLS_REAL_METHODS);
|
||||||
private final Engine engine = mock(Engine.class);
|
private final Engine engine = mock(Engine.class);
|
||||||
@@ -132,7 +199,7 @@ public class CommandFindHydrologyTest {
|
|||||||
private final CompletableFuture<Boolean> teleport = new CompletableFuture<>();
|
private final CompletableFuture<Boolean> teleport = new CompletableFuture<>();
|
||||||
private final MockedStatic<J> scheduler = mockStatic(J.class);
|
private final MockedStatic<J> scheduler = mockStatic(J.class);
|
||||||
private final MockedStatic<BukkitPlatform> platform = mockStatic(BukkitPlatform.class);
|
private final MockedStatic<BukkitPlatform> platform = mockStatic(BukkitPlatform.class);
|
||||||
private final MockedStatic<GenerationSemanticQueries> queries = mockStatic(GenerationSemanticQueries.class);
|
private final MockedStatic<GenerationSemanticQueries> queries = mockStatic(GenerationSemanticQueries.class, CALLS_REAL_METHODS);
|
||||||
private final MockedStatic<IrisLanguage> language = mockStatic(IrisLanguage.class, invocation -> "teleported");
|
private final MockedStatic<IrisLanguage> language = mockStatic(IrisLanguage.class, invocation -> "teleported");
|
||||||
private final MockedStatic<Iris> logging = mockStatic(Iris.class);
|
private final MockedStatic<Iris> logging = mockStatic(Iris.class);
|
||||||
|
|
||||||
@@ -160,10 +227,10 @@ public class CommandFindHydrologyTest {
|
|||||||
});
|
});
|
||||||
platform.when(() -> BukkitPlatform.chunkAtAsync(any(World.class), anyInt(), anyInt(), eq(true))).thenReturn(chunk);
|
platform.when(() -> BukkitPlatform.chunkAtAsync(any(World.class), anyInt(), anyInt(), eq(true))).thenReturn(chunk);
|
||||||
platform.when(() -> BukkitPlatform.teleportAsync(eq(player), any(Location.class))).thenReturn(teleport);
|
platform.when(() -> BukkitPlatform.teleportAsync(eq(player), any(Location.class))).thenReturn(teleport);
|
||||||
queries.when(() -> GenerationSemanticQueries.nearestRiver(eq(engine), any(), eq(0), eq(0), anyInt(), any()))
|
queries.when(() -> GenerationSemanticQueries.nearestRiver(eq(engine), any(), eq(0), eq(0), anyInt()))
|
||||||
.thenReturn(Optional.of(new GenerationSemanticQueries.RiverResult(
|
.thenReturn(Optional.of(new GenerationSemanticQueries.RiverResult(
|
||||||
GenerationSemanticQueries.RiverSource.ACTIVE_PREDICTION,
|
GenerationSemanticQueries.RiverSource.ACTIVE_PREDICTION,
|
||||||
HydrologyFeatureType.RIFFLE, Optional.empty(), 1L, 32, 63, 32, OptionalLong.empty())));
|
HydrologyFeatureType.RIVER, 1L, 32, 63, 32, OptionalLong.empty())));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
-19
@@ -17,7 +17,6 @@ import art.arcane.iris.structure.authoring.StructureOwnershipManifest;
|
|||||||
import art.arcane.iris.structure.authoring.StructureSource;
|
import art.arcane.iris.structure.authoring.StructureSource;
|
||||||
import art.arcane.iris.structure.authoring.StructureTransactionWriter;
|
import art.arcane.iris.structure.authoring.StructureTransactionWriter;
|
||||||
import art.arcane.iris.structure.conversion.IrisStructureAdoptionInputKind;
|
import art.arcane.iris.structure.conversion.IrisStructureAdoptionInputKind;
|
||||||
import art.arcane.iris.structure.export.VanillaJigsawExportFormat;
|
|
||||||
import art.arcane.iris.world.IrisCreator;
|
import art.arcane.iris.world.IrisCreator;
|
||||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
import art.arcane.iris.pack.value.IrisDirection;
|
import art.arcane.iris.pack.value.IrisDirection;
|
||||||
@@ -413,24 +412,6 @@ public class CommandJigsawContractTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void exportOutputIsOneSafeChildArtifact() {
|
|
||||||
Path root = Path.of("build", "jigsaw-exports").toAbsolutePath().normalize();
|
|
||||||
|
|
||||||
assertEquals(root.resolve("village.zip"), CommandJigsawExports.resolveExportDestination(
|
|
||||||
root, "village", VanillaJigsawExportFormat.ZIP));
|
|
||||||
assertEquals(root.resolve("village"), CommandJigsawExports.resolveExportDestination(
|
|
||||||
root, "village", VanillaJigsawExportFormat.DIRECTORY));
|
|
||||||
assertThrows(IllegalArgumentException.class, () -> CommandJigsawExports.resolveExportDestination(
|
|
||||||
root, "", VanillaJigsawExportFormat.DIRECTORY));
|
|
||||||
assertThrows(IllegalArgumentException.class, () -> CommandJigsawExports.resolveExportDestination(
|
|
||||||
root, ".", VanillaJigsawExportFormat.DIRECTORY));
|
|
||||||
assertThrows(IllegalArgumentException.class, () -> CommandJigsawExports.resolveExportDestination(
|
|
||||||
root, "../all-exports", VanillaJigsawExportFormat.DIRECTORY));
|
|
||||||
assertThrows(IllegalArgumentException.class, () -> CommandJigsawExports.resolveExportDestination(
|
|
||||||
root, "nested/export", VanillaJigsawExportFormat.DIRECTORY));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void exportLeaseRejectsDuplicatePlayerAndDestination() {
|
public void exportLeaseRejectsDuplicatePlayerAndDestination() {
|
||||||
UUID firstPlayer = UUID.randomUUID();
|
UUID firstPlayer = UUID.randomUUID();
|
||||||
|
|||||||
+5
-5
@@ -20,26 +20,26 @@ import static org.mockito.Mockito.when;
|
|||||||
|
|
||||||
public class CommandObjectFluidHeightTest {
|
public class CommandObjectFluidHeightTest {
|
||||||
@Test
|
@Test
|
||||||
public void absoluteObjectPlacementShiftsTheColumnRiverHeadFromNegativeMinY() {
|
public void absoluteObjectPlacementShiftsTheColumnWaterHeadFromNegativeMinY() {
|
||||||
World world = mock(World.class);
|
World world = mock(World.class);
|
||||||
Engine engine = mock(Engine.class);
|
Engine engine = mock(Engine.class);
|
||||||
IrisDimension dimension = mock(IrisDimension.class);
|
IrisDimension dimension = mock(IrisDimension.class);
|
||||||
IrisComplex complex = mock(IrisComplex.class);
|
IrisComplex complex = mock(IrisComplex.class);
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
ProceduralStream<Double> riverHead = mock(ProceduralStream.class);
|
ProceduralStream<Double> waterHead = mock(ProceduralStream.class);
|
||||||
Map<Block, BlockData> future = new HashMap<>();
|
Map<Block, BlockData> future = new HashMap<>();
|
||||||
|
|
||||||
when(engine.getMinHeight()).thenReturn(-64);
|
when(engine.getMinHeight()).thenReturn(-64);
|
||||||
when(engine.getDimension()).thenReturn(dimension);
|
when(engine.getDimension()).thenReturn(dimension);
|
||||||
when(engine.getComplex()).thenReturn(complex);
|
when(engine.getComplex()).thenReturn(complex);
|
||||||
when(dimension.getFluidHeight()).thenReturn(127);
|
when(dimension.getFluidHeight()).thenReturn(127);
|
||||||
when(complex.getRiverWaterSurfaceStream()).thenReturn(riverHead);
|
when(complex.getWaterHeadStream()).thenReturn(waterHead);
|
||||||
when(riverHead.get(12, -7)).thenReturn(131D);
|
when(waterHead.get(12, -7)).thenReturn(131D);
|
||||||
|
|
||||||
IObjectPlacer placer = CommandObject.createPlacer(world, future, engine);
|
IObjectPlacer placer = CommandObject.createPlacer(world, future, engine);
|
||||||
|
|
||||||
assertEquals(63, placer.getFluidHeight());
|
assertEquals(63, placer.getFluidHeight());
|
||||||
assertEquals(67, placer.getFluidHeight(12, -7));
|
assertEquals(67, placer.getFluidHeight(12, -7));
|
||||||
verify(riverHead).get(12, -7);
|
verify(waterHead).get(12, -7);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+112
@@ -0,0 +1,112 @@
|
|||||||
|
package art.arcane.iris.command;
|
||||||
|
|
||||||
|
import art.arcane.iris.Iris;
|
||||||
|
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||||
|
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||||
|
import art.arcane.iris.studio.StudioSVC;
|
||||||
|
import art.arcane.iris.studio.workspace.IrisProject;
|
||||||
|
import art.arcane.volmlib.util.director.annotations.Director;
|
||||||
|
import art.arcane.volmlib.util.localization.LanguageAudience;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class CommandStudioHotloadTest {
|
||||||
|
@Test
|
||||||
|
public void hotloadAndRefreshReportWhenNoStudioIsOpen() throws Exception {
|
||||||
|
RecordingSender sender = new RecordingSender();
|
||||||
|
StudioSVC studio = mock(StudioSVC.class);
|
||||||
|
Method hotload = CommandStudio.class.getDeclaredMethod("hotload");
|
||||||
|
assertTrue(List.of(hotload.getAnnotation(Director.class).aliases()).contains("refresh"));
|
||||||
|
|
||||||
|
try (MockedStatic<Iris> iris = mockStatic(Iris.class)) {
|
||||||
|
iris.when(() -> Iris.service(StudioSVC.class)).thenReturn(studio);
|
||||||
|
hotload.invoke(new TestCommand(sender));
|
||||||
|
assertTrue(sender.messages.getLast(), sender.messages.getLast().contains("No open studio"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void completionFeedbackWaitsForTheHotload() {
|
||||||
|
assertCompletionFeedback(null, "Studio hotloaded.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failedHotloadReportsFailureAndLogsTheCause() {
|
||||||
|
assertCompletionFeedback(new IllegalStateException("Invalid pack"), "Studio hotload failed: Invalid pack");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertCompletionFeedback(Throwable failure, String expected) {
|
||||||
|
RecordingSender sender = new RecordingSender();
|
||||||
|
StudioSVC studio = mock(StudioSVC.class);
|
||||||
|
PlatformChunkGenerator generator = mock(PlatformChunkGenerator.class);
|
||||||
|
IrisProject project = new IrisProject(new File("tutorial"));
|
||||||
|
project.setActiveProvider(generator);
|
||||||
|
CompletableFuture<Void> pending = new CompletableFuture<>();
|
||||||
|
UUID audience = UUID.randomUUID();
|
||||||
|
when(studio.getActiveProject()).thenReturn(project);
|
||||||
|
when(generator.hotloadAsync()).thenReturn(pending);
|
||||||
|
|
||||||
|
try (MockedStatic<Iris> iris = mockStatic(Iris.class)) {
|
||||||
|
iris.when(() -> Iris.service(StudioSVC.class)).thenReturn(studio);
|
||||||
|
LanguageAudience.run(audience, () -> new TestCommand(sender).hotload());
|
||||||
|
assertNull(LanguageAudience.current());
|
||||||
|
|
||||||
|
verify(generator).hotloadAsync();
|
||||||
|
assertEquals(1, sender.messages.size());
|
||||||
|
assertTrue(sender.messages.getFirst().contains("Hotloading Studio"));
|
||||||
|
if (failure == null) {
|
||||||
|
pending.complete(null);
|
||||||
|
} else {
|
||||||
|
pending.completeExceptionally(failure);
|
||||||
|
iris.verify(() -> Iris.reportError(failure));
|
||||||
|
}
|
||||||
|
assertEquals(2, sender.messages.size());
|
||||||
|
assertTrue(sender.messages.getLast(), sender.messages.getLast().contains(expected));
|
||||||
|
assertEquals(List.of(audience, audience), sender.audiences);
|
||||||
|
assertNull(LanguageAudience.current());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class TestCommand extends CommandStudio {
|
||||||
|
private final VolmitSender sender;
|
||||||
|
|
||||||
|
private TestCommand(VolmitSender sender) {
|
||||||
|
this.sender = sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public VolmitSender sender() {
|
||||||
|
return sender;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RecordingSender extends VolmitSender {
|
||||||
|
private final List<String> messages = new ArrayList<>();
|
||||||
|
private final List<UUID> audiences = new ArrayList<>();
|
||||||
|
|
||||||
|
private RecordingSender() {
|
||||||
|
super(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendMessage(String message) {
|
||||||
|
messages.add(message);
|
||||||
|
audiences.add(LanguageAudience.current());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
package art.arcane.iris.command;
|
||||||
|
|
||||||
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||||
|
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||||
|
import art.arcane.iris.studio.map.MapWindow;
|
||||||
|
import art.arcane.iris.studio.map.PaletteExplorerStart;
|
||||||
|
import art.arcane.iris.studio.view.GuiHost;
|
||||||
|
import art.arcane.iris.world.IrisToolbelt;
|
||||||
|
import art.arcane.iris.world.task.J;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.ArgumentMatchers.same;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class StudioMapPositionTest {
|
||||||
|
@Test
|
||||||
|
public void mapCapturesItsStartingPositionOnThePlayerScheduler() {
|
||||||
|
IrisSettings previous = IrisSettings.settings;
|
||||||
|
IrisSettings.settings = new IrisSettings();
|
||||||
|
IrisSettings.settings.getGui().setUseServerLaunchedGuis(true);
|
||||||
|
World world = mock(World.class);
|
||||||
|
Player player = mock(Player.class);
|
||||||
|
VolmitSender sender = mock(VolmitSender.class);
|
||||||
|
PlatformChunkGenerator generator = mock(PlatformChunkGenerator.class);
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
UUID playerId = UUID.randomUUID();
|
||||||
|
UUID worldId = UUID.randomUUID();
|
||||||
|
AtomicBoolean owned = new AtomicBoolean();
|
||||||
|
AtomicReference<Runnable> task = new AtomicReference<>();
|
||||||
|
AtomicReference<PaletteExplorerStart> start = new AtomicReference<>();
|
||||||
|
when(world.getUID()).thenReturn(worldId);
|
||||||
|
when(player.getUniqueId()).thenReturn(playerId);
|
||||||
|
when(player.isOnline()).thenReturn(true);
|
||||||
|
when(player.getWorld()).thenAnswer(invocation -> {
|
||||||
|
assertTrue("world read must use the player scheduler", owned.get());
|
||||||
|
return world;
|
||||||
|
});
|
||||||
|
when(player.getLocation()).thenAnswer(invocation -> {
|
||||||
|
assertTrue("location read must use the player scheduler", owned.get());
|
||||||
|
return new Location(world, 33.25D, 91D, -72.5D);
|
||||||
|
});
|
||||||
|
when(sender.isPlayer()).thenReturn(true);
|
||||||
|
when(sender.player()).thenReturn(player);
|
||||||
|
when(generator.getEngine()).thenReturn(engine);
|
||||||
|
CommandStudio command = new CommandStudio() {
|
||||||
|
@Override
|
||||||
|
public VolmitSender sender() {
|
||||||
|
return sender;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
try (MockedStatic<GuiHost> host = mockStatic(GuiHost.class);
|
||||||
|
MockedStatic<IrisToolbelt> toolbelt = mockStatic(IrisToolbelt.class);
|
||||||
|
MockedStatic<J> scheduling = mockStatic(J.class);
|
||||||
|
MockedStatic<MapWindow> windows = mockStatic(MapWindow.class)) {
|
||||||
|
host.when(GuiHost::isAvailable).thenReturn(true);
|
||||||
|
toolbelt.when(() -> IrisToolbelt.isIrisWorld(world)).thenReturn(true);
|
||||||
|
toolbelt.when(() -> IrisToolbelt.access(world)).thenReturn(generator);
|
||||||
|
scheduling.when(() -> J.runEntity(same(player), any(Runnable.class), eq(0), any(Runnable.class)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
task.set(invocation.getArgument(1));
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
windows.when(() -> MapWindow.openMap(any(PaletteExplorerStart.class))).thenAnswer(invocation -> {
|
||||||
|
start.set(invocation.getArgument(0));
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
command.map(world);
|
||||||
|
assertNull(start.get());
|
||||||
|
owned.set(true);
|
||||||
|
task.get().run();
|
||||||
|
|
||||||
|
assertEquals(engine, start.get().engine());
|
||||||
|
assertEquals(playerId, start.get().playerId());
|
||||||
|
assertEquals(worldId, start.get().worldId());
|
||||||
|
assertEquals(33.25D, start.get().centerX(), 0D);
|
||||||
|
assertEquals(-72.5D, start.get().centerZ(), 0D);
|
||||||
|
} finally {
|
||||||
|
IrisSettings.settings = previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
package art.arcane.iris.platform.agent;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.foreign.Arena;
|
||||||
|
import java.lang.foreign.FunctionDescriptor;
|
||||||
|
import java.lang.foreign.Linker;
|
||||||
|
import java.lang.foreign.MemorySegment;
|
||||||
|
import java.lang.foreign.ValueLayout;
|
||||||
|
import java.lang.invoke.MethodHandle;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.junit.Assume.assumeFalse;
|
||||||
|
|
||||||
|
public class AttachSignalTest {
|
||||||
|
private static final int SIGQUIT = 3;
|
||||||
|
private static final int SIGTERM = 15;
|
||||||
|
private static final long ACTION_BYTES = 256;
|
||||||
|
private static final MethodHandle SIGACTION = Linker.nativeLinker().downcallHandle(
|
||||||
|
Linker.nativeLinker().defaultLookup().find("sigaction").orElseThrow(),
|
||||||
|
FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS));
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void aConsoleThatResetsQuitGetsTheJvmHandlerBack() throws Throwable {
|
||||||
|
assumeFalse(System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win"));
|
||||||
|
long jvmHandler = handler(SIGTERM);
|
||||||
|
assertNotEquals(0L, jvmHandler);
|
||||||
|
try (Arena arena = Arena.ofConfined()) {
|
||||||
|
MemorySegment original = arena.allocate(ACTION_BYTES);
|
||||||
|
assertEquals(0, (int) SIGACTION.invokeExact(SIGQUIT, MemorySegment.NULL, original));
|
||||||
|
try {
|
||||||
|
MemorySegment reset = arena.allocate(ACTION_BYTES);
|
||||||
|
assertEquals(0, (int) SIGACTION.invokeExact(SIGQUIT, reset, MemorySegment.NULL));
|
||||||
|
assertEquals(0L, handler(SIGQUIT));
|
||||||
|
|
||||||
|
assertTrue(AttachSignal.restoreJvmHandler());
|
||||||
|
|
||||||
|
assertEquals(jvmHandler, handler(SIGQUIT));
|
||||||
|
assertFalse(AttachSignal.restoreJvmHandler());
|
||||||
|
} finally {
|
||||||
|
assertEquals(0, (int) SIGACTION.invokeExact(SIGQUIT, original, MemorySegment.NULL));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long handler(int signal) throws Throwable {
|
||||||
|
try (Arena arena = Arena.ofConfined()) {
|
||||||
|
MemorySegment action = arena.allocate(ACTION_BYTES);
|
||||||
|
assertEquals(0, (int) SIGACTION.invokeExact(signal, MemorySegment.NULL, action));
|
||||||
|
return action.get(ValueLayout.JAVA_LONG, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
-45
@@ -9,16 +9,18 @@ import art.arcane.iris.configuration.IrisSettings;
|
|||||||
import art.arcane.iris.generation.biome.IrisBiome;
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
import art.arcane.iris.generation.runtime.BiomeEnvironment;
|
import art.arcane.iris.generation.runtime.BiomeEnvironment;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||||
|
import art.arcane.iris.generation.terrain.InferredType;
|
||||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||||
import art.arcane.iris.pack.loading.IrisData;
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
|
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
|
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
import art.arcane.volmlib.util.stream.interpolation.Interpolated;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureRef;
|
import art.arcane.iris.api.terrain.IrisColumnSample;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
|
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||||
import art.arcane.iris.platform.bukkit.plugin.IrisService;
|
import art.arcane.iris.platform.bukkit.plugin.IrisService;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.generator.ChunkGenerator;
|
import org.bukkit.generator.ChunkGenerator;
|
||||||
@@ -26,6 +28,7 @@ import org.junit.Test;
|
|||||||
import org.mockito.MockedStatic;
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -112,10 +115,32 @@ public class IrisTerrainSVCTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void acceptedHydrologyMapsToThePublicDiagnosticStates() {
|
public void waterRolesMapToThePublicRiverFields() {
|
||||||
assertEquals(IrisRiverState.NONE, IrisTerrainSVC.riverState(null));
|
for (WaterRole role : WaterRole.values()) {
|
||||||
assertEquals(IrisRiverState.WET, IrisTerrainSVC.riverState(river(true)));
|
assertEquals(role.name(), role.wet() ? IrisRiverState.WET : IrisRiverState.NONE, IrisTerrainSVC.riverState(role));
|
||||||
assertEquals(IrisRiverState.DRY, IrisTerrainSVC.riverState(river(false)));
|
double distance = switch (role) {
|
||||||
|
case NONE -> IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE;
|
||||||
|
case CHANNEL, POND, MOUTH, CASCADE, WATERFALL, INLET -> 0D;
|
||||||
|
case SHORE, POND_SHORE -> 1D;
|
||||||
|
case BANK, DAM -> 2D;
|
||||||
|
};
|
||||||
|
assertEquals(role.name(), distance, IrisTerrainSVC.riverDistance(role), 0D);
|
||||||
|
int flow = switch (role) {
|
||||||
|
case NONE, SHORE, POND_SHORE, BANK, DAM -> IrisColumnSample.UNAVAILABLE_RIVER_FLOW;
|
||||||
|
case CHANNEL, MOUTH, CASCADE, WATERFALL -> 1;
|
||||||
|
case POND, INLET -> 0;
|
||||||
|
};
|
||||||
|
assertEquals(role.name(), flow, IrisTerrainSVC.riverFlow(role));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void columnSamplesReportTheWaterRoleOfEachColumn() throws Exception {
|
||||||
|
assertEquals(List.of("RIVER", "WET", "0.0", "1", "6"), sampled(WaterRole.CHANNEL, 70));
|
||||||
|
assertEquals(List.of("RIVER", "WET", "0.0", "0", "6"), sampled(WaterRole.POND, 70));
|
||||||
|
assertEquals(List.of("RIVER_SHORE", "NONE", "1.0", "-1", "unavailable"), sampled(WaterRole.SHORE, 50));
|
||||||
|
assertEquals(List.of("LAND", "NONE", "2.0", "-1", "unavailable"), sampled(WaterRole.BANK, 50));
|
||||||
|
assertEquals(List.of("LAND", "NONE", "NaN", "-1", "unavailable"), sampled(WaterRole.NONE, 50));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -219,6 +244,33 @@ public class IrisTerrainSVCTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<String> sampled(WaterRole role, int head) throws Exception {
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
IrisComplex complex = mock(IrisComplex.class);
|
||||||
|
IrisBiome biome = mock(IrisBiome.class);
|
||||||
|
when(biome.getInferredType()).thenReturn(InferredType.LAND);
|
||||||
|
when(engine.getComplex()).thenReturn(complex);
|
||||||
|
when(engine.getDimension()).thenReturn(mock(IrisDimension.class));
|
||||||
|
when(engine.getHeight(0, 0)).thenReturn(60);
|
||||||
|
when(engine.getMinHeight()).thenReturn(-64);
|
||||||
|
when(engine.getSurfaceBiome(0, 0)).thenReturn(biome);
|
||||||
|
when(complex.getWaterRoleStream()).thenReturn(ProceduralStream.of((x, z) -> role, Interpolated.of(value -> 0D, value -> role)));
|
||||||
|
when(complex.getWaterHeadStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> (double) head));
|
||||||
|
IrisColumnQuery query = IrisColumnQuery.rect(0, 0, 0, 0, 1, EnumSet.of(IrisColumnField.SURFACE_KIND,
|
||||||
|
IrisColumnField.RIVER_STATE, IrisColumnField.RIVER_DISTANCE, IrisColumnField.RIVER_FLOW,
|
||||||
|
IrisColumnField.RIVER_WATER_SURFACE_Y));
|
||||||
|
List<IrisColumnSample> samples = new ArrayList<>();
|
||||||
|
try (MockedStatic<IrisSettings> settings = mockStatic(IrisSettings.class)) {
|
||||||
|
settings.when(IrisSettings::get).thenReturn(new IrisSettings());
|
||||||
|
assertTrue(enabledService().sampleColumns(world(engine), query, samples::add));
|
||||||
|
}
|
||||||
|
assertEquals(1, samples.size());
|
||||||
|
IrisColumnSample sample = samples.get(0);
|
||||||
|
return List.of(sample.surfaceKind().name(), sample.riverState().name(), String.valueOf(sample.riverDistance()),
|
||||||
|
String.valueOf(sample.riverFlow()),
|
||||||
|
sample.riverWaterSurfaceY() == IrisColumnSample.UNAVAILABLE_HEIGHT ? "unavailable" : String.valueOf(sample.riverWaterSurfaceY()));
|
||||||
|
}
|
||||||
|
|
||||||
private static World world(Engine engine) {
|
private static World world(Engine engine) {
|
||||||
World world = mock(World.class);
|
World world = mock(World.class);
|
||||||
ChunkGenerator generator = mock(ChunkGenerator.class, withSettings().extraInterfaces(PlatformChunkGenerator.class));
|
ChunkGenerator generator = mock(ChunkGenerator.class, withSettings().extraInterfaces(PlatformChunkGenerator.class));
|
||||||
@@ -235,41 +287,4 @@ public class IrisTerrainSVCTest {
|
|||||||
((AtomicBoolean) enabled.get(service)).set(true);
|
((AtomicBoolean) enabled.get(service)).set(true);
|
||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HydrologyColumnSample river(boolean connectedFluid) {
|
|
||||||
HydrologyFeatureRef feature = new HydrologyFeatureRef(
|
|
||||||
1L,
|
|
||||||
HydrologyFeatureType.SURFACE_POOL,
|
|
||||||
2L,
|
|
||||||
3L,
|
|
||||||
0,
|
|
||||||
63,
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
0,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
HydrologyColumnLayer layer = new HydrologyColumnLayer(
|
|
||||||
feature,
|
|
||||||
60,
|
|
||||||
63,
|
|
||||||
63,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
connectedFluid,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
connectedFluid,
|
|
||||||
false,
|
|
||||||
"water",
|
|
||||||
"river",
|
|
||||||
"mouth",
|
|
||||||
"shore",
|
|
||||||
"dry",
|
|
||||||
"cave"
|
|
||||||
);
|
|
||||||
return new HydrologyColumnSample(0, 0, 70, 63, false, "parent", List.of(layer));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+18
-93
@@ -1,15 +1,10 @@
|
|||||||
package art.arcane.iris.platform.bukkit.terrain;
|
package art.arcane.iris.platform.bukkit.terrain;
|
||||||
|
|
||||||
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
|
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureRef;
|
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
|
|
||||||
import art.arcane.iris.generation.terrain.InferredType;
|
import art.arcane.iris.generation.terrain.InferredType;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
@@ -69,98 +64,28 @@ public class IrisSurfaceClassifierTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void acceptedSurfaceRolesOverrideGenericOceanAndLandKinds() {
|
public void waterRolesOverrideGenericOceanAndLandKinds() {
|
||||||
assertEquals(IrisSurfaceKind.RIVER, IrisSurfaceClassifier.classify(
|
for (WaterRole role : WaterRole.values()) {
|
||||||
60,
|
IrisSurfaceKind expected = switch (role) {
|
||||||
63,
|
case NONE -> IrisSurfaceKind.OCEAN;
|
||||||
InferredType.SEA,
|
case SHORE, POND_SHORE -> IrisSurfaceKind.RIVER_SHORE;
|
||||||
hydrology(HydrologyFeatureType.SURFACE_POOL, true, false, true)
|
case BANK, DAM -> IrisSurfaceKind.LAND;
|
||||||
));
|
case CHANNEL, POND, MOUTH, CASCADE, WATERFALL, INLET -> IrisSurfaceKind.RIVER;
|
||||||
assertEquals(IrisSurfaceKind.RIVER, IrisSurfaceClassifier.classify(
|
};
|
||||||
60,
|
assertEquals(role.name(), expected, IrisSurfaceClassifier.classify(60, 63, InferredType.SEA, role));
|
||||||
63,
|
}
|
||||||
InferredType.SEA,
|
|
||||||
hydrology(HydrologyFeatureType.MOUTH, true, false, true)
|
|
||||||
));
|
|
||||||
assertEquals(IrisSurfaceKind.RIVER_SHORE, IrisSurfaceClassifier.classify(
|
|
||||||
64,
|
|
||||||
63,
|
|
||||||
InferredType.SHORE,
|
|
||||||
hydrology(HydrologyFeatureType.SURFACE_POOL, false, true, false)
|
|
||||||
));
|
|
||||||
assertEquals(IrisSurfaceKind.DRY_CHANNEL, IrisSurfaceClassifier.classify(
|
|
||||||
60,
|
|
||||||
60,
|
|
||||||
InferredType.LAND,
|
|
||||||
hydrology(HydrologyFeatureType.SURFACE_POOL, true, false, false)
|
|
||||||
));
|
|
||||||
assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(
|
|
||||||
60,
|
|
||||||
60,
|
|
||||||
InferredType.LAND,
|
|
||||||
hydrology(HydrologyFeatureType.SURFACE_POOL, false, false, false)
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void voidClassificationWinsOverAcceptedRiverGeometry() {
|
public void voidClassificationWinsOverWaterRoles() {
|
||||||
assertEquals(IrisSurfaceKind.VOID, IrisSurfaceClassifier.classify(
|
for (WaterRole role : WaterRole.values()) {
|
||||||
0,
|
assertEquals(role.name(), IrisSurfaceKind.VOID, IrisSurfaceClassifier.classify(0, 63, InferredType.LAND, role));
|
||||||
63,
|
}
|
||||||
InferredType.LAND,
|
|
||||||
hydrology(HydrologyFeatureType.SURFACE_POOL, true, false, true)
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void absentRejectedCandidatesDoNotCreatePublicRiverKinds() {
|
public void columnsWithoutAWaterRoleKeepTheGenericKinds() {
|
||||||
assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(
|
assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(64, 63, InferredType.LAND, WaterRole.NONE));
|
||||||
64,
|
assertEquals(IrisSurfaceKind.SHORE, IrisSurfaceClassifier.classify(64, 63, InferredType.SHORE, WaterRole.NONE));
|
||||||
63,
|
|
||||||
InferredType.LAND,
|
|
||||||
null
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static HydrologyColumnSample hydrology(
|
|
||||||
HydrologyFeatureType type,
|
|
||||||
boolean channel,
|
|
||||||
boolean shore,
|
|
||||||
boolean connectedFluid
|
|
||||||
) {
|
|
||||||
HydrologyFeatureRef feature = new HydrologyFeatureRef(
|
|
||||||
1L,
|
|
||||||
type,
|
|
||||||
2L,
|
|
||||||
3L,
|
|
||||||
0,
|
|
||||||
63,
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
0,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
HydrologyColumnLayer layer = new HydrologyColumnLayer(
|
|
||||||
feature,
|
|
||||||
60,
|
|
||||||
63,
|
|
||||||
63,
|
|
||||||
channel,
|
|
||||||
shore,
|
|
||||||
!channel && !shore,
|
|
||||||
connectedFluid,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
connectedFluid && channel,
|
|
||||||
false,
|
|
||||||
"water",
|
|
||||||
"river",
|
|
||||||
"mouth",
|
|
||||||
"shore",
|
|
||||||
"dry",
|
|
||||||
"cave"
|
|
||||||
);
|
|
||||||
return new HydrologyColumnSample(0, 0, 70, 63, false, "parent", List.of(layer));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+165
@@ -0,0 +1,165 @@
|
|||||||
|
package art.arcane.iris.studio.view;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||||
|
import art.arcane.iris.world.IrisWorld;
|
||||||
|
import art.arcane.iris.world.task.J;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.ArgumentMatchers.same;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class BukkitOpenerPositionTest {
|
||||||
|
@Test
|
||||||
|
public void playerMarkersAreCapturedOnTheirEntitySchedulers() {
|
||||||
|
try (Harness harness = new Harness()) {
|
||||||
|
harness.binding.when(() -> BukkitWorldBinding.players(harness.target)).thenReturn(List.of(harness.player));
|
||||||
|
when(harness.player.getName()).thenReturn("Explorer");
|
||||||
|
assertTrue(harness.overlay.players().isEmpty());
|
||||||
|
|
||||||
|
harness.runEntity();
|
||||||
|
|
||||||
|
List<GuiMarker> markers = harness.overlay.players();
|
||||||
|
assertEquals(1, markers.size());
|
||||||
|
assertEquals("Explorer", markers.getFirst().label());
|
||||||
|
assertEquals(18.5D, markers.getFirst().worldX(), 0D);
|
||||||
|
assertEquals(-4.25D, markers.getFirst().worldZ(), 0D);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void positionIsCapturedOnlyOnTheOwningEntityTask() {
|
||||||
|
try (Harness harness = new Harness()) {
|
||||||
|
harness.overlay.requestOpenerPosition(harness.results::add);
|
||||||
|
assertTrue(harness.results.isEmpty());
|
||||||
|
|
||||||
|
harness.runEntity();
|
||||||
|
|
||||||
|
assertEquals(List.of(OpenerPosition.inWorld(18.5D, 72D, -4.25D)), harness.results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void retiredEntityCompletesTheRequestOnce() {
|
||||||
|
try (Harness harness = new Harness()) {
|
||||||
|
harness.overlay.requestOpenerPosition(harness.results::add);
|
||||||
|
assertTrue("retirement callback must be registered", harness.retired != null);
|
||||||
|
|
||||||
|
harness.retired.run();
|
||||||
|
harness.retired.run();
|
||||||
|
|
||||||
|
assertEquals(List.of(OpenerPosition.OFFLINE), harness.results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void disconnectBeforeCaptureCompletesAsOfflineAndReconnectCanRefresh() {
|
||||||
|
try (Harness harness = new Harness()) {
|
||||||
|
harness.overlay.requestOpenerPosition(harness.results::add);
|
||||||
|
when(harness.player.isOnline()).thenReturn(false);
|
||||||
|
harness.runEntity();
|
||||||
|
assertEquals(List.of(OpenerPosition.OFFLINE), harness.results);
|
||||||
|
|
||||||
|
when(harness.player.isOnline()).thenReturn(true);
|
||||||
|
harness.overlay.requestOpenerPosition(harness.results::add);
|
||||||
|
harness.runEntity();
|
||||||
|
assertEquals(List.of(OpenerPosition.OFFLINE, OpenerPosition.inWorld(18.5D, 72D, -4.25D)), harness.results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectedEntityTaskCompletesOnceEvenIfRetirementAlreadyRan() {
|
||||||
|
try (Harness harness = new Harness()) {
|
||||||
|
harness.reject = true;
|
||||||
|
harness.overlay.requestOpenerPosition(harness.results::add);
|
||||||
|
assertEquals(List.of(OpenerPosition.OFFLINE), harness.results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void changingWorldBeforeCaptureReportsOtherWorld() {
|
||||||
|
try (Harness harness = new Harness()) {
|
||||||
|
harness.overlay.requestOpenerPosition(harness.results::add);
|
||||||
|
when(harness.player.getWorld()).thenReturn(mock(World.class));
|
||||||
|
harness.runEntity();
|
||||||
|
assertEquals(List.of(OpenerPosition.IN_OTHER_WORLD), harness.results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Harness implements AutoCloseable {
|
||||||
|
private final Player player = mock(Player.class);
|
||||||
|
private final World world = mock(World.class);
|
||||||
|
private final Engine engine = mock(Engine.class);
|
||||||
|
private final IrisWorld target = mock(IrisWorld.class);
|
||||||
|
private final UUID opener = UUID.randomUUID();
|
||||||
|
private final List<OpenerPosition> results = new ArrayList<>();
|
||||||
|
private final MockedStatic<J> scheduling = mockStatic(J.class);
|
||||||
|
private final MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
|
||||||
|
private final MockedStatic<BukkitWorldBinding> binding = mockStatic(BukkitWorldBinding.class);
|
||||||
|
private final BukkitVisionOverlay overlay = new BukkitVisionOverlay(engine, opener);
|
||||||
|
private Runnable task;
|
||||||
|
private Runnable retired;
|
||||||
|
private boolean entityOwned;
|
||||||
|
private boolean reject;
|
||||||
|
|
||||||
|
private Harness() {
|
||||||
|
when(engine.getWorld()).thenReturn(target);
|
||||||
|
when(player.isOnline()).thenReturn(true);
|
||||||
|
when(player.getWorld()).thenReturn(world);
|
||||||
|
when(player.getLocation()).thenAnswer(invocation -> {
|
||||||
|
assertTrue("location read outside owning entity task", entityOwned);
|
||||||
|
return new Location(world, 18.5D, 72D, -4.25D);
|
||||||
|
});
|
||||||
|
bukkit.when(() -> Bukkit.getPlayer(opener)).thenReturn(player);
|
||||||
|
binding.when(() -> BukkitWorldBinding.world(target)).thenReturn(world);
|
||||||
|
scheduling.when(() -> J.runGlobal(any(Runnable.class))).thenAnswer(invocation -> {
|
||||||
|
invocation.getArgument(0, Runnable.class).run();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
scheduling.when(() -> J.runEntity(same(player), any(Runnable.class))).thenAnswer(invocation -> {
|
||||||
|
task = invocation.getArgument(1);
|
||||||
|
return !reject;
|
||||||
|
});
|
||||||
|
scheduling.when(() -> J.runEntity(same(player), any(Runnable.class), eq(0), any(Runnable.class)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
task = invocation.getArgument(1);
|
||||||
|
retired = invocation.getArgument(3);
|
||||||
|
if (reject) {
|
||||||
|
retired.run();
|
||||||
|
}
|
||||||
|
return !reject;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runEntity() {
|
||||||
|
entityOwned = true;
|
||||||
|
try {
|
||||||
|
task.run();
|
||||||
|
} finally {
|
||||||
|
entityOwned = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
binding.close();
|
||||||
|
bukkit.close();
|
||||||
|
scheduling.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+55
@@ -11,12 +11,16 @@ import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
|||||||
import org.bukkit.NamespacedKey;
|
import org.bukkit.NamespacedKey;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.junit.Assert.assertThrows;
|
import static org.junit.Assert.assertThrows;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
@@ -42,4 +46,55 @@ public class PendingWorldReplacementAdmissionTest {
|
|||||||
executor.awaitTermination(5L, TimeUnit.SECONDS);
|
executor.awaitTermination(5L, TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shutdownRejectsReplacementsThatStartAfterIt() {
|
||||||
|
PendingWorldReplacementManager manager = new PendingWorldReplacementManager(mock(Iris.class));
|
||||||
|
manager.awaitStagingForShutdown();
|
||||||
|
|
||||||
|
IOException rejected = assertThrows(IOException.class, () -> manager.stageReplacement(
|
||||||
|
mock(VolmitSender.class), NamespacedKey.minecraft("overworld"), mock(IrisDimension.class), null));
|
||||||
|
|
||||||
|
assertTrue(rejected.getMessage().contains("shutting down"));
|
||||||
|
assertTrue(LifecycleOperationCoordinator.get().isIdle());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shutdownWaitsForTheReplacementBeingStaged() throws Exception {
|
||||||
|
PendingWorldReplacementManager manager = new PendingWorldReplacementManager(mock(Iris.class));
|
||||||
|
AtomicReference<Throwable> stagingFailure = new AtomicReference<>();
|
||||||
|
Thread staging = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
manager.stageReplacement(mock(VolmitSender.class), NamespacedKey.minecraft("overworld"),
|
||||||
|
mock(IrisDimension.class), null);
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
stagingFailure.set(failure);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Thread shutdown = new Thread(manager::awaitStagingForShutdown);
|
||||||
|
synchronized (manager) {
|
||||||
|
staging.start();
|
||||||
|
awaitState(staging, Thread.State.BLOCKED);
|
||||||
|
shutdown.start();
|
||||||
|
awaitState(shutdown, Thread.State.WAITING);
|
||||||
|
assertTrue(staging.isAlive());
|
||||||
|
}
|
||||||
|
shutdown.join(TimeUnit.SECONDS.toMillis(10L));
|
||||||
|
staging.join(TimeUnit.SECONDS.toMillis(10L));
|
||||||
|
|
||||||
|
assertFalse(shutdown.isAlive());
|
||||||
|
assertFalse(staging.isAlive());
|
||||||
|
assertTrue(stagingFailure.get() instanceof Exception);
|
||||||
|
assertTrue(LifecycleOperationCoordinator.get().isIdle());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void awaitState(Thread thread, Thread.State state) throws InterruptedException {
|
||||||
|
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
|
||||||
|
while (thread.getState() != state) {
|
||||||
|
if (System.nanoTime() > deadline) {
|
||||||
|
throw new AssertionError(thread.getName() + " never reached " + state + "; it is " + thread.getState());
|
||||||
|
}
|
||||||
|
Thread.sleep(5L);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -62,7 +62,7 @@ apply from: file('../../gradle/java21-runtime-exclusions.gradle')
|
|||||||
|
|
||||||
Properties rootProperties = new Properties()
|
Properties rootProperties = new Properties()
|
||||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.5'))
|
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.7'))
|
||||||
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
|
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
|
||||||
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(irisModTarget.fabricLoader.toString())
|
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(irisModTarget.fabricLoader.toString())
|
||||||
Closure<String> irisArtifactName = { String platform, String targetVersion ->
|
Closure<String> irisArtifactName = { String platform, String targetVersion ->
|
||||||
@@ -131,6 +131,8 @@ configurations.named('bundle').configure {
|
|||||||
exclude(group: 'de.crazydev22.slimjar.helper')
|
exclude(group: 'de.crazydev22.slimjar.helper')
|
||||||
exclude(group: 'de.crazydev22.slimjar')
|
exclude(group: 'de.crazydev22.slimjar')
|
||||||
exclude(group: 'io.github.slimjar')
|
exclude(group: 'io.github.slimjar')
|
||||||
|
// The game ships its own LWJGL; without the modules it leaves out, the map's 3D views draw in software.
|
||||||
|
exclude(group: 'org.lwjgl')
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations.compileClasspath.extendsFrom(configurations.devBundle)
|
configurations.compileClasspath.extendsFrom(configurations.devBundle)
|
||||||
@@ -153,7 +155,7 @@ dependencies {
|
|||||||
String modConfiguration = irisModTarget.fabricRemap ? 'modImplementation' : 'implementation'
|
String modConfiguration = irisModTarget.fabricRemap ? 'modImplementation' : 'implementation'
|
||||||
add(modConfiguration, "net.fabricmc:fabric-loader:${fabricLoaderVersion}")
|
add(modConfiguration, "net.fabricmc:fabric-loader:${fabricLoaderVersion}")
|
||||||
testImplementation('junit:junit:4.13.2')
|
testImplementation('junit:junit:4.13.2')
|
||||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
testImplementation(libs.mockito.core)
|
||||||
testImplementation(testFixtures('art.arcane:core'))
|
testImplementation(testFixtures('art.arcane:core'))
|
||||||
testImplementation(libs.spigot) {
|
testImplementation(libs.spigot) {
|
||||||
transitive = false
|
transitive = false
|
||||||
@@ -216,7 +218,9 @@ dependencies {
|
|||||||
libs.caffeine,
|
libs.caffeine,
|
||||||
'com.fulcrumgenomics:jlibdeflate:0.2.0',
|
'com.fulcrumgenomics:jlibdeflate:0.2.0',
|
||||||
libs.dom4j,
|
libs.dom4j,
|
||||||
libs.jaxen
|
libs.jaxen,
|
||||||
|
libs.flatlaf,
|
||||||
|
libs.flatlaf.extras
|
||||||
]
|
]
|
||||||
shared.each { Object notation ->
|
shared.each { Object notation ->
|
||||||
['bundle', 'devBundle'].each { String configuration ->
|
['bundle', 'devBundle'].each { String configuration ->
|
||||||
@@ -236,6 +240,8 @@ tasks.named('compileJava', JavaCompile).configure {
|
|||||||
options.release.set((int) irisModTarget.java)
|
options.release.set((int) irisModTarget.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply from: file('../../gradle/mockito-agent.gradle')
|
||||||
|
|
||||||
tasks.named('test').configure {
|
tasks.named('test').configure {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ apply from: file('../../gradle/java21-runtime-exclusions.gradle')
|
|||||||
|
|
||||||
Properties rootProperties = new Properties()
|
Properties rootProperties = new Properties()
|
||||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.5'))
|
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.7'))
|
||||||
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
|
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
|
||||||
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(irisModTarget.forge.toString())
|
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(irisModTarget.forge.toString())
|
||||||
String buildLane = gradle.extensions.extraProperties.has('irisBuildLane')
|
String buildLane = gradle.extensions.extraProperties.has('irisBuildLane')
|
||||||
@@ -152,12 +152,16 @@ configurations.named('bundle').configure {
|
|||||||
exclude(group: 'de.crazydev22.slimjar.helper')
|
exclude(group: 'de.crazydev22.slimjar.helper')
|
||||||
exclude(group: 'de.crazydev22.slimjar')
|
exclude(group: 'de.crazydev22.slimjar')
|
||||||
exclude(group: 'io.github.slimjar')
|
exclude(group: 'io.github.slimjar')
|
||||||
|
// The game ships its own LWJGL; without the modules it leaves out, the map's 3D views draw in software.
|
||||||
|
exclude(group: 'org.lwjgl')
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations.named('runBundle').configure {
|
configurations.named('runBundle').configure {
|
||||||
exclude(group: 'de.crazydev22.slimjar.helper')
|
exclude(group: 'de.crazydev22.slimjar.helper')
|
||||||
exclude(group: 'de.crazydev22.slimjar')
|
exclude(group: 'de.crazydev22.slimjar')
|
||||||
exclude(group: 'io.github.slimjar')
|
exclude(group: 'io.github.slimjar')
|
||||||
|
// The game ships its own LWJGL; without the modules it leaves out, the map's 3D views draw in software.
|
||||||
|
exclude(group: 'org.lwjgl')
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations.compileClasspath.extendsFrom(configurations.devBundle)
|
configurations.compileClasspath.extendsFrom(configurations.devBundle)
|
||||||
@@ -178,7 +182,7 @@ dependencies {
|
|||||||
? minecraft.dependency("net.minecraftforge:forge:${forgeVersion}")
|
? minecraft.dependency("net.minecraftforge:forge:${forgeVersion}")
|
||||||
: minecraft.dependency("iris-${buildLane}", "net.minecraftforge:forge:${forgeVersion}"))
|
: minecraft.dependency("iris-${buildLane}", "net.minecraftforge:forge:${forgeVersion}"))
|
||||||
testImplementation('junit:junit:4.13.2')
|
testImplementation('junit:junit:4.13.2')
|
||||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
testImplementation(libs.mockito.core)
|
||||||
testImplementation(testFixtures('art.arcane:core'))
|
testImplementation(testFixtures('art.arcane:core'))
|
||||||
testImplementation(libs.spigot) {
|
testImplementation(libs.spigot) {
|
||||||
transitive = false
|
transitive = false
|
||||||
@@ -215,7 +219,9 @@ dependencies {
|
|||||||
libs.caffeine,
|
libs.caffeine,
|
||||||
'com.fulcrumgenomics:jlibdeflate:0.2.0',
|
'com.fulcrumgenomics:jlibdeflate:0.2.0',
|
||||||
libs.dom4j,
|
libs.dom4j,
|
||||||
libs.jaxen
|
libs.jaxen,
|
||||||
|
libs.flatlaf,
|
||||||
|
libs.flatlaf.extras
|
||||||
]
|
]
|
||||||
shared.each { Object notation ->
|
shared.each { Object notation ->
|
||||||
['bundle', 'devBundle', 'runBundle'].each { String configuration ->
|
['bundle', 'devBundle', 'runBundle'].each { String configuration ->
|
||||||
@@ -272,6 +278,8 @@ tasks.named('compileJava', JavaCompile).configure {
|
|||||||
options.release.set((int) irisModTarget.java)
|
options.release.set((int) irisModTarget.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply from: file('../../gradle/mockito-agent.gradle')
|
||||||
|
|
||||||
tasks.named('test').configure {
|
tasks.named('test').configure {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-1
@@ -105,6 +105,7 @@ import java.util.Objects;
|
|||||||
import java.util.OptionalLong;
|
import java.util.OptionalLong;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.function.IntConsumer;
|
import java.util.function.IntConsumer;
|
||||||
@@ -946,6 +947,14 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
|
|||||||
+ " requires an attached generation-history runtime router."));
|
+ " requires an attached generation-history runtime router."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String structureStateKey() {
|
||||||
|
Engine current = boundEngineOrNull();
|
||||||
|
if (current == null || allowsGenerationHistoryBypass(current)) {
|
||||||
|
return "transient:" + UUID.randomUUID();
|
||||||
|
}
|
||||||
|
return "epoch:" + requireHistoryRouter(current, "structure state key").history().activeEpoch().epochId();
|
||||||
|
}
|
||||||
|
|
||||||
boolean allowsGenerationHistoryBypass(Engine current) {
|
boolean allowsGenerationHistoryBypass(Engine current) {
|
||||||
if (generationMode != ModdedGenerationMode.TRANSIENT_STUDIO) {
|
if (generationMode != ModdedGenerationMode.TRANSIENT_STUDIO) {
|
||||||
return false;
|
return false;
|
||||||
@@ -985,7 +994,7 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
|
|||||||
ModdedBiomePolicy<H, S> policy = new ModdedBiomePolicy<>(source);
|
ModdedBiomePolicy<H, S> policy = new ModdedBiomePolicy<>(source);
|
||||||
policy.bind(new ModdedBiomePolicy.RuntimeCallbacks(this::boundEngineOrNull, this::awaitStructureEngine,
|
policy.bind(new ModdedBiomePolicy.RuntimeCallbacks(this::boundEngineOrNull, this::awaitStructureEngine,
|
||||||
this::allowsGenerationHistoryBypass, this::configuredStructureBiomeKeys,
|
this::allowsGenerationHistoryBypass, this::configuredStructureBiomeKeys,
|
||||||
IrisModdedChunkGenerator::retainedBiomeKeys, this::visibleBiomeSeed));
|
IrisModdedChunkGenerator::retainedBiomeKeys, this::visibleBiomeSeed, this::structureStateKey));
|
||||||
return policy;
|
return policy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1395,6 +1395,11 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
|
|||||||
return requireConfiguredStructureBiomeKeys(exactStructureBiomeKeys());
|
return requireConfiguredStructureBiomeKeys(exactStructureBiomeKeys());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String structureStateKey() {
|
||||||
|
return Objects.requireNonNull(runtime, "runtime").structureStateKey().get();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int horizontalSearchStep(int blockY, int searchRadius) {
|
public int horizontalSearchStep(int blockY, int searchRadius) {
|
||||||
return horizontalBiomeSearchQuartStep(blockY, searchRadius);
|
return horizontalBiomeSearchQuartStep(blockY, searchRadius);
|
||||||
@@ -1402,6 +1407,7 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
|
|||||||
|
|
||||||
record RuntimeCallbacks(Supplier<Engine> currentEngine, Supplier<Engine> awaitEngine,
|
record RuntimeCallbacks(Supplier<Engine> currentEngine, Supplier<Engine> awaitEngine,
|
||||||
Predicate<Engine> historyBypass, Supplier<Set<String>> configuredStructureKeys,
|
Predicate<Engine> historyBypass, Supplier<Set<String>> configuredStructureKeys,
|
||||||
Function<Engine, Set<String>> retainedBiomeKeys, LongSupplier visibleBiomeSeed) {
|
Function<Engine, Set<String>> retainedBiomeKeys, LongSupplier visibleBiomeSeed,
|
||||||
|
Supplier<String> structureStateKey) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -227,6 +227,7 @@ public final class ModdedEngineBootstrap {
|
|||||||
failure = runStopStage(failure, "wand service", ModdedWandService::clearAll);
|
failure = runStopStage(failure, "wand service", ModdedWandService::clearAll);
|
||||||
failure = runStopStage(failure, "block break handler", ModdedBlockBreakHandler::clear);
|
failure = runStopStage(failure, "block break handler", ModdedBlockBreakHandler::clear);
|
||||||
failure = runStopStage(failure, "studio commands", ModdedStudioCommands::clear);
|
failure = runStopStage(failure, "studio commands", ModdedStudioCommands::clear);
|
||||||
|
failure = runStopStage(failure, "gui windows", GuiHost::shutdown);
|
||||||
failure = runStopStage(failure, "gui host", ModdedGuiHost::clear);
|
failure = runStopStage(failure, "gui host", ModdedGuiHost::clear);
|
||||||
failure = runStopStage(failure, "services", () -> services().disableAll());
|
failure = runStopStage(failure, "services", () -> services().disableAll());
|
||||||
reportStopFailure("stopping", failure);
|
reportStopFailure("stopping", failure);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeEntitySpawns;
|
|||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedLoader;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedLoader;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBiomeRegistry;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBiomeRegistry;
|
||||||
import art.arcane.iris.BuildConstants;
|
import art.arcane.iris.BuildConstants;
|
||||||
|
import art.arcane.iris.platform.ClasspathVanillaDataSource;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandExecutor;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandExecutor;
|
||||||
import art.arcane.iris.spi.PlatformGenerationRegistry;
|
import art.arcane.iris.spi.PlatformGenerationRegistry;
|
||||||
@@ -35,6 +36,7 @@ import art.arcane.volmlib.nativelib.entity.NativeEntityType;
|
|||||||
import art.arcane.iris.spi.PlatformRegistries;
|
import art.arcane.iris.spi.PlatformRegistries;
|
||||||
import art.arcane.iris.spi.PlatformScheduler;
|
import art.arcane.iris.spi.PlatformScheduler;
|
||||||
import art.arcane.iris.spi.PlatformStructureHooks;
|
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||||
|
import art.arcane.iris.spi.VanillaDataSource;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -56,6 +58,7 @@ public final class ModdedPlatform implements IrisPlatform {
|
|||||||
private final ModdedScheduler scheduler;
|
private final ModdedScheduler scheduler;
|
||||||
private final ModdedStructureHooks structureHooks;
|
private final ModdedStructureHooks structureHooks;
|
||||||
private final ModdedBiomeWriter biomeWriter;
|
private final ModdedBiomeWriter biomeWriter;
|
||||||
|
private final ClasspathVanillaDataSource vanillaData;
|
||||||
|
|
||||||
public ModdedPlatform(NativeModdedLoader loader) {
|
public ModdedPlatform(NativeModdedLoader loader) {
|
||||||
this.loader = loader;
|
this.loader = loader;
|
||||||
@@ -65,6 +68,7 @@ public final class ModdedPlatform implements IrisPlatform {
|
|||||||
this.scheduler = new ModdedScheduler();
|
this.scheduler = new ModdedScheduler();
|
||||||
this.structureHooks = new ModdedStructureHooks(ModdedEngineBootstrap::currentServer);
|
this.structureHooks = new ModdedStructureHooks(ModdedEngineBootstrap::currentServer);
|
||||||
this.biomeWriter = new ModdedBiomeWriter(new NativeBiomeRegistry(ModdedEngineBootstrap::currentServer, "minecraft:plains"));
|
this.biomeWriter = new ModdedBiomeWriter(new NativeBiomeRegistry(ModdedEngineBootstrap::currentServer, "minecraft:plains"));
|
||||||
|
this.vanillaData = new ClasspathVanillaDataSource(() -> overworldClass(ModdedEngineBootstrap.currentServer()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void errorSink(Consumer<Throwable> sink) {
|
public static void errorSink(Consumer<Throwable> sink) {
|
||||||
@@ -110,6 +114,11 @@ public final class ModdedPlatform implements IrisPlatform {
|
|||||||
return biomeWriter;
|
return biomeWriter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public VanillaDataSource vanillaData() {
|
||||||
|
return vanillaData;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public File dataFolder() {
|
public File dataFolder() {
|
||||||
File folder = loader.configDir().resolve("iris").toFile();
|
File folder = loader.configDir().resolve("iris").toFile();
|
||||||
@@ -365,4 +374,15 @@ public final class ModdedPlatform implements IrisPlatform {
|
|||||||
"modded-generation-registry-v1");
|
"modded-generation-registry-v1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Class<?> overworldClass(NativeModdedServer server) {
|
||||||
|
if (server == null) {
|
||||||
|
throw new IllegalStateException("Vanilla data needs a running Minecraft server.");
|
||||||
|
}
|
||||||
|
NativeWorld overworld = server.overworld();
|
||||||
|
if (overworld == null) {
|
||||||
|
throw new IllegalStateException("Vanilla data needs the Minecraft server overworld to be loaded.");
|
||||||
|
}
|
||||||
|
return overworld.nativeHandle().getClass();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+3
-1
@@ -150,6 +150,7 @@ final class ModdedCommandHelp {
|
|||||||
Entry.command("close", "", ModdedHelpMessages.COMMAND_CLOSE_CLOSE_THE_OPEN_STUDIO_DIMENSION_AND_DISCARD_ITS_WORLD, "x"),
|
Entry.command("close", "", ModdedHelpMessages.COMMAND_CLOSE_CLOSE_THE_OPEN_STUDIO_DIMENSION_AND_DISCARD_ITS_WORLD, "x"),
|
||||||
Entry.command("tpstudio", "", ModdedHelpMessages.COMMAND_TPSTUDIO_TELEPORT_INTO_THE_OPEN_STUDIO_DIMENSION, "stp"),
|
Entry.command("tpstudio", "", ModdedHelpMessages.COMMAND_TPSTUDIO_TELEPORT_INTO_THE_OPEN_STUDIO_DIMENSION, "stp"),
|
||||||
Entry.command("status", "", ModdedHelpMessages.COMMAND_STATUS_SHOW_THE_OPEN_STUDIO_DIMENSION_AND_ITS_PACK),
|
Entry.command("status", "", ModdedHelpMessages.COMMAND_STATUS_SHOW_THE_OPEN_STUDIO_DIMENSION_AND_ITS_PACK),
|
||||||
|
Entry.command("hotload", "", ModdedHelpMessages.COMMAND_HOTLOAD_REFRESH_THE_OPEN_STUDIO, "refresh"),
|
||||||
Entry.command("noise", "[generator] [seed]", ModdedHelpMessages.COMMAND_NOISE_OPEN_THE_NOISE_EXPLORER_GUI_ON_THE_SERVER_DISPLAY, "nmap"),
|
Entry.command("noise", "[generator] [seed]", ModdedHelpMessages.COMMAND_NOISE_OPEN_THE_NOISE_EXPLORER_GUI_ON_THE_SERVER_DISPLAY, "nmap"),
|
||||||
Entry.command("map", "", ModdedHelpMessages.COMMAND_MAP_OPEN_THE_VISION_MAP_GUI_ON_THE_SERVER_DISPLAY, "render"),
|
Entry.command("map", "", ModdedHelpMessages.COMMAND_MAP_OPEN_THE_VISION_MAP_GUI_ON_THE_SERVER_DISPLAY, "render"),
|
||||||
Entry.command("vscode", "[pack]", ModdedHelpMessages.COMMAND_VSCODE_REGENERATE_THE_CODE_WORKSPACE_FOR_A_PACK_AND_OPEN_IT_IN_YOUR, "vsc"),
|
Entry.command("vscode", "[pack]", ModdedHelpMessages.COMMAND_VSCODE_REGENERATE_THE_CODE_WORKSPACE_FOR_A_PACK_AND_OPEN_IT_IN_YOUR, "vsc"),
|
||||||
@@ -188,7 +189,8 @@ final class ModdedCommandHelp {
|
|||||||
Entry.command("install", "", ModdedHelpMessages.COMMAND_INSTALL_INSTALL_DIMENSION_TYPE_OVERRIDES_FOR_LOADED_IRIS_DIMENSIONS),
|
Entry.command("install", "", ModdedHelpMessages.COMMAND_INSTALL_INSTALL_DIMENSION_TYPE_OVERRIDES_FOR_LOADED_IRIS_DIMENSIONS),
|
||||||
Entry.command("list", "", ModdedHelpMessages.COMMAND_LIST_LIST_CONFIGURED_AND_INSTALLED_DATAPACKS, "ls"),
|
Entry.command("list", "", ModdedHelpMessages.COMMAND_LIST_LIST_CONFIGURED_AND_INSTALLED_DATAPACKS, "ls"),
|
||||||
Entry.command("ingest", "", ModdedHelpMessages.COMMAND_INGEST_EXPLAIN_BUKKIT_DATAPACK_INGEST_WORKFLOW, "pull"),
|
Entry.command("ingest", "", ModdedHelpMessages.COMMAND_INGEST_EXPLAIN_BUKKIT_DATAPACK_INGEST_WORKFLOW, "pull"),
|
||||||
Entry.command("remove", "<id>", ModdedHelpMessages.COMMAND_REMOVE_EXPLAIN_DATAPACK_REMOVAL_WORKFLOW, "rm")
|
Entry.command("remove", "<id>", ModdedHelpMessages.COMMAND_REMOVE_EXPLAIN_DATAPACK_REMOVAL_WORKFLOW, "rm"),
|
||||||
|
Entry.command("export", "<dimension> [version=] [mode=overworld] [namespace=iris_export] [output=] [format=zip] [objects=true] [replace=false]", ModdedHelpMessages.COMMAND_EXPORT_EXPORT_A_DIMENSION_AS_A_VANILLA_WORLDGEN_DATAPACK)
|
||||||
));
|
));
|
||||||
SECTIONS.put("datapacks", SECTIONS.get("datapack"));
|
SECTIONS.put("datapacks", SECTIONS.get("datapack"));
|
||||||
SECTIONS.put("dp", SECTIONS.get("datapack"));
|
SECTIONS.put("dp", SECTIONS.get("datapack"));
|
||||||
|
|||||||
+2
-20
@@ -25,7 +25,7 @@ import art.arcane.iris.generation.runtime.Engine;
|
|||||||
import art.arcane.iris.structure.placement.IrisStructureLocator;
|
import art.arcane.iris.structure.placement.IrisStructureLocator;
|
||||||
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
|
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
|
||||||
import art.arcane.iris.structure.placement.StructureReachability;
|
import art.arcane.iris.structure.placement.StructureReachability;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureQuery;
|
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
|
||||||
import art.arcane.iris.world.history.GenerationFindCatalog;
|
import art.arcane.iris.world.history.GenerationFindCatalog;
|
||||||
import art.arcane.iris.generation.biome.IrisBiome;
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
|
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
|
||||||
@@ -61,7 +61,7 @@ final class ModdedCommandSuggestions {
|
|||||||
static final SuggestionProvider<NativeCommandSource> OBJECT_KEYS = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestObjectKeys(context, builder);
|
static final SuggestionProvider<NativeCommandSource> OBJECT_KEYS = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestObjectKeys(context, builder);
|
||||||
static final SuggestionProvider<NativeCommandSource> STRUCTURE_KEYS = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestStructureKeys(context, builder);
|
static final SuggestionProvider<NativeCommandSource> STRUCTURE_KEYS = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestStructureKeys(context, builder);
|
||||||
static final SuggestionProvider<NativeCommandSource> POI_TYPES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> NativeCommandRegistration.suggest(List.of("buried_treasure"), builder);
|
static final SuggestionProvider<NativeCommandSource> POI_TYPES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> NativeCommandRegistration.suggest(List.of("buried_treasure"), builder);
|
||||||
static final SuggestionProvider<NativeCommandSource> HYDROLOGY_TYPES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestHydrologyTypes(context, builder);
|
static final SuggestionProvider<NativeCommandSource> HYDROLOGY_TYPES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> NativeCommandRegistration.suggest(HydrologyFeatureType.keys(), builder);
|
||||||
static final SuggestionProvider<NativeCommandSource> PACK_NAMES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestPackNames(context, builder);
|
static final SuggestionProvider<NativeCommandSource> PACK_NAMES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestPackNames(context, builder);
|
||||||
static final SuggestionProvider<NativeCommandSource> DIMENSION_NAMES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestDimensionNames(context, builder);
|
static final SuggestionProvider<NativeCommandSource> DIMENSION_NAMES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestDimensionNames(context, builder);
|
||||||
|
|
||||||
@@ -87,24 +87,6 @@ final class ModdedCommandSuggestions {
|
|||||||
return builder.buildFuture();
|
return builder.buildFuture();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CompletableFuture<Suggestions> suggestHydrologyTypes(
|
|
||||||
CommandContext<NativeCommandSource> context,
|
|
||||||
SuggestionsBuilder builder
|
|
||||||
) {
|
|
||||||
ModdedCommandFeedback.tab(context.getSource());
|
|
||||||
try {
|
|
||||||
Engine engine = IrisModdedCommands.engineFor(context.getSource().world());
|
|
||||||
if (engine != null && engine.getComplex().getHydrologyRuntime() != null) {
|
|
||||||
return NativeCommandRegistration.suggest(
|
|
||||||
engine.getComplex().getHydrologyRuntime().featureQueryKeys(), builder);
|
|
||||||
}
|
|
||||||
} catch (Throwable error) {
|
|
||||||
warnTabFailure("hydrology types", context.getSource(), error);
|
|
||||||
}
|
|
||||||
return NativeCommandRegistration.suggest(
|
|
||||||
HydrologyFeatureQuery.suggestions(List.of()), builder);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Set<String> reachableBiomeKeys(Engine engine) {
|
static Set<String> reachableBiomeKeys(Engine engine) {
|
||||||
return reachableBiomeKeys(GenerationFindCatalog.biomes(engine));
|
return reachableBiomeKeys(GenerationFindCatalog.biomes(engine));
|
||||||
}
|
}
|
||||||
|
|||||||
+122
@@ -18,7 +18,14 @@
|
|||||||
|
|
||||||
package art.arcane.iris.modded.command;
|
package art.arcane.iris.modded.command;
|
||||||
|
|
||||||
|
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
||||||
import art.arcane.iris.modded.ModdedIrisLog;
|
import art.arcane.iris.modded.ModdedIrisLog;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportProgress;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportResult;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportService;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportServices;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
|
||||||
import art.arcane.iris.pack.loading.IrisData;
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
import art.arcane.iris.pack.datapack.DataVersion;
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
import art.arcane.iris.pack.PackDirectoryResolver;
|
import art.arcane.iris.pack.PackDirectoryResolver;
|
||||||
@@ -33,6 +40,7 @@ import com.mojang.brigadier.context.CommandContext;
|
|||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandSource;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandSource;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandRegistration;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandRegistration;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandText;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldDimensions;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldDimensions;
|
||||||
|
|
||||||
@@ -40,6 +48,7 @@ import java.io.File;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
@@ -47,15 +56,20 @@ import java.util.LinkedHashSet;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import art.arcane.iris.localization.DirectorCommandMessages;
|
||||||
import art.arcane.iris.localization.IrisLanguage;
|
import art.arcane.iris.localization.IrisLanguage;
|
||||||
import art.arcane.iris.modded.localization.ModdedCommandMessages;
|
import art.arcane.iris.modded.localization.ModdedCommandMessages;
|
||||||
import art.arcane.iris.localization.RuntimeUiMessages;
|
import art.arcane.iris.localization.RuntimeUiMessages;
|
||||||
|
import art.arcane.volmlib.util.localization.LanguageAudience;
|
||||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||||
|
import art.arcane.volmlib.util.localization.TextKey;
|
||||||
public final class ModdedDatapackCommands {
|
public final class ModdedDatapackCommands {
|
||||||
private static final Predicate<NativeCommandSource> GATE = NativeCommandRegistration.GAMEMASTERS;
|
private static final Predicate<NativeCommandSource> GATE = NativeCommandRegistration.GAMEMASTERS;
|
||||||
private static final String WORLD_PACK_NAME = "iris";
|
private static final String WORLD_PACK_NAME = "iris";
|
||||||
|
private static final int RESOURCE_REPORTS = 4;
|
||||||
|
|
||||||
private ModdedDatapackCommands() {
|
private ModdedDatapackCommands() {
|
||||||
}
|
}
|
||||||
@@ -79,6 +93,11 @@ public final class ModdedDatapackCommands {
|
|||||||
root.then(message("ingest", "Managed Modrinth datapack ingest is Bukkit-only. On modded servers install the datapack folder or zip in world/datapacks, enable it, restart, then use /iris datapack list to confirm it is enabled. Registered structures generate natively in Iris dimensions."));
|
root.then(message("ingest", "Managed Modrinth datapack ingest is Bukkit-only. On modded servers install the datapack folder or zip in world/datapacks, enable it, restart, then use /iris datapack list to confirm it is enabled. Registered structures generate natively in Iris dimensions."));
|
||||||
root.then(message("pull", "Managed Modrinth datapack ingest is Bukkit-only. On modded servers install the datapack folder or zip in world/datapacks, enable it, restart, then use /iris datapack list to confirm it is enabled. Registered structures generate natively in Iris dimensions."));
|
root.then(message("pull", "Managed Modrinth datapack ingest is Bukkit-only. On modded servers install the datapack folder or zip in world/datapacks, enable it, restart, then use /iris datapack list to confirm it is enabled. Registered structures generate natively in Iris dimensions."));
|
||||||
|
|
||||||
|
root.then(NativeCommandRegistration.literal("export")
|
||||||
|
.executes(ModdedCommandTree.localized((CommandContext<NativeCommandSource> context) -> export(context.getSource(), "")))
|
||||||
|
.then(NativeCommandRegistration.argument("args", StringArgumentType.greedyString())
|
||||||
|
.executes(ModdedCommandTree.localized((CommandContext<NativeCommandSource> context) -> export(context.getSource(), StringArgumentType.getString(context, "args"))))));
|
||||||
|
|
||||||
root.then(message("remove", "Managed datapack removal is Bukkit-only. On modded servers disable the pack, delete its folder or zip from world/datapacks, and restart."));
|
root.then(message("remove", "Managed datapack removal is Bukkit-only. On modded servers disable the pack, delete its folder or zip from world/datapacks, and restart."));
|
||||||
root.then(message("rm", "Managed datapack removal is Bukkit-only. On modded servers disable the pack, delete its folder or zip from world/datapacks, and restart."));
|
root.then(message("rm", "Managed datapack removal is Bukkit-only. On modded servers disable the pack, delete its folder or zip from world/datapacks, and restart."));
|
||||||
|
|
||||||
@@ -291,4 +310,107 @@ public final class ModdedDatapackCommands {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int export(NativeCommandSource source, String raw) {
|
||||||
|
ModdedDatapackExportArguments arguments;
|
||||||
|
try {
|
||||||
|
arguments = ModdedDatapackExportArguments.parse(raw);
|
||||||
|
} catch (IllegalArgumentException invalid) {
|
||||||
|
IrisModdedCommands.fail(source, IrisLanguage.plain(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_INVALID, MessageArgument.untrusted("reason", invalid.getMessage())));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
File packsRoot = ModdedPackCommands.packsRoot();
|
||||||
|
File pack = PackDirectoryResolver.resolveExisting(packsRoot, arguments.dimension());
|
||||||
|
if (pack == null) {
|
||||||
|
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PACK_NOT_FOUND_UNDER, MessageArgument.untrusted("name", arguments.dimension()), MessageArgument.untrusted("value", packsRoot.getAbsolutePath())));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
WorldgenExportService service = WorldgenExportServices.get();
|
||||||
|
WorldgenExportRequest request = arguments.toRequest(pack.toPath(), exportRoot());
|
||||||
|
IrisModdedCommands.ok(source, IrisLanguage.plain(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_STARTED, MessageArgument.untrusted("dimension", request.dimension()), MessageArgument.untrusted("version", arguments.target().getVersion())));
|
||||||
|
ExportFeedback feedback = new ExportFeedback(source);
|
||||||
|
Thread worker = new Thread(() -> runExport(service, request, feedback), "Iris Datapack Export");
|
||||||
|
worker.setDaemon(true);
|
||||||
|
worker.start();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path exportRoot() {
|
||||||
|
return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("exports").resolve("datapacks");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void runExport(WorldgenExportService service, WorldgenExportRequest request, ExportFeedback feedback) {
|
||||||
|
WorldgenExportResult result;
|
||||||
|
try {
|
||||||
|
result = service.export(request, feedback);
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
ModdedIrisLog.error("Iris datapack export failed for " + request.dimension(), failure);
|
||||||
|
feedback.post(() -> IrisModdedCommands.fail(feedback.source, IrisLanguage.plain(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_FAILED, MessageArgument.untrusted("reason", IrisLanguage.errorDetail(failure)))));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
feedback.post(() -> reportExport(feedback.source, result));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void reportExport(NativeCommandSource source, WorldgenExportResult result) {
|
||||||
|
for (ExportDiagnostic diagnostic : result.diagnostics()) {
|
||||||
|
String line = IrisLanguage.plain(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_DIAGNOSTIC, MessageArgument.untrusted("key", diagnosticLabel(diagnostic)), MessageArgument.untrusted("message", diagnostic.message()));
|
||||||
|
ModdedCommandFeedback.send(source, NativeCommandText.literal(line).withStyle(diagnosticFormat(diagnostic.severity())));
|
||||||
|
}
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
IrisModdedCommands.ok(source, IrisLanguage.plain(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_EXPORTED, MessageArgument.untrusted("path", result.output())));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TextKey outcome = result.status() == WorldgenExportResult.Status.REJECTED
|
||||||
|
? DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_REJECTED
|
||||||
|
: DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_FAILED;
|
||||||
|
IrisModdedCommands.fail(source, IrisLanguage.plain(outcome, MessageArgument.untrusted("reason", result.message())));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NativeCommandText.Format diagnosticFormat(ExportDiagnostic.Severity severity) {
|
||||||
|
return switch (severity) {
|
||||||
|
case ERROR -> NativeCommandText.Format.RED;
|
||||||
|
case WARNING -> NativeCommandText.Format.YELLOW;
|
||||||
|
case INFO -> NativeCommandText.Format.GRAY;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String diagnosticLabel(ExportDiagnostic diagnostic) {
|
||||||
|
return diagnostic.resource().isEmpty()
|
||||||
|
? diagnostic.limit().name()
|
||||||
|
: diagnostic.limit().name() + " " + diagnostic.resource();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ExportFeedback implements WorldgenExportProgress {
|
||||||
|
private final NativeCommandSource source;
|
||||||
|
private final NativeModdedServer server;
|
||||||
|
private final UUID audience;
|
||||||
|
|
||||||
|
private ExportFeedback(NativeCommandSource source) {
|
||||||
|
this.source = source;
|
||||||
|
this.server = source.server();
|
||||||
|
this.audience = source.playerId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stage(String stage) {
|
||||||
|
post(() -> progress(IrisLanguage.plain(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_STAGE, MessageArgument.untrusted("stage", stage))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void resources(int done, int total) {
|
||||||
|
int stride = Math.max(1, total / RESOURCE_REPORTS);
|
||||||
|
if (done != total && done % stride != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
post(() -> progress(IrisLanguage.plain(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_RESOURCES, MessageArgument.untrusted("done", done), MessageArgument.untrusted("total", total))));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void post(Runnable task) {
|
||||||
|
server.execute(() -> LanguageAudience.run(audience, task));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void progress(String message) {
|
||||||
|
ModdedCommandFeedback.send(source, NativeCommandText.literal(message).withStyle(NativeCommandText.Format.GRAY));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package art.arcane.iris.modded.command;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
|
import art.arcane.iris.pack.datapack.export.DatapackExportDestinations;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportTargets;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
record ModdedDatapackExportArguments(String dimension, String version, String mode, String namespace, String output,
|
||||||
|
String format, boolean objects, boolean replace) {
|
||||||
|
private static final Pattern DIMENSION = Pattern.compile("[A-Za-z0-9_/.-]+");
|
||||||
|
private static final String VERSION_KEY = "version";
|
||||||
|
private static final String MODE_KEY = "mode";
|
||||||
|
private static final String NAMESPACE_KEY = "namespace";
|
||||||
|
private static final String OUTPUT_KEY = "output";
|
||||||
|
private static final String FORMAT_KEY = "format";
|
||||||
|
private static final String OBJECTS_KEY = "objects";
|
||||||
|
private static final String REPLACE_KEY = "replace";
|
||||||
|
private static final Set<String> OPTIONS = Set.of(VERSION_KEY, MODE_KEY, NAMESPACE_KEY, OUTPUT_KEY, FORMAT_KEY,
|
||||||
|
OBJECTS_KEY, REPLACE_KEY);
|
||||||
|
|
||||||
|
ModdedDatapackExportArguments {
|
||||||
|
Objects.requireNonNull(dimension, "dimension");
|
||||||
|
Objects.requireNonNull(version, "version");
|
||||||
|
Objects.requireNonNull(mode, "mode");
|
||||||
|
Objects.requireNonNull(namespace, "namespace");
|
||||||
|
Objects.requireNonNull(output, "output");
|
||||||
|
Objects.requireNonNull(format, "format");
|
||||||
|
}
|
||||||
|
|
||||||
|
static ModdedDatapackExportArguments parse(String raw) {
|
||||||
|
String trimmed = raw == null ? "" : raw.trim();
|
||||||
|
String[] tokens = trimmed.isEmpty() ? new String[0] : trimmed.split("\\s+");
|
||||||
|
String dimension = null;
|
||||||
|
Map<String, String> options = new HashMap<>(OPTIONS.size());
|
||||||
|
for (String token : tokens) {
|
||||||
|
int equals = token.indexOf('=');
|
||||||
|
if (equals < 0) {
|
||||||
|
if (dimension != null) {
|
||||||
|
throw new IllegalArgumentException("unexpected argument '" + token + "'");
|
||||||
|
}
|
||||||
|
dimension = token;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String key = token.substring(0, equals);
|
||||||
|
if (!OPTIONS.contains(key)) {
|
||||||
|
throw new IllegalArgumentException("unknown option '" + key + "'");
|
||||||
|
}
|
||||||
|
if (options.putIfAbsent(key, token.substring(equals + 1)) != null) {
|
||||||
|
throw new IllegalArgumentException("option '" + key + "' given twice");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dimension == null) {
|
||||||
|
throw new IllegalArgumentException("missing dimension");
|
||||||
|
}
|
||||||
|
if (!DIMENSION.matcher(dimension).matches() || dimension.contains("..")) {
|
||||||
|
throw new IllegalArgumentException("invalid dimension key '" + dimension + "'");
|
||||||
|
}
|
||||||
|
ModdedDatapackExportArguments arguments = new ModdedDatapackExportArguments(
|
||||||
|
dimension,
|
||||||
|
options.getOrDefault(VERSION_KEY, ""),
|
||||||
|
options.getOrDefault(MODE_KEY, WorldgenExportRequest.DEFAULT_MODE),
|
||||||
|
options.getOrDefault(NAMESPACE_KEY, WorldgenExportRequest.DEFAULT_NAMESPACE),
|
||||||
|
options.getOrDefault(OUTPUT_KEY, ""),
|
||||||
|
options.getOrDefault(FORMAT_KEY, WorldgenExportRequest.DEFAULT_FORMAT),
|
||||||
|
flag(OBJECTS_KEY, options.get(OBJECTS_KEY), true),
|
||||||
|
flag(REPLACE_KEY, options.get(REPLACE_KEY), false));
|
||||||
|
arguments.target();
|
||||||
|
WorldgenExportMode.parse(arguments.mode());
|
||||||
|
DatapackExportDestinations.parseFormat(arguments.format());
|
||||||
|
return arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
DataVersion target() {
|
||||||
|
return WorldgenExportTargets.resolve(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
WorldgenExportRequest toRequest(Path pack, Path exportRoot) {
|
||||||
|
return WorldgenExportRequest.builder()
|
||||||
|
.pack(pack)
|
||||||
|
.dimension(dimension)
|
||||||
|
.version(version)
|
||||||
|
.mode(mode)
|
||||||
|
.namespace(namespace)
|
||||||
|
.output(output)
|
||||||
|
.format(format)
|
||||||
|
.objects(objects)
|
||||||
|
.replace(replace)
|
||||||
|
.exportRoot(exportRoot)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean flag(String key, String raw, boolean fallback) {
|
||||||
|
if (raw == null) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return switch (raw) {
|
||||||
|
case "true" -> true;
|
||||||
|
case "false" -> false;
|
||||||
|
default -> throw new IllegalArgumentException(key + " must be true or false");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-22
@@ -39,8 +39,7 @@ import art.arcane.iris.structure.placement.StructureReachability;
|
|||||||
import art.arcane.iris.generation.runtime.WrongEngineBroException;
|
import art.arcane.iris.generation.runtime.WrongEngineBroException;
|
||||||
import art.arcane.iris.world.history.GenerationSemanticQueries;
|
import art.arcane.iris.world.history.GenerationSemanticQueries;
|
||||||
import art.arcane.iris.world.history.GenerationFindCatalog;
|
import art.arcane.iris.world.history.GenerationFindCatalog;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureQuery;
|
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
|
||||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
|
|
||||||
import art.arcane.iris.generation.biome.IrisBiome;
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
|
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
|
||||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||||
@@ -143,39 +142,31 @@ final class ModdedLocateCommands {
|
|||||||
IrisModdedCommands.fail(source, "This dimension is not generated by Iris.");
|
IrisModdedCommands.fail(source, "This dimension is not generated by Iris.");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
IrisHydrologyRuntime runtime = engine.getComplex().getHydrologyRuntime();
|
HydrologyFeatureType featureType;
|
||||||
HydrologyFeatureQuery query;
|
|
||||||
try {
|
try {
|
||||||
query = HydrologyFeatureQuery.parse(type);
|
featureType = HydrologyFeatureType.parse(type);
|
||||||
} catch (IllegalArgumentException error) {
|
} catch (IllegalArgumentException error) {
|
||||||
IrisModdedCommands.fail(source, error.getMessage());
|
IrisModdedCommands.fail(source, error.getMessage());
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
String label = featureType.key().replace('_', ' ');
|
||||||
int originX = player.blockX();
|
int originX = player.blockX();
|
||||||
int originZ = player.blockZ();
|
int originZ = player.blockZ();
|
||||||
int requestedDistance = runtime == null
|
int maximumDistance = GenerationSemanticQueries.RIVER_SEARCH_DISTANCE;
|
||||||
? 8192
|
|
||||||
: Math.min(8192, runtime.settings().routing().tileSize() * 15);
|
|
||||||
int maximumDistance = runtime == null
|
|
||||||
? requestedDistance
|
|
||||||
: runtime.maximumFeatureSearchDistance(originX, originZ, requestedDistance);
|
|
||||||
NativeModdedServer server = source.server();
|
NativeModdedServer server = source.server();
|
||||||
IrisModdedCommands.ok(source, "Searching accepted hydrology plans for " + type + "...");
|
IrisModdedCommands.ok(source, GenerationSemanticQueries.riverSearchMessage(featureType, maximumDistance));
|
||||||
MultiBurst.burst.completeValueAsync(() -> GenerationSemanticQueries.nearestRiver(
|
MultiBurst.burst.completeValueAsync(() -> GenerationSemanticQueries.nearestRiver(
|
||||||
engine, query, originX, originZ, maximumDistance,
|
engine, featureType, originX, originZ, maximumDistance).orElse(null))
|
||||||
(int visited) -> server.execute(() -> IrisModdedCommands.ok(source,
|
|
||||||
"Searched " + visited + " hydrology tiles for " + type + "..."))).orElse(null))
|
|
||||||
.whenComplete((GenerationSemanticQueries.RiverResult feature, Throwable error) -> server.execute(() -> {
|
.whenComplete((GenerationSemanticQueries.RiverResult feature, Throwable error) -> server.execute(() -> {
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
ModdedIrisLog.error("Hydrology locate failed for {}", type, error);
|
Throwable cause = error instanceof CompletionException && error.getCause() != null ? error.getCause() : error;
|
||||||
IrisModdedCommands.fail(source,
|
ModdedIrisLog.error("Hydrology locate failed for {}", featureType.key(), cause);
|
||||||
"Could not locate " + type + " hydrology: " + error.getClass().getSimpleName());
|
IrisModdedCommands.fail(source, "Could not search for " + label + ": " + cause.getClass().getSimpleName()
|
||||||
|
+ (cause.getMessage() == null ? "" : " - " + cause.getMessage()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (feature == null) {
|
if (feature == null) {
|
||||||
IrisModdedCommands.fail(source,
|
IrisModdedCommands.fail(source, GenerationSemanticQueries.riverNotFoundMessage(featureType, maximumDistance));
|
||||||
"No accepted " + type + " hydrology feature found within "
|
|
||||||
+ maximumDistance + " blocks.");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
teleportToStructure(
|
teleportToStructure(
|
||||||
@@ -185,7 +176,7 @@ final class ModdedLocateCommands {
|
|||||||
feature.x(),
|
feature.x(),
|
||||||
feature.y() + 2,
|
feature.y() + 2,
|
||||||
feature.z(),
|
feature.z(),
|
||||||
type + " hydrology feature"
|
label
|
||||||
);
|
);
|
||||||
}));
|
}));
|
||||||
return 1;
|
return 1;
|
||||||
|
|||||||
+19
@@ -23,6 +23,7 @@ import art.arcane.volmlib.nativelib.terrain.NativePregenRuntime;
|
|||||||
import art.arcane.volmlib.nativelib.terrain.NativePregenRuntime.ChunkLoad;
|
import art.arcane.volmlib.nativelib.terrain.NativePregenRuntime.ChunkLoad;
|
||||||
import art.arcane.iris.configuration.IrisSettings;
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
import art.arcane.iris.world.pregen.MantleHeapPressure;
|
import art.arcane.iris.world.pregen.MantleHeapPressure;
|
||||||
|
import art.arcane.iris.world.pregen.PregenHydrology;
|
||||||
import art.arcane.iris.world.pregen.PregenListener;
|
import art.arcane.iris.world.pregen.PregenListener;
|
||||||
import art.arcane.iris.world.pregen.PregenMantleBackpressure;
|
import art.arcane.iris.world.pregen.PregenMantleBackpressure;
|
||||||
import art.arcane.iris.world.pregen.PregeneratorMethod;
|
import art.arcane.iris.world.pregen.PregeneratorMethod;
|
||||||
@@ -30,6 +31,7 @@ import art.arcane.iris.generation.runtime.Engine;
|
|||||||
import art.arcane.iris.modded.ModdedGenPool;
|
import art.arcane.iris.modded.ModdedGenPool;
|
||||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
@@ -71,6 +73,8 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
|||||||
private final PregenMantleBackpressure backpressure;
|
private final PregenMantleBackpressure backpressure;
|
||||||
private final PauseWhenEmptyGuard pauseGuard;
|
private final PauseWhenEmptyGuard pauseGuard;
|
||||||
private final LongSupplier clock;
|
private final LongSupplier clock;
|
||||||
|
private final AtomicBoolean closing = new AtomicBoolean();
|
||||||
|
private final PregenHydrology hydrology;
|
||||||
|
|
||||||
public ModdedPregenMethod(Configuration configuration) {
|
public ModdedPregenMethod(Configuration configuration) {
|
||||||
this(configuration, System::nanoTime);
|
this(configuration, System::nanoTime);
|
||||||
@@ -80,6 +84,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
|||||||
this.clock = Objects.requireNonNull(clock);
|
this.clock = Objects.requireNonNull(clock);
|
||||||
runtime = configuration.runtime();
|
runtime = configuration.runtime();
|
||||||
engine = configuration.engine();
|
engine = configuration.engine();
|
||||||
|
hydrology = new PregenHydrology(() -> engine);
|
||||||
sync = configuration.sync();
|
sync = configuration.sync();
|
||||||
pauseGuard = new PauseWhenEmptyGuard(runtime);
|
pauseGuard = new PauseWhenEmptyGuard(runtime);
|
||||||
IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen();
|
IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen();
|
||||||
@@ -121,6 +126,8 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
|
closing.set(true);
|
||||||
|
hydrology.close();
|
||||||
try {
|
try {
|
||||||
awaitPendingChunks();
|
awaitPendingChunks();
|
||||||
ModdedIrisLog.info("Iris modded pregen closing: dim={} completed={} pending={} peakInFlight={} finalLimit={}",
|
ModdedIrisLog.info("Iris modded pregen closing: dim={} completed={} pending={} peakInFlight={} finalLimit={}",
|
||||||
@@ -341,6 +348,18 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
|
||||||
|
hydrology.bound(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRegionsAhead(List<long[]> regions) {
|
||||||
|
if (!closing.get()) {
|
||||||
|
hydrology.prepare(regions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getMethod(int x, int z) {
|
public String getMethod(int x, int z) {
|
||||||
return "Modded";
|
return "Modded";
|
||||||
|
|||||||
+92
-9
@@ -23,10 +23,9 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldView;
|
|||||||
|
|
||||||
import art.arcane.iris.modded.ModdedIrisLog;
|
import art.arcane.iris.modded.ModdedIrisLog;
|
||||||
import art.arcane.iris.configuration.IrisSettings;
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
|
import art.arcane.iris.studio.map.MapWindow;
|
||||||
|
import art.arcane.iris.studio.map.PaletteExplorerStart;
|
||||||
import art.arcane.iris.studio.view.GuiHost;
|
import art.arcane.iris.studio.view.GuiHost;
|
||||||
import art.arcane.iris.studio.view.ImageMapStudioGUI;
|
|
||||||
import art.arcane.iris.studio.view.NoiseExplorerGUI;
|
|
||||||
import art.arcane.iris.studio.view.VisionGUI;
|
|
||||||
import art.arcane.iris.pack.loading.IrisData;
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
import art.arcane.iris.pack.StructurePackageClosure;
|
import art.arcane.iris.pack.StructurePackageClosure;
|
||||||
import art.arcane.iris.pack.ImageMapPackageClosure;
|
import art.arcane.iris.pack.ImageMapPackageClosure;
|
||||||
@@ -38,6 +37,7 @@ import art.arcane.iris.generation.biome.IrisBiome;
|
|||||||
import art.arcane.iris.generation.biome.IrisBiomeGeneratorLink;
|
import art.arcane.iris.generation.biome.IrisBiomeGeneratorLink;
|
||||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
import art.arcane.iris.world.entity.IrisEntitySpawn;
|
import art.arcane.iris.world.entity.IrisEntitySpawn;
|
||||||
|
import art.arcane.iris.world.IrisWorld;
|
||||||
import art.arcane.iris.generation.noise.IrisGenerator;
|
import art.arcane.iris.generation.noise.IrisGenerator;
|
||||||
import art.arcane.iris.pack.PackExportClosure;
|
import art.arcane.iris.pack.PackExportClosure;
|
||||||
import art.arcane.iris.world.entity.IrisEntity;
|
import art.arcane.iris.world.entity.IrisEntity;
|
||||||
@@ -49,6 +49,7 @@ import art.arcane.iris.modded.ModdedDimensionManager;
|
|||||||
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
||||||
import art.arcane.iris.modded.ModdedScheduler;
|
import art.arcane.iris.modded.ModdedScheduler;
|
||||||
import art.arcane.iris.modded.ModdedWorkspaceGenerator;
|
import art.arcane.iris.modded.ModdedWorkspaceGenerator;
|
||||||
|
import art.arcane.iris.modded.service.ModdedStudioHotloadService;
|
||||||
import art.arcane.iris.generation.concurrent.BurstExecutor;
|
import art.arcane.iris.generation.concurrent.BurstExecutor;
|
||||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||||
import art.arcane.volmlib.util.collection.KMap;
|
import art.arcane.volmlib.util.collection.KMap;
|
||||||
@@ -85,6 +86,7 @@ import java.util.UUID;
|
|||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CompletionException;
|
import java.util.concurrent.CompletionException;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@@ -92,6 +94,7 @@ import java.util.stream.Stream;
|
|||||||
|
|
||||||
import art.arcane.iris.localization.IrisLanguage;
|
import art.arcane.iris.localization.IrisLanguage;
|
||||||
import art.arcane.iris.modded.localization.ModdedCommandMessages;
|
import art.arcane.iris.modded.localization.ModdedCommandMessages;
|
||||||
|
import art.arcane.volmlib.util.localization.LanguageAudience;
|
||||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||||
public final class ModdedStudioCommands {
|
public final class ModdedStudioCommands {
|
||||||
private static final Predicate<NativeCommandSource> GATE = NativeCommandRegistration.GAMEMASTERS;
|
private static final Predicate<NativeCommandSource> GATE = NativeCommandRegistration.GAMEMASTERS;
|
||||||
@@ -152,6 +155,10 @@ public final class ModdedStudioCommands {
|
|||||||
.executes(ModdedCommandTree.localized((CommandContext<NativeCommandSource> context) -> tpStudio(context.getSource()))));
|
.executes(ModdedCommandTree.localized((CommandContext<NativeCommandSource> context) -> tpStudio(context.getSource()))));
|
||||||
root.then(NativeCommandRegistration.literal("status")
|
root.then(NativeCommandRegistration.literal("status")
|
||||||
.executes(ModdedCommandTree.localized((CommandContext<NativeCommandSource> context) -> status(context.getSource()))));
|
.executes(ModdedCommandTree.localized((CommandContext<NativeCommandSource> context) -> status(context.getSource()))));
|
||||||
|
root.then(NativeCommandRegistration.literal("hotload")
|
||||||
|
.executes(ModdedCommandTree.localized((CommandContext<NativeCommandSource> context) -> hotload(context.getSource()))));
|
||||||
|
root.then(NativeCommandRegistration.literal("refresh")
|
||||||
|
.executes(ModdedCommandTree.localized((CommandContext<NativeCommandSource> context) -> hotload(context.getSource()))));
|
||||||
root.then(workspaceTree("vscode", true));
|
root.then(workspaceTree("vscode", true));
|
||||||
root.then(workspaceTree("vsc", true));
|
root.then(workspaceTree("vsc", true));
|
||||||
root.then(workspaceTree("update", false));
|
root.then(workspaceTree("update", false));
|
||||||
@@ -247,8 +254,9 @@ public final class ModdedStudioCommands {
|
|||||||
NativeProtocolPlayer player = source.player();
|
NativeProtocolPlayer player = source.player();
|
||||||
ModdedGuiHost.bindContext(worldView(source.server(), level), engine, player == null ? null : player.id());
|
ModdedGuiHost.bindContext(worldView(source.server(), level), engine, player == null ? null : player.id());
|
||||||
}
|
}
|
||||||
if (generatorKey == null || generatorKey.isBlank()) {
|
Engine mapEngine = engine != null ? engine : GuiHost.get().findActiveEngine();
|
||||||
NoiseExplorerGUI.launch();
|
if ((generatorKey == null || generatorKey.isBlank()) && mapEngine != null) {
|
||||||
|
MapWindow.openNoise(mapStart(mapEngine, source.player()), new MapWindow.NoiseSelection(null, null, seed));
|
||||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_NOISE_EXPLORER_ON_SERVER_DISPLAY));
|
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_NOISE_EXPLORER_ON_SERVER_DISPLAY));
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -262,7 +270,7 @@ public final class ModdedStudioCommands {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
String selectedGeneratorKey = generatorKey.trim();
|
String selectedGeneratorKey = generatorKey.trim();
|
||||||
NoiseExplorerGUI.launchGeneratorKey(selectedGeneratorKey, generator, seed);
|
MapWindow.openNoise(mapStart(engine, source.player()), new MapWindow.NoiseSelection(selectedGeneratorKey, generator, seed));
|
||||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_NOISE_EXPLORER_GENERATOR_SEED, MessageArgument.untrusted("value", generatorKey.trim()), MessageArgument.untrusted("seed", seed)));
|
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_NOISE_EXPLORER_GENERATOR_SEED, MessageArgument.untrusted("value", generatorKey.trim()), MessageArgument.untrusted("seed", seed)));
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -280,11 +288,20 @@ public final class ModdedStudioCommands {
|
|||||||
}
|
}
|
||||||
NativeProtocolPlayer player = source.player();
|
NativeProtocolPlayer player = source.player();
|
||||||
ModdedGuiHost.bindContext(worldView(source.server(), level), engine, player == null ? null : player.id());
|
ModdedGuiHost.bindContext(worldView(source.server(), level), engine, player == null ? null : player.id());
|
||||||
VisionGUI.launch(engine, player == null ? null : player.id());
|
MapWindow.openMap(mapStart(engine, player));
|
||||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_VISION_MAP_ON_SERVER_DISPLAY, MessageArgument.untrusted("value", level.name())));
|
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_VISION_MAP_ON_SERVER_DISPLAY, MessageArgument.untrusted("value", level.name())));
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Modded worlds have no Bukkit world id; the map finds the engine again through the bound GUI context.
|
||||||
|
private static PaletteExplorerStart mapStart(Engine engine, NativeProtocolPlayer player) {
|
||||||
|
IrisWorld world = engine.getWorld();
|
||||||
|
NativeWorld level = world == null ? null : world.platformWorld();
|
||||||
|
boolean inWorld = player != null && player.connected() && level != null && player.inWorld(level);
|
||||||
|
return new PaletteExplorerStart(engine, null, player == null ? null : player.id(), null,
|
||||||
|
inWorld ? player.x() : 0D, inWorld ? player.z() : 0D);
|
||||||
|
}
|
||||||
|
|
||||||
private static int imageMap(NativeCommandSource source) {
|
private static int imageMap(NativeCommandSource source) {
|
||||||
NativeWorld level = source.world();
|
NativeWorld level = source.world();
|
||||||
Engine engine = IrisModdedCommands.engineFor(level);
|
Engine engine = IrisModdedCommands.engineFor(level);
|
||||||
@@ -294,13 +311,13 @@ public final class ModdedStudioCommands {
|
|||||||
));
|
));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (!ImageMapStudioGUI.isAvailable()) {
|
if (!GuiHost.isAvailable() || !IrisSettings.get().getGui().isUseServerLaunchedGuis()) {
|
||||||
IrisModdedCommands.fail(source, guiUnavailableMessage());
|
IrisModdedCommands.fail(source, guiUnavailableMessage());
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
NativeProtocolPlayer player = source.player();
|
NativeProtocolPlayer player = source.player();
|
||||||
ModdedGuiHost.bindContext(worldView(source.server(), level), engine, player == null ? null : player.id());
|
ModdedGuiHost.bindContext(worldView(source.server(), level), engine, player == null ? null : player.id());
|
||||||
ImageMapStudioGUI.launch(engine);
|
MapWindow.openImageMaps(mapStart(engine, player));
|
||||||
IrisModdedCommands.ok(source, IrisLanguage.plain(
|
IrisModdedCommands.ok(source, IrisLanguage.plain(
|
||||||
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_IMAGE_MAP_STUDIO_ON_SERVER_DISPLAY,
|
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_IMAGE_MAP_STUDIO_ON_SERVER_DISPLAY,
|
||||||
MessageArgument.untrusted("value", level.name())
|
MessageArgument.untrusted("value", level.name())
|
||||||
@@ -643,6 +660,72 @@ public final class ModdedStudioCommands {
|
|||||||
return transition;
|
return transition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int hotload(NativeCommandSource source) {
|
||||||
|
NativeProtocolPlayer player = source.player();
|
||||||
|
NativeModdedServer server = source.server();
|
||||||
|
UUID owner = player == null ? CONSOLE_OWNER : player.id();
|
||||||
|
UUID audience = source.playerId();
|
||||||
|
Executor feedback = task -> server.execute(() -> LanguageAudience.run(audience, task));
|
||||||
|
CompletableFuture<Void> transition = TRANSITIONS.submit(owner, () -> hotloadTransition(source, owner, feedback));
|
||||||
|
transition.whenComplete((ignored, failure) -> {
|
||||||
|
if (failure == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Throwable cause = unwrapFailure(failure);
|
||||||
|
ModdedIrisLog.error("Iris Studio hotload command failed", cause);
|
||||||
|
feedback.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(
|
||||||
|
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_HOTLOAD_FAILED,
|
||||||
|
MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(cause)))));
|
||||||
|
});
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CompletableFuture<Void> hotloadTransition(
|
||||||
|
NativeCommandSource source,
|
||||||
|
UUID owner,
|
||||||
|
Executor feedback
|
||||||
|
) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> hotloadStudio(source, owner, feedback), feedback)
|
||||||
|
.thenCompose(request -> request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CompletableFuture<Void> hotloadStudio(
|
||||||
|
NativeCommandSource source,
|
||||||
|
UUID owner,
|
||||||
|
Executor feedback
|
||||||
|
) {
|
||||||
|
String dimensionId = STUDIOS.get(owner);
|
||||||
|
if (dimensionId == null) {
|
||||||
|
IrisModdedCommands.fail(source, IrisLanguage.plain(
|
||||||
|
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_YOU_DO_NOT_HAVE_OPEN_STUDIO_USE_IRIS_STUDIO_OPEN));
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
}
|
||||||
|
NativeWorld studio = ModdedDimensionManager.level(source.server(), dimensionId);
|
||||||
|
if (studio == null) {
|
||||||
|
STUDIOS.remove(owner, dimensionId);
|
||||||
|
IrisModdedCommands.fail(source, IrisLanguage.plain(
|
||||||
|
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_YOUR_STUDIO_DIMENSION_IS_NO_LONGER_LOADED_USE_IRIS_STUDIO));
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
}
|
||||||
|
Engine engine = IrisModdedCommands.engineFor(studio);
|
||||||
|
ModdedStudioHotloadService service = ModdedEngineBootstrap.services().service(ModdedStudioHotloadService.class);
|
||||||
|
if (engine == null || service == null) {
|
||||||
|
throw new IllegalStateException("The Studio engine or hotload service is unavailable.");
|
||||||
|
}
|
||||||
|
return service.requestHotload(dimensionId, engine)
|
||||||
|
.thenAcceptAsync(completed -> sendHotloadResult(source, completed), feedback);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void sendHotloadResult(NativeCommandSource source, boolean completed) {
|
||||||
|
if (completed) {
|
||||||
|
IrisModdedCommands.ok(source, IrisLanguage.plain(
|
||||||
|
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_HOTLOAD_COMPLETE));
|
||||||
|
} else {
|
||||||
|
IrisModdedCommands.fail(source, IrisLanguage.plain(
|
||||||
|
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_HOTLOAD_BUSY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static int status(NativeCommandSource source) {
|
private static int status(NativeCommandSource source) {
|
||||||
NativeModdedServer server = source.server();
|
NativeModdedServer server = source.server();
|
||||||
List<ModdedDimensionManager.Handle> handles = ModdedDimensionManager.handles();
|
List<ModdedDimensionManager.Handle> handles = ModdedDimensionManager.handles();
|
||||||
|
|||||||
+48
-26
@@ -18,21 +18,22 @@
|
|||||||
|
|
||||||
package art.arcane.iris.modded.command;
|
package art.arcane.iris.modded.command;
|
||||||
|
|
||||||
import art.arcane.iris.studio.view.GuiHost;
|
import art.arcane.iris.studio.view.GuiMarker;
|
||||||
import art.arcane.volmlib.nativelib.view.WorldMarker;
|
|
||||||
import art.arcane.iris.studio.view.GuiOverlay;
|
import art.arcane.iris.studio.view.GuiOverlay;
|
||||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
import art.arcane.iris.studio.view.OpenerPosition;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.iris.studio.render.RenderType;
|
|
||||||
import art.arcane.iris.modded.ModdedIrisLog;
|
import art.arcane.iris.modded.ModdedIrisLog;
|
||||||
import art.arcane.iris.modded.ModdedTeleportDeadline;
|
import art.arcane.iris.modded.ModdedTeleportDeadline;
|
||||||
|
import art.arcane.iris.world.IrisWorld;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||||
|
import art.arcane.volmlib.nativelib.view.WorldView;
|
||||||
|
|
||||||
import java.awt.Desktop;
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import art.arcane.volmlib.nativelib.view.WorldView;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
public final class ModdedVisionOverlay implements GuiOverlay {
|
public final class ModdedVisionOverlay implements GuiOverlay {
|
||||||
@@ -46,14 +47,33 @@ public final class ModdedVisionOverlay implements GuiOverlay {
|
|||||||
opener = context.opener();
|
opener = context.opener();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WorldView doesn't report which way a player faces, so their markers all face south.
|
||||||
@Override
|
@Override
|
||||||
public List<WorldMarker> players() {
|
public List<GuiMarker> players() {
|
||||||
return world.players();
|
return world.players().stream()
|
||||||
|
.map(player -> GuiMarker.player(player.label(), player.worldX(), player.worldZ(), 0F))
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void requestEntities(Consumer<List<WorldMarker>> sink) {
|
public void requestEntities(Consumer<List<GuiMarker>> sink) {
|
||||||
world.requestEntities(sink);
|
world.requestEntities(entities -> sink.accept(entities.stream()
|
||||||
|
.map(entity -> GuiMarker.entity(entity.label(), entity.worldX(), entity.worldY(), entity.worldZ(),
|
||||||
|
entity.health(), entity.maxHealth()))
|
||||||
|
.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void requestOpenerPosition(Consumer<OpenerPosition> sink) {
|
||||||
|
if (opener == null) {
|
||||||
|
sink.accept(OpenerPosition.OFFLINE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
world.execute(() -> sink.accept(openerPosition()));
|
||||||
|
} catch (RejectedExecutionException failure) {
|
||||||
|
sink.accept(OpenerPosition.OFFLINE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -80,23 +100,25 @@ public final class ModdedVisionOverlay implements GuiOverlay {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private OpenerPosition openerPosition() {
|
||||||
public String openInEditor(double worldX, double worldZ, RenderType type) {
|
IrisWorld target = engine.getWorld();
|
||||||
if (!GuiHost.isAvailable() || !Desktop.isDesktopSupported()) {
|
if (target == null || engine.isClosing() || engine.isClosed()) {
|
||||||
return null;
|
return OpenerPosition.OFFLINE;
|
||||||
}
|
}
|
||||||
IrisComplex complex = engine.getComplex();
|
NativeWorld level = target.platformWorld();
|
||||||
File file = switch (type) {
|
if (level == null) {
|
||||||
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT, RIVER ->
|
return OpenerPosition.OFFLINE;
|
||||||
complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode();
|
|
||||||
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
|
|
||||||
case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode();
|
|
||||||
case REGION -> complex.getRegionStream().get(worldX, worldZ).openInVSCode();
|
|
||||||
case CAVE_LAND -> complex.getCaveBiomeStream().get(worldX, worldZ).openInVSCode();
|
|
||||||
default -> null;
|
|
||||||
};
|
|
||||||
return file == null ? null : file.getName();
|
|
||||||
}
|
}
|
||||||
|
NativeProtocolPlayer player = NativeModdedServer.forWorld(level).player(opener);
|
||||||
|
if (player == null || !player.connected()) {
|
||||||
|
return OpenerPosition.OFFLINE;
|
||||||
|
}
|
||||||
|
if (!player.inWorld(level)) {
|
||||||
|
return OpenerPosition.IN_OTHER_WORLD;
|
||||||
|
}
|
||||||
|
return OpenerPosition.inWorld(player.x(), player.y(), player.z());
|
||||||
|
}
|
||||||
|
|
||||||
public record Context(WorldView world, Engine engine, UUID opener) {
|
public record Context(WorldView world, Engine engine, UUID opener) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
@@ -1016,6 +1016,18 @@ public final class ModdedCommandMessages {
|
|||||||
"iris.modded.moddedstudiocommands.studio_close_failed",
|
"iris.modded.moddedstudiocommands.studio_close_failed",
|
||||||
"Studio close failed: " + "{value}" + "{errorMessage}"
|
"Studio close failed: " + "{value}" + "{errorMessage}"
|
||||||
);
|
);
|
||||||
|
public static final TextKey MODDED_STUDIO_COMMANDS_STUDIO_HOTLOAD_COMPLETE = TextKey.of(
|
||||||
|
"iris.modded.moddedstudiocommands.studio_hotload_complete",
|
||||||
|
"Studio hotloaded."
|
||||||
|
);
|
||||||
|
public static final TextKey MODDED_STUDIO_COMMANDS_STUDIO_HOTLOAD_BUSY = TextKey.of(
|
||||||
|
"iris.modded.moddedstudiocommands.studio_hotload_busy",
|
||||||
|
"Studio is already checking or hotloading its pack. Try again after it finishes."
|
||||||
|
);
|
||||||
|
public static final TextKey MODDED_STUDIO_COMMANDS_STUDIO_HOTLOAD_FAILED = TextKey.of(
|
||||||
|
"iris.modded.moddedstudiocommands.studio_hotload_failed",
|
||||||
|
"Studio hotload failed{errorMessage}"
|
||||||
|
);
|
||||||
public static final TextKey MODDED_STUDIO_COMMANDS_STUDIO_CLOSED_WAS_EVACUATED_UNLOADED_ITS_REGION_DATA_DELETED = TextKey.of(
|
public static final TextKey MODDED_STUDIO_COMMANDS_STUDIO_CLOSED_WAS_EVACUATED_UNLOADED_ITS_REGION_DATA_DELETED = TextKey.of(
|
||||||
"iris.modded.moddedstudiocommands.studio_closed_was_evacuated_unloaded_its_region_data_deleted",
|
"iris.modded.moddedstudiocommands.studio_closed_was_evacuated_unloaded_its_region_data_deleted",
|
||||||
"Studio closed: " + "{dimensionId}" + " was evacuated, unloaded and its region data deleted."
|
"Studio closed: " + "{dimensionId}" + " was evacuated, unloaded and its region data deleted."
|
||||||
@@ -1530,6 +1542,9 @@ public final class ModdedCommandMessages {
|
|||||||
MODDED_STUDIO_COMMANDS_STUDIO_OPEN_NOW_RUNS_SEED_USE_IRIS_STUDIO_CLOSE_WHEN,
|
MODDED_STUDIO_COMMANDS_STUDIO_OPEN_NOW_RUNS_SEED_USE_IRIS_STUDIO_CLOSE_WHEN,
|
||||||
MODDED_STUDIO_COMMANDS_YOU_DO_NOT_HAVE_OPEN_STUDIO_USE_IRIS_STUDIO_OPEN,
|
MODDED_STUDIO_COMMANDS_YOU_DO_NOT_HAVE_OPEN_STUDIO_USE_IRIS_STUDIO_OPEN,
|
||||||
MODDED_STUDIO_COMMANDS_STUDIO_CLOSE_FAILED,
|
MODDED_STUDIO_COMMANDS_STUDIO_CLOSE_FAILED,
|
||||||
|
MODDED_STUDIO_COMMANDS_STUDIO_HOTLOAD_COMPLETE,
|
||||||
|
MODDED_STUDIO_COMMANDS_STUDIO_HOTLOAD_BUSY,
|
||||||
|
MODDED_STUDIO_COMMANDS_STUDIO_HOTLOAD_FAILED,
|
||||||
MODDED_STUDIO_COMMANDS_STUDIO_CLOSED_WAS_EVACUATED_UNLOADED_ITS_REGION_DATA_DELETED,
|
MODDED_STUDIO_COMMANDS_STUDIO_CLOSED_WAS_EVACUATED_UNLOADED_ITS_REGION_DATA_DELETED,
|
||||||
MODDED_STUDIO_COMMANDS_NO_STUDIO_DIMENSIONS_ARE_CURRENTLY_OPEN,
|
MODDED_STUDIO_COMMANDS_NO_STUDIO_DIMENSIONS_ARE_CURRENTLY_OPEN,
|
||||||
MODDED_STUDIO_COMMANDS_ACTIVE_STUDIO_DIMENSION_S,
|
MODDED_STUDIO_COMMANDS_ACTIVE_STUDIO_DIMENSION_S,
|
||||||
|
|||||||
+10
@@ -242,6 +242,10 @@ public final class ModdedHelpMessages {
|
|||||||
"iris.modded.help.entry.command.status_studio",
|
"iris.modded.help.entry.command.status_studio",
|
||||||
"Show the open studio dimension and its pack"
|
"Show the open studio dimension and its pack"
|
||||||
);
|
);
|
||||||
|
public static final TextKey COMMAND_HOTLOAD_REFRESH_THE_OPEN_STUDIO = TextKey.of(
|
||||||
|
"iris.modded.help.entry.command.hotload_studio",
|
||||||
|
"Reload the open Studio pack for newly generated terrain"
|
||||||
|
);
|
||||||
public static final TextKey COMMAND_NOISE_OPEN_THE_NOISE_EXPLORER_GUI_ON_THE_SERVER_DISPLAY = TextKey.of(
|
public static final TextKey COMMAND_NOISE_OPEN_THE_NOISE_EXPLORER_GUI_ON_THE_SERVER_DISPLAY = TextKey.of(
|
||||||
"iris.modded.help.entry.command.noise",
|
"iris.modded.help.entry.command.noise",
|
||||||
"Open the Noise Explorer GUI on the server display"
|
"Open the Noise Explorer GUI on the server display"
|
||||||
@@ -326,6 +330,10 @@ public final class ModdedHelpMessages {
|
|||||||
"iris.modded.help.entry.command.remove",
|
"iris.modded.help.entry.command.remove",
|
||||||
"Explain datapack removal workflow"
|
"Explain datapack removal workflow"
|
||||||
);
|
);
|
||||||
|
public static final TextKey COMMAND_EXPORT_EXPORT_A_DIMENSION_AS_A_VANILLA_WORLDGEN_DATAPACK = TextKey.of(
|
||||||
|
"iris.modded.help.entry.command.export_datapack",
|
||||||
|
"Export a dimension as a vanilla worldgen datapack"
|
||||||
|
);
|
||||||
public static final TextKey COMMAND_LIST_REGENERATE_STRUCTURE_INDEX_JSON = TextKey.of(
|
public static final TextKey COMMAND_LIST_REGENERATE_STRUCTURE_INDEX_JSON = TextKey.of(
|
||||||
"iris.modded.help.entry.command.list_structures",
|
"iris.modded.help.entry.command.list_structures",
|
||||||
"Regenerate structure-index.json"
|
"Regenerate structure-index.json"
|
||||||
@@ -479,6 +487,7 @@ public final class ModdedHelpMessages {
|
|||||||
COMMAND_CLOSE_CLOSE_THE_OPEN_STUDIO_DIMENSION_AND_DISCARD_ITS_WORLD,
|
COMMAND_CLOSE_CLOSE_THE_OPEN_STUDIO_DIMENSION_AND_DISCARD_ITS_WORLD,
|
||||||
COMMAND_TPSTUDIO_TELEPORT_INTO_THE_OPEN_STUDIO_DIMENSION,
|
COMMAND_TPSTUDIO_TELEPORT_INTO_THE_OPEN_STUDIO_DIMENSION,
|
||||||
COMMAND_STATUS_SHOW_THE_OPEN_STUDIO_DIMENSION_AND_ITS_PACK,
|
COMMAND_STATUS_SHOW_THE_OPEN_STUDIO_DIMENSION_AND_ITS_PACK,
|
||||||
|
COMMAND_HOTLOAD_REFRESH_THE_OPEN_STUDIO,
|
||||||
COMMAND_NOISE_OPEN_THE_NOISE_EXPLORER_GUI_ON_THE_SERVER_DISPLAY,
|
COMMAND_NOISE_OPEN_THE_NOISE_EXPLORER_GUI_ON_THE_SERVER_DISPLAY,
|
||||||
COMMAND_MAP_OPEN_THE_VISION_MAP_GUI_ON_THE_SERVER_DISPLAY,
|
COMMAND_MAP_OPEN_THE_VISION_MAP_GUI_ON_THE_SERVER_DISPLAY,
|
||||||
COMMAND_VSCODE_REGENERATE_THE_CODE_WORKSPACE_FOR_A_PACK_AND_OPEN_IT_IN_YOUR,
|
COMMAND_VSCODE_REGENERATE_THE_CODE_WORKSPACE_FOR_A_PACK_AND_OPEN_IT_IN_YOUR,
|
||||||
@@ -500,6 +509,7 @@ public final class ModdedHelpMessages {
|
|||||||
COMMAND_LIST_LIST_CONFIGURED_AND_INSTALLED_DATAPACKS,
|
COMMAND_LIST_LIST_CONFIGURED_AND_INSTALLED_DATAPACKS,
|
||||||
COMMAND_INGEST_EXPLAIN_BUKKIT_DATAPACK_INGEST_WORKFLOW,
|
COMMAND_INGEST_EXPLAIN_BUKKIT_DATAPACK_INGEST_WORKFLOW,
|
||||||
COMMAND_REMOVE_EXPLAIN_DATAPACK_REMOVAL_WORKFLOW,
|
COMMAND_REMOVE_EXPLAIN_DATAPACK_REMOVAL_WORKFLOW,
|
||||||
|
COMMAND_EXPORT_EXPORT_A_DIMENSION_AS_A_VANILLA_WORLDGEN_DATAPACK,
|
||||||
COMMAND_LIST_REGENERATE_STRUCTURE_INDEX_JSON,
|
COMMAND_LIST_REGENERATE_STRUCTURE_INDEX_JSON,
|
||||||
COMMAND_INFO_RESOLVE_AN_IRIS_STRUCTURE_GRAPH_AND_REPORT_BOUNDS,
|
COMMAND_INFO_RESOLVE_AN_IRIS_STRUCTURE_GRAPH_AND_REPORT_BOUNDS,
|
||||||
COMMAND_PLACE_ASSEMBLE_AND_PLACE_AN_IRIS_STRUCTURE_AT_YOUR_LOCATION,
|
COMMAND_PLACE_ASSEMBLE_AND_PLACE_AN_IRIS_STRUCTURE_AT_YOUR_LOCATION,
|
||||||
|
|||||||
+34
-7
@@ -123,6 +123,33 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
|||||||
watches.clear();
|
watches.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<Boolean> requestHotload(String dimensionId, Engine engine) {
|
||||||
|
if (!dimensionId.startsWith(STUDIO_DIMENSION_PREFIX) || !engine.isStudio()) {
|
||||||
|
return CompletableFuture.failedFuture(new IllegalArgumentException("Only Studio worlds can be hotloaded."));
|
||||||
|
}
|
||||||
|
ExecutorService active = executor;
|
||||||
|
if (active == null || active.isShutdown()) {
|
||||||
|
return CompletableFuture.failedFuture(new IllegalStateException("Iris Studio hotload is unavailable."));
|
||||||
|
}
|
||||||
|
Watch watch = watches.computeIfAbsent(dimensionId, (String key) -> new Watch());
|
||||||
|
if (!watch.busy.compareAndSet(false, true)) {
|
||||||
|
return CompletableFuture.completedFuture(false);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
try {
|
||||||
|
hotload(dimensionId, engine);
|
||||||
|
return true;
|
||||||
|
} finally {
|
||||||
|
watch.busy.set(false);
|
||||||
|
}
|
||||||
|
}, active);
|
||||||
|
} catch (RejectedExecutionException rejected) {
|
||||||
|
watch.busy.set(false);
|
||||||
|
return CompletableFuture.failedFuture(rejected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void refreshWorkspace(Engine engine) {
|
public void refreshWorkspace(Engine engine) {
|
||||||
writeWorkspace(engine, "workspace refresh");
|
writeWorkspace(engine, "workspace refresh");
|
||||||
@@ -254,7 +281,7 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
|||||||
try {
|
try {
|
||||||
active.execute(() -> {
|
active.execute(() -> {
|
||||||
try {
|
try {
|
||||||
poll(dimensionId, watch, generator, engine);
|
poll(dimensionId, watch, engine);
|
||||||
} finally {
|
} finally {
|
||||||
watch.busy.set(false);
|
watch.busy.set(false);
|
||||||
}
|
}
|
||||||
@@ -332,14 +359,14 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
|||||||
return isPregeneratorActive(engine);
|
return isPregeneratorActive(engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void poll(String dimensionId, Watch watch, IrisModdedChunkGenerator generator, Engine engine) {
|
private void poll(String dimensionId, Watch watch, Engine engine) {
|
||||||
try {
|
try {
|
||||||
if (watch.engine != engine) {
|
if (watch.engine != engine) {
|
||||||
watch.close();
|
watch.close();
|
||||||
watch.engine = engine;
|
watch.engine = engine;
|
||||||
watch.folder = new ReactiveFolder(
|
watch.folder = new ReactiveFolder(
|
||||||
engine.getData().getDataFolder(),
|
engine.getData().getDataFolder(),
|
||||||
(KList<File> created, KList<File> changed, KList<File> deleted) -> hotload(dimensionId, generator, engine),
|
(KList<File> created, KList<File> changed, KList<File> deleted) -> hotload(dimensionId, engine),
|
||||||
new KList<>(".iob", ".json"),
|
new KList<>(".iob", ".json"),
|
||||||
new KList<>(".iris"),
|
new KList<>(".iris"),
|
||||||
new KList<>());
|
new KList<>());
|
||||||
@@ -354,12 +381,12 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void hotload(String dimensionId, IrisModdedChunkGenerator generator, Engine engine) {
|
private void hotload(String dimensionId, Engine engine) {
|
||||||
if (engine.isClosed()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
try {
|
try {
|
||||||
|
if (engine.isClosed() || engine.isClosing()) {
|
||||||
|
throw new IllegalStateException("The Studio world is closing or closed.");
|
||||||
|
}
|
||||||
engine.hotloadSilently();
|
engine.hotloadSilently();
|
||||||
ModdedIrisLog.info("Iris studio hotload {} pack={} {}ms", dimensionId, engine.getDimension().getLoadKey(), System.currentTimeMillis() - start);
|
ModdedIrisLog.info("Iris studio hotload {} pack={} {}ms", dimensionId, engine.getDimension().getLoadKey(), System.currentTimeMillis() - start);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
|
|||||||
+13
-4
@@ -102,6 +102,15 @@ public class ModdedBiomePolicyTest {
|
|||||||
assertSame(desert, policy.orderedPossibleBiomes().getFirst());
|
assertSame(desert, policy.orderedPossibleBiomes().getFirst());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void structureStateKeyComesFromTheBoundRuntime() {
|
||||||
|
ModdedBiomePolicy<BiomeToken, Object> policy = new ModdedBiomePolicy<>(new Access(new ArrayList<>()));
|
||||||
|
policy.bind(new ModdedBiomePolicy.RuntimeCallbacks(() -> null, () -> null, engine -> false,
|
||||||
|
Set::of, engine -> Set.of(), () -> 0L, () -> "epoch:example"));
|
||||||
|
|
||||||
|
assertEquals("epoch:example", policy.structureStateKey());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void missingRequiredStructureBiomesFailBeforePublishingAPartialSet() {
|
public void missingRequiredStructureBiomesFailBeforePublishingAPartialSet() {
|
||||||
AtomicReference<Set<String>> configured = new AtomicReference<>(Set.of("example:missing"));
|
AtomicReference<Set<String>> configured = new AtomicReference<>(Set.of("example:missing"));
|
||||||
@@ -130,7 +139,7 @@ public class ModdedBiomePolicyTest {
|
|||||||
ModdedBiomePolicy<BiomeToken, Object> policy = new ModdedBiomePolicy<>(
|
ModdedBiomePolicy<BiomeToken, Object> policy = new ModdedBiomePolicy<>(
|
||||||
new Access(new ArrayList<>(List.of(forest))));
|
new Access(new ArrayList<>(List.of(forest))));
|
||||||
policy.bind(new ModdedBiomePolicy.RuntimeCallbacks(() -> engine, () -> engine, bound -> true,
|
policy.bind(new ModdedBiomePolicy.RuntimeCallbacks(() -> engine, () -> engine, bound -> true,
|
||||||
() -> Set.of(forest.key()), bound -> Set.of(), () -> 0L));
|
() -> Set.of(forest.key()), bound -> Set.of(), () -> 0L, () -> "transient:test"));
|
||||||
|
|
||||||
assertSame(forest, policy.requiredStructureBiome(6000, 0, -6000, null));
|
assertSame(forest, policy.requiredStructureBiome(6000, 0, -6000, null));
|
||||||
assertSame(forest, policy.requiredStructureBiome(6000, -30, -6000, null));
|
assertSame(forest, policy.requiredStructureBiome(6000, -30, -6000, null));
|
||||||
@@ -212,7 +221,7 @@ public class ModdedBiomePolicyTest {
|
|||||||
ModdedBiomePolicy<BiomeToken, Object> policy = new ModdedBiomePolicy<>(
|
ModdedBiomePolicy<BiomeToken, Object> policy = new ModdedBiomePolicy<>(
|
||||||
new Access(new ArrayList<>(List.of(plains, mirage))));
|
new Access(new ArrayList<>(List.of(plains, mirage))));
|
||||||
policy.bind(new ModdedBiomePolicy.RuntimeCallbacks(() -> engine, () -> engine, bound -> false,
|
policy.bind(new ModdedBiomePolicy.RuntimeCallbacks(() -> engine, () -> engine, bound -> false,
|
||||||
() -> Set.of(plains.key(), mirage.key()), bound -> Set.of(), () -> 0L));
|
() -> Set.of(plains.key(), mirage.key()), bound -> Set.of(), () -> 0L, () -> "transient:test"));
|
||||||
int[] columns = {0, 1024, 2048, 3072, 4096, 5120};
|
int[] columns = {0, 1024, 2048, 3072, 4096, 5120};
|
||||||
|
|
||||||
NativeBiomeSourcePolicy.BiomeLocation<BiomeToken> found = policy.findClosestBiome3d(
|
NativeBiomeSourcePolicy.BiomeLocation<BiomeToken> found = policy.findClosestBiome3d(
|
||||||
@@ -241,7 +250,7 @@ public class ModdedBiomePolicyTest {
|
|||||||
private static ModdedBiomePolicy<BiomeToken, Object> boundTo(Engine engine) {
|
private static ModdedBiomePolicy<BiomeToken, Object> boundTo(Engine engine) {
|
||||||
ModdedBiomePolicy<BiomeToken, Object> policy = new ModdedBiomePolicy<>(new Access(new ArrayList<>()));
|
ModdedBiomePolicy<BiomeToken, Object> policy = new ModdedBiomePolicy<>(new Access(new ArrayList<>()));
|
||||||
policy.bind(new ModdedBiomePolicy.RuntimeCallbacks(() -> engine, () -> engine, bound -> true,
|
policy.bind(new ModdedBiomePolicy.RuntimeCallbacks(() -> engine, () -> engine, bound -> true,
|
||||||
Set::of, bound -> Set.of(), () -> 0L));
|
Set::of, bound -> Set.of(), () -> 0L, () -> "transient:test"));
|
||||||
return policy;
|
return policy;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,7 +258,7 @@ public class ModdedBiomePolicyTest {
|
|||||||
AtomicReference<Set<String>> configured) {
|
AtomicReference<Set<String>> configured) {
|
||||||
ModdedBiomePolicy<BiomeToken, Object> policy = new ModdedBiomePolicy<>(new Access(entries));
|
ModdedBiomePolicy<BiomeToken, Object> policy = new ModdedBiomePolicy<>(new Access(entries));
|
||||||
policy.bind(new ModdedBiomePolicy.RuntimeCallbacks(() -> null, () -> null, engine -> false,
|
policy.bind(new ModdedBiomePolicy.RuntimeCallbacks(() -> null, () -> null, engine -> false,
|
||||||
configured::get, engine -> Set.of(), () -> 0L));
|
configured::get, engine -> Set.of(), () -> 0L, () -> "transient:test"));
|
||||||
return policy;
|
return policy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
import art.arcane.iris.platform.ClasspathVanillaDataSource;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class ModdedVanillaDataTest {
|
||||||
|
private static final String PLAINS = "data/minecraft/worldgen/biome/plains.json";
|
||||||
|
private static final String BIOME_TAGS = "data/minecraft/tags/worldgen/biome";
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapRegistries() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void overworldLevelClassReadsTheGameVanillaData() {
|
||||||
|
NativeModdedServer server = mock(NativeModdedServer.class);
|
||||||
|
NativeWorld overworld = mock(NativeWorld.class);
|
||||||
|
when(overworld.nativeHandle()).thenReturn(mock(ServerLevel.class));
|
||||||
|
when(server.overworld()).thenReturn(overworld);
|
||||||
|
Class<?> anchor = ModdedPlatform.overworldClass(server);
|
||||||
|
assertSame(ServerLevel.class, anchor);
|
||||||
|
ClasspathVanillaDataSource source = new ClasspathVanillaDataSource(() -> anchor);
|
||||||
|
String plains = new String(source.read(PLAINS).orElseThrow(), StandardCharsets.UTF_8);
|
||||||
|
assertTrue(plains.contains("\"temperature\""));
|
||||||
|
assertTrue(source.list(BIOME_TAGS).contains(BIOME_TAGS + "/is_overworld.json"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void vanillaDataNeedsALoadedOverworld() {
|
||||||
|
NativeModdedServer server = mock(NativeModdedServer.class);
|
||||||
|
assertThrows(IllegalStateException.class, () -> ModdedPlatform.overworldClass(null));
|
||||||
|
assertThrows(IllegalStateException.class, () -> ModdedPlatform.overworldClass(server));
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
@@ -63,6 +63,9 @@ public class IrisModdedCommandParityTest {
|
|||||||
CommandNode<CommandSourceStack> studio = child(iris, "studio");
|
CommandNode<CommandSourceStack> studio = child(iris, "studio");
|
||||||
child(studio, "package");
|
child(studio, "package");
|
||||||
child(studio, "pkg");
|
child(studio, "pkg");
|
||||||
|
CommandNode<CommandSourceStack> datapackExport = child(child(iris, "datapack"), "export");
|
||||||
|
assertNotNull(datapackExport.getCommand());
|
||||||
|
assertNotNull(child(datapackExport, "args").getCommand());
|
||||||
|
|
||||||
CommandNode<CommandSourceStack> download = child(iris, "download");
|
CommandNode<CommandSourceStack> download = child(iris, "download");
|
||||||
CommandNode<CommandSourceStack> source = child(download, "source");
|
CommandNode<CommandSourceStack> source = child(download, "source");
|
||||||
@@ -211,6 +214,8 @@ public class IrisModdedCommandParityTest {
|
|||||||
assertTrue(root.stream().anyMatch((String line) -> line.startsWith("/iris teleport <dimension> [player] (tp) - ")));
|
assertTrue(root.stream().anyMatch((String line) -> line.startsWith("/iris teleport <dimension> [player] (tp) - ")));
|
||||||
assertTrue(ModdedCommandHelp.consoleLines("pregen").stream().anyMatch((String line) ->
|
assertTrue(ModdedCommandHelp.consoleLines("pregen").stream().anyMatch((String line) ->
|
||||||
line.startsWith("/iris pregen start <radius> [dimension] [at] [x] [z] [gui] [sync] [nocache] - ")));
|
line.startsWith("/iris pregen start <radius> [dimension] [at] [x] [z] [gui] [sync] [nocache] - ")));
|
||||||
|
assertTrue(ModdedCommandHelp.consoleLines("datapack").stream().anyMatch((String line) ->
|
||||||
|
line.startsWith("/iris datapack export <dimension> [version=] [mode=overworld] [namespace=iris_export] [output=] [format=zip] [objects=true] [replace=false] - ")));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CommandNode<CommandSourceStack> child(
|
private static CommandNode<CommandSourceStack> child(
|
||||||
|
|||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
package art.arcane.iris.modded.command;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class ModdedDatapackExportArgumentsTest {
|
||||||
|
@Test
|
||||||
|
public void unknownKeyFails() {
|
||||||
|
assertEquals("unknown option 'seed'", reason("overworld seed=1"));
|
||||||
|
assertEquals("unknown option 'Version'", reason("overworld Version=26.3"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void duplicateKeyFails() {
|
||||||
|
assertEquals("option 'replace' given twice", reason("overworld replace=true replace=false"));
|
||||||
|
assertEquals("option 'version' given twice", reason("overworld version= version=26.3"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void firstBareTokenIsDimension() {
|
||||||
|
ModdedDatapackExportArguments arguments = ModdedDatapackExportArguments.parse(
|
||||||
|
" version=26.3 overworld mode=dimension namespace=worlds output=export_one format=directory objects=false replace=true ");
|
||||||
|
|
||||||
|
assertEquals("overworld", arguments.dimension());
|
||||||
|
assertEquals("26.3", arguments.version());
|
||||||
|
assertEquals("dimension", arguments.mode());
|
||||||
|
assertEquals("worlds", arguments.namespace());
|
||||||
|
assertEquals("export_one", arguments.output());
|
||||||
|
assertEquals("directory", arguments.format());
|
||||||
|
assertFalse(arguments.objects());
|
||||||
|
assertTrue(arguments.replace());
|
||||||
|
assertSame(DataVersion.V26_3, arguments.target());
|
||||||
|
assertEquals("unexpected argument 'underworld'", reason("overworld underworld"));
|
||||||
|
assertEquals("missing dimension", reason(" "));
|
||||||
|
assertEquals("missing dimension", reason("version=26.3"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultsMatchBukkit() {
|
||||||
|
ModdedDatapackExportArguments arguments = ModdedDatapackExportArguments.parse("overworld");
|
||||||
|
|
||||||
|
assertEquals("", arguments.version());
|
||||||
|
assertEquals("overworld", arguments.mode());
|
||||||
|
assertEquals("iris_export", arguments.namespace());
|
||||||
|
assertEquals("", arguments.output());
|
||||||
|
assertEquals("zip", arguments.format());
|
||||||
|
assertTrue(arguments.objects());
|
||||||
|
assertFalse(arguments.replace());
|
||||||
|
assertSame(DataVersion.V26_3, arguments.target());
|
||||||
|
|
||||||
|
Path pack = Path.of("packs", "overworld");
|
||||||
|
Path exportRoot = Path.of("exports", "datapacks");
|
||||||
|
WorldgenExportRequest expected = WorldgenExportRequest.builder()
|
||||||
|
.pack(pack)
|
||||||
|
.dimension("overworld")
|
||||||
|
.exportRoot(exportRoot)
|
||||||
|
.build();
|
||||||
|
assertEquals(expected, arguments.toRequest(pack, exportRoot));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unsupportedValuesFailWithSharedMessages() {
|
||||||
|
assertEquals("Unsupported datapack export version '1.21.1'. Supported: 26.3",
|
||||||
|
reason("overworld version=1.21.1"));
|
||||||
|
assertEquals("Unsupported datapack export version '26.2'. Supported: 26.3",
|
||||||
|
reason("overworld version=26.2"));
|
||||||
|
assertEquals("Unknown export mode 'nether'. Expected: preset, dimension, overworld",
|
||||||
|
reason("overworld mode=nether"));
|
||||||
|
assertEquals("Export format must be directory or zip.", reason("overworld format=rar"));
|
||||||
|
assertEquals("objects must be true or false", reason("overworld objects=yes"));
|
||||||
|
assertEquals("replace must be true or false", reason("overworld replace=1"));
|
||||||
|
assertEquals("invalid dimension key 'a..b'", reason("a..b"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String reason(String raw) {
|
||||||
|
return assertThrows(IllegalArgumentException.class, () -> ModdedDatapackExportArguments.parse(raw)).getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
+149
@@ -0,0 +1,149 @@
|
|||||||
|
package art.arcane.iris.modded.command;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
|
||||||
|
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureQuery;
|
||||||
|
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureRef;
|
||||||
|
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
|
import art.arcane.iris.world.history.GenerationSemanticQueries;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandSource;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeStructureQueries;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.MockedConstruction;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockConstruction;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class ModdedLocateRiverTest {
|
||||||
|
private static final int DISTANCE = GenerationSemanticQueries.RIVER_SEARCH_DISTANCE;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unknownTypeFailsBeforeSearching() {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
try (MockedStatic<IrisModdedCommands> commands = fixture.commands()) {
|
||||||
|
assertEquals(0, ModdedLocateCommands.gotoRiver(fixture.source, "ocean_trench"));
|
||||||
|
|
||||||
|
commands.verify(() -> IrisModdedCommands.fail(eq(fixture.source), anyString()));
|
||||||
|
commands.verify(() -> IrisModdedCommands.ok(eq(fixture.source), anyString()), never());
|
||||||
|
verify(fixture.server, never()).execute(any());
|
||||||
|
verify(fixture.runtime, never()).nearestFeature(anyInt(), anyInt(), any(), anyInt(), any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void missingFeatureReportsNotFound() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
when(fixture.runtime.nearestFeature(anyInt(), anyInt(), any(), anyInt(), any())).thenReturn(Optional.empty());
|
||||||
|
try (MockedStatic<IrisModdedCommands> commands = fixture.commands()) {
|
||||||
|
assertEquals(1, ModdedLocateCommands.gotoRiver(fixture.source, "river"));
|
||||||
|
fixture.finishOnServerThread();
|
||||||
|
|
||||||
|
commands.verify(() -> IrisModdedCommands.fail(fixture.source, "No river found within " + DISTANCE + " blocks."));
|
||||||
|
verify(fixture.player, never()).teleport(any(), anyDouble(), anyDouble(), anyDouble());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void plannerFailureIsReportedAsAnErrorNotAsNotFound() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
when(fixture.runtime.nearestFeature(anyInt(), anyInt(), any(), anyInt(), any()))
|
||||||
|
.thenThrow(new IllegalStateException("window planner exploded"));
|
||||||
|
try (MockedStatic<IrisModdedCommands> commands = fixture.commands()) {
|
||||||
|
assertEquals(1, ModdedLocateCommands.gotoRiver(fixture.source, "major_river"));
|
||||||
|
fixture.finishOnServerThread();
|
||||||
|
|
||||||
|
commands.verify(() -> IrisModdedCommands.fail(fixture.source,
|
||||||
|
"Could not search for major river: IllegalStateException - window planner exploded"));
|
||||||
|
commands.verify(() -> IrisModdedCommands.fail(eq(fixture.source), eq("No major river found within " + DISTANCE + " blocks.")),
|
||||||
|
never());
|
||||||
|
verify(fixture.player, never()).teleport(any(), anyDouble(), anyDouble(), anyDouble());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void foundFeatureTeleportsAboveItFromThePlayersPosition() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
when(fixture.runtime.nearestFeature(anyInt(), anyInt(), any(), anyInt(), any()))
|
||||||
|
.thenReturn(Optional.of(new HydrologyFeatureRef(9L, HydrologyFeatureType.WATERFALL, 400, 130, -250)));
|
||||||
|
when(fixture.player.teleport(fixture.world, 400.5D, 68D, -249.5D)).thenReturn(true);
|
||||||
|
try (MockedStatic<IrisModdedCommands> commands = fixture.commands();
|
||||||
|
MockedConstruction<NativeStructureQueries> queries = mockConstruction(NativeStructureQueries.class)) {
|
||||||
|
assertEquals(1, ModdedLocateCommands.gotoRiver(fixture.source, "waterfall"));
|
||||||
|
fixture.finishOnServerThread();
|
||||||
|
|
||||||
|
ArgumentCaptor<HydrologyFeatureQuery> query = ArgumentCaptor.forClass(HydrologyFeatureQuery.class);
|
||||||
|
verify(fixture.runtime).nearestFeature(eq(120), eq(-75), query.capture(), eq(DISTANCE), any());
|
||||||
|
assertEquals(HydrologyFeatureQuery.of(HydrologyFeatureType.WATERFALL), query.getValue());
|
||||||
|
verify(queries.constructed().getFirst()).loadChunk(400 >> 4, -250 >> 4);
|
||||||
|
verify(fixture.player).teleport(fixture.world, 400.5D, 68D, -249.5D);
|
||||||
|
commands.verify(() -> IrisModdedCommands.ok(eq(fixture.source), anyString()), times(2));
|
||||||
|
commands.verify(() -> IrisModdedCommands.fail(eq(fixture.source), anyString()), never());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Fixture {
|
||||||
|
private final NativeCommandSource source = mock(NativeCommandSource.class);
|
||||||
|
private final NativeWorld world = mock(NativeWorld.class);
|
||||||
|
private final NativeProtocolPlayer player = mock(NativeProtocolPlayer.class);
|
||||||
|
private final NativeModdedServer server = mock(NativeModdedServer.class);
|
||||||
|
private final Engine engine = mock(Engine.class);
|
||||||
|
private final HydrologyRuntime runtime = mock(HydrologyRuntime.class);
|
||||||
|
private final CompletableFuture<Runnable> completion = new CompletableFuture<>();
|
||||||
|
|
||||||
|
private Fixture() {
|
||||||
|
IrisComplex complex = mock(IrisComplex.class);
|
||||||
|
IrisDimension dimension = mock(IrisDimension.class);
|
||||||
|
when(source.player()).thenReturn(player);
|
||||||
|
when(source.world()).thenReturn(world);
|
||||||
|
when(source.server()).thenReturn(server);
|
||||||
|
when(player.blockX()).thenReturn(120);
|
||||||
|
when(player.blockZ()).thenReturn(-75);
|
||||||
|
when(player.connected()).thenReturn(true);
|
||||||
|
when(player.inWorld(world)).thenReturn(true);
|
||||||
|
when(world.minHeight()).thenReturn(-64);
|
||||||
|
when(world.maxHeight()).thenReturn(320);
|
||||||
|
when(engine.getComplex()).thenReturn(complex);
|
||||||
|
when(engine.getDimension()).thenReturn(dimension);
|
||||||
|
when(dimension.getMinHeight()).thenReturn(-64);
|
||||||
|
when(complex.getHydrologyRuntime()).thenReturn(runtime);
|
||||||
|
when(runtime.enabled()).thenReturn(true);
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
completion.complete(invocation.getArgument(0));
|
||||||
|
return null;
|
||||||
|
}).when(server).execute(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
private MockedStatic<IrisModdedCommands> commands() {
|
||||||
|
MockedStatic<IrisModdedCommands> commands = mockStatic(IrisModdedCommands.class);
|
||||||
|
commands.when(() -> IrisModdedCommands.engineFor(world)).thenReturn(engine);
|
||||||
|
return commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishOnServerThread() throws Exception {
|
||||||
|
completion.get(10L, TimeUnit.SECONDS).run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
@@ -1,6 +1,7 @@
|
|||||||
package art.arcane.iris.modded.command;
|
package art.arcane.iris.modded.command;
|
||||||
|
|
||||||
import art.arcane.iris.configuration.IrisSettings;
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
|
import art.arcane.iris.generation.hydrology.engine.HydrologyPregeneration;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.iris.modded.ModdedGenPool;
|
import art.arcane.iris.modded.ModdedGenPool;
|
||||||
import art.arcane.iris.world.pregen.MantleHeapPressure;
|
import art.arcane.iris.world.pregen.MantleHeapPressure;
|
||||||
@@ -11,8 +12,10 @@ import org.junit.After;
|
|||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.InOrder;
|
||||||
import org.mockito.MockedStatic;
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
@@ -31,9 +34,11 @@ import static org.junit.Assert.assertThrows;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyList;
|
||||||
import static org.mockito.Mockito.doAnswer;
|
import static org.mockito.Mockito.doAnswer;
|
||||||
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
|
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
|
||||||
import static org.mockito.Mockito.doThrow;
|
import static org.mockito.Mockito.doThrow;
|
||||||
|
import static org.mockito.Mockito.inOrder;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.mockStatic;
|
import static org.mockito.Mockito.mockStatic;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
@@ -89,6 +94,25 @@ public class ModdedPregenMethodLifecycleTest {
|
|||||||
verify(runtime, never()).execute(any());
|
verify(runtime, never()).execute(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void closeWithdrawsQueuedHydrologyPrefetchAndRefusesLaterAnnouncements() {
|
||||||
|
HydrologyPregeneration epoch = mock(HydrologyPregeneration.class);
|
||||||
|
when(engine.getComplex().getHydrologyRuntime().openPregeneration()).thenReturn(epoch);
|
||||||
|
when(runtime.isServerThread()).thenReturn(true);
|
||||||
|
assertTrue(method.deferFinalSaveToServerThread());
|
||||||
|
List<long[]> ahead = List.of(new long[]{0L, 0L}, new long[]{1L, 0L});
|
||||||
|
|
||||||
|
method.onRegionsAhead(ahead);
|
||||||
|
method.close();
|
||||||
|
method.onRegionsAhead(ahead);
|
||||||
|
|
||||||
|
InOrder order = inOrder(epoch);
|
||||||
|
order.verify(epoch).prepare(ahead);
|
||||||
|
order.verify(epoch).close();
|
||||||
|
verify(epoch, times(1)).prepare(anyList());
|
||||||
|
method.completeDeferredFinalSave();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void asynchronousCallerCannotClaimTheShutdownSave() {
|
public void asynchronousCallerCannotClaimTheShutdownSave() {
|
||||||
assertFalse(method.deferFinalSaveToServerThread());
|
assertFalse(method.deferFinalSaveToServerThread());
|
||||||
|
|||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
package art.arcane.iris.modded.command;
|
||||||
|
|
||||||
|
import art.arcane.iris.localization.IrisLanguage;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandSource;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandText;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||||
|
import art.arcane.volmlib.util.localization.LanguageAudience;
|
||||||
|
import com.mojang.brigadier.CommandDispatcher;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class ModdedStudioHotloadLanguageTest {
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
private final UUID playerId = UUID.randomUUID();
|
||||||
|
private File dataFolder;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrap() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() throws Exception {
|
||||||
|
dataFolder = temporaryFolder.newFolder();
|
||||||
|
assertTrue(IrisLanguage.reload(dataFolder, "en_US"));
|
||||||
|
Files.writeString(dataFolder.toPath().resolve("languages/de_DE.toml"),
|
||||||
|
"\"iris.modded.moddedstudiocommands.you_do_not_have_open_studio_use_iris_studio_open\" = \"Kein Studio geöffnet.\"\n");
|
||||||
|
IrisLanguage.start();
|
||||||
|
IrisLanguage.selections().selectPlayer(playerId, "de_DE").get(5L, TimeUnit.SECONDS);
|
||||||
|
ModdedStudioCommands.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void tearDown() {
|
||||||
|
ModdedStudioCommands.clear();
|
||||||
|
IrisLanguage.shutdown();
|
||||||
|
assertTrue(IrisLanguage.reload(dataFolder, "en_US"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void delayedHotloadFeedbackKeepsThePlayersLanguage() throws Exception {
|
||||||
|
assertEquals(List.of("Kein Studio geöffnet."), runCommand("studio hotload", true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void delayedConsoleRefreshUsesTheServerLanguage() throws Exception {
|
||||||
|
assertEquals(List.of("You do not have an open studio. Use /iris studio open <pack> first."),
|
||||||
|
runCommand("studio refresh", false));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> runCommand(String command, boolean playerCommand) throws Exception {
|
||||||
|
NativeModdedServer server = mock(NativeModdedServer.class);
|
||||||
|
List<Runnable> tasks = new ArrayList<>();
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
tasks.add(invocation.getArgument(0));
|
||||||
|
return null;
|
||||||
|
}).when(server).execute(any(Runnable.class));
|
||||||
|
NativeCommandSource source = mock(NativeCommandSource.class);
|
||||||
|
when(source.server()).thenReturn(server);
|
||||||
|
when(source.isGameMaster()).thenReturn(true);
|
||||||
|
if (playerCommand) {
|
||||||
|
NativeProtocolPlayer player = mock(NativeProtocolPlayer.class);
|
||||||
|
when(player.id()).thenReturn(playerId);
|
||||||
|
when(source.player()).thenReturn(player);
|
||||||
|
when(source.playerId()).thenReturn(playerId);
|
||||||
|
}
|
||||||
|
List<String> messages = new ArrayList<>();
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
NativeCommandText text = invocation.getArgument(0);
|
||||||
|
messages.add(text.getString());
|
||||||
|
return null;
|
||||||
|
}).when(source).sendFailure(any());
|
||||||
|
CommandDispatcher<NativeCommandSource> dispatcher = new CommandDispatcher<>();
|
||||||
|
dispatcher.register(ModdedStudioCommands.tree("studio"));
|
||||||
|
|
||||||
|
assertEquals(1, dispatcher.execute(command, source));
|
||||||
|
assertTrue(messages.isEmpty());
|
||||||
|
assertNull(LanguageAudience.current());
|
||||||
|
UUID ambientAudience = playerCommand ? UUID.randomUUID() : playerId;
|
||||||
|
try (LanguageAudience.Scope audience = LanguageAudience.open(ambientAudience)) {
|
||||||
|
while (!tasks.isEmpty()) {
|
||||||
|
tasks.removeFirst().run();
|
||||||
|
assertEquals(ambientAudience, LanguageAudience.current());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
}
|
||||||
+73
@@ -1,25 +1,98 @@
|
|||||||
package art.arcane.iris.modded.command;
|
package art.arcane.iris.modded.command;
|
||||||
|
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.studio.view.OpenerPosition;
|
||||||
|
import art.arcane.iris.world.IrisWorld;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||||
import art.arcane.volmlib.nativelib.view.WorldView;
|
import art.arcane.volmlib.nativelib.view.WorldView;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
import org.mockito.invocation.InvocationOnMock;
|
import org.mockito.invocation.InvocationOnMock;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.doAnswer;
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class ModdedVisionOverlayTest {
|
public class ModdedVisionOverlayTest {
|
||||||
|
@Test
|
||||||
|
public void rejectedServerTaskCompletesThePositionRequestAsOffline() {
|
||||||
|
WorldView world = mock(WorldView.class);
|
||||||
|
doThrow(new RejectedExecutionException("Server stopped")).when(world).execute(any(Runnable.class));
|
||||||
|
ModdedVisionOverlay overlay = new ModdedVisionOverlay(new ModdedVisionOverlay.Context(world,
|
||||||
|
mock(Engine.class), UUID.randomUUID()));
|
||||||
|
AtomicReference<OpenerPosition> position = new AtomicReference<>();
|
||||||
|
|
||||||
|
overlay.requestOpenerPosition(position::set);
|
||||||
|
|
||||||
|
assertEquals(OpenerPosition.OFFLINE, position.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void openerPositionRunsOnTheServerAndTracksDisconnectAndWorldChanges() {
|
||||||
|
WorldView world = mock(WorldView.class);
|
||||||
|
AtomicReference<Runnable> scheduled = new AtomicReference<>();
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
scheduled.set(invocation.getArgument(0));
|
||||||
|
return null;
|
||||||
|
}).when(world).execute(any(Runnable.class));
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
IrisWorld irisWorld = mock(IrisWorld.class);
|
||||||
|
NativeWorld level = mock(NativeWorld.class);
|
||||||
|
NativeModdedServer server = mock(NativeModdedServer.class);
|
||||||
|
NativeProtocolPlayer player = mock(NativeProtocolPlayer.class);
|
||||||
|
UUID opener = UUID.randomUUID();
|
||||||
|
when(engine.getWorld()).thenReturn(irisWorld);
|
||||||
|
when(irisWorld.platformWorld()).thenReturn(level);
|
||||||
|
when(server.player(opener)).thenReturn(player);
|
||||||
|
when(player.connected()).thenReturn(true);
|
||||||
|
when(player.inWorld(level)).thenReturn(true);
|
||||||
|
when(player.x()).thenReturn(21.5D);
|
||||||
|
when(player.y()).thenReturn(85.25D);
|
||||||
|
when(player.z()).thenReturn(-39.5D);
|
||||||
|
ModdedVisionOverlay overlay = new ModdedVisionOverlay(new ModdedVisionOverlay.Context(world, engine, opener));
|
||||||
|
AtomicReference<OpenerPosition> position = new AtomicReference<>();
|
||||||
|
try (MockedStatic<NativeModdedServer> nativeServer = mockStatic(NativeModdedServer.class)) {
|
||||||
|
nativeServer.when(() -> NativeModdedServer.forWorld(level)).thenReturn(server);
|
||||||
|
|
||||||
|
overlay.requestOpenerPosition(position::set);
|
||||||
|
assertNull("position reads must wait for the server task", position.get());
|
||||||
|
scheduled.get().run();
|
||||||
|
assertEquals(OpenerPosition.inWorld(21.5D, 85.25D, -39.5D), position.get());
|
||||||
|
|
||||||
|
overlay.requestOpenerPosition(position::set);
|
||||||
|
when(player.connected()).thenReturn(false);
|
||||||
|
scheduled.get().run();
|
||||||
|
assertEquals(OpenerPosition.OFFLINE, position.get());
|
||||||
|
|
||||||
|
when(player.connected()).thenReturn(true);
|
||||||
|
when(player.inWorld(level)).thenReturn(false);
|
||||||
|
overlay.requestOpenerPosition(position::set);
|
||||||
|
scheduled.get().run();
|
||||||
|
assertEquals(OpenerPosition.IN_OTHER_WORLD, position.get());
|
||||||
|
|
||||||
|
when(player.inWorld(level)).thenReturn(true);
|
||||||
|
overlay.requestOpenerPosition(position::set);
|
||||||
|
scheduled.get().run();
|
||||||
|
assertEquals(OpenerPosition.inWorld(21.5D, 85.25D, -39.5D), position.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Vision click can land anywhere in the world, so the destination chunk is usually cold and gets the same
|
* A Vision click can land anywhere in the world, so the destination chunk is usually cold and gets the same
|
||||||
* bounded deadline as /iris tp.
|
* bounded deadline as /iris tp.
|
||||||
|
|||||||
+119
@@ -1,5 +1,6 @@
|
|||||||
package art.arcane.iris.modded.service;
|
package art.arcane.iris.modded.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
import art.arcane.iris.modded.ModdedScheduler;
|
import art.arcane.iris.modded.ModdedScheduler;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
@@ -8,19 +9,130 @@ import org.mockito.InOrder;
|
|||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNotSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
import static org.mockito.Mockito.inOrder;
|
import static org.mockito.Mockito.inOrder;
|
||||||
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class ModdedStudioHotloadServiceTest {
|
public class ModdedStudioHotloadServiceTest {
|
||||||
|
@Test
|
||||||
|
public void manualHotloadRunsWithoutAFileChangeOnTheWorker() throws Exception {
|
||||||
|
ModdedStudioHotloadService service = new ModdedStudioHotloadService();
|
||||||
|
Engine engine = studioEngine();
|
||||||
|
Thread caller = Thread.currentThread();
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
assertNotSame(caller, Thread.currentThread());
|
||||||
|
return null;
|
||||||
|
}).when(engine).hotloadSilently();
|
||||||
|
service.onEnable();
|
||||||
|
try {
|
||||||
|
assertTrue(service.requestHotload("irisworldgen:studio_test", engine).get(5L, TimeUnit.SECONDS));
|
||||||
|
verify(engine).hotloadSilently();
|
||||||
|
} finally {
|
||||||
|
service.onDisable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void overlappingManualHotloadIsRejected() throws Exception {
|
||||||
|
ModdedStudioHotloadService service = new ModdedStudioHotloadService();
|
||||||
|
Engine engine = studioEngine();
|
||||||
|
CountDownLatch entered = new CountDownLatch(1);
|
||||||
|
CountDownLatch release = new CountDownLatch(1);
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
entered.countDown();
|
||||||
|
assertTrue(release.await(5L, TimeUnit.SECONDS));
|
||||||
|
return null;
|
||||||
|
}).when(engine).hotloadSilently();
|
||||||
|
service.onEnable();
|
||||||
|
try {
|
||||||
|
CompletableFuture<Boolean> first = service.requestHotload("irisworldgen:studio_test", engine);
|
||||||
|
assertTrue(entered.await(5L, TimeUnit.SECONDS));
|
||||||
|
assertFalse(service.requestHotload("irisworldgen:studio_test", engine).get(5L, TimeUnit.SECONDS));
|
||||||
|
release.countDown();
|
||||||
|
assertTrue(first.get(5L, TimeUnit.SECONDS));
|
||||||
|
verify(engine).hotloadSilently();
|
||||||
|
} finally {
|
||||||
|
release.countDown();
|
||||||
|
service.onDisable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void closingStudioIsNotHotloadedAfterWaitingInTheQueue() throws Exception {
|
||||||
|
ModdedStudioHotloadService service = new ModdedStudioHotloadService();
|
||||||
|
Engine engine = studioEngine();
|
||||||
|
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||||
|
CountDownLatch release = new CountDownLatch(1);
|
||||||
|
executor.submit(() -> {
|
||||||
|
try {
|
||||||
|
assertTrue(release.await(5L, TimeUnit.SECONDS));
|
||||||
|
} catch (InterruptedException failure) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IllegalStateException(failure);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setExecutor(service, executor);
|
||||||
|
try {
|
||||||
|
CompletableFuture<Boolean> requested = service.requestHotload("irisworldgen:studio_test", engine);
|
||||||
|
when(engine.isClosing()).thenReturn(true);
|
||||||
|
release.countDown();
|
||||||
|
assertThrows(ExecutionException.class, () -> requested.get(5L, TimeUnit.SECONDS));
|
||||||
|
verify(engine, never()).hotloadSilently();
|
||||||
|
} finally {
|
||||||
|
release.countDown();
|
||||||
|
service.onDisable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void manualHotloadRejectsProductionWorlds() throws Exception {
|
||||||
|
ModdedStudioHotloadService service = new ModdedStudioHotloadService();
|
||||||
|
Engine engine = studioEngine();
|
||||||
|
service.onEnable();
|
||||||
|
try {
|
||||||
|
assertThrows(ExecutionException.class, () -> service.requestHotload("irisworldgen:production", engine)
|
||||||
|
.get(5L, TimeUnit.SECONDS));
|
||||||
|
when(engine.isStudio()).thenReturn(false);
|
||||||
|
assertThrows(ExecutionException.class, () -> service.requestHotload("irisworldgen:studio_test", engine)
|
||||||
|
.get(5L, TimeUnit.SECONDS));
|
||||||
|
verify(engine, never()).hotloadSilently();
|
||||||
|
} finally {
|
||||||
|
service.onDisable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failedManualHotloadReleasesTheWatchForRetry() throws Exception {
|
||||||
|
ModdedStudioHotloadService service = new ModdedStudioHotloadService();
|
||||||
|
Engine engine = studioEngine();
|
||||||
|
doThrow(new IllegalStateException("Invalid pack")).doNothing().when(engine).hotloadSilently();
|
||||||
|
service.onEnable();
|
||||||
|
try {
|
||||||
|
assertThrows(ExecutionException.class, () -> service.requestHotload("irisworldgen:studio_test", engine)
|
||||||
|
.get(5L, TimeUnit.SECONDS));
|
||||||
|
assertTrue(service.requestHotload("irisworldgen:studio_test", engine).get(5L, TimeUnit.SECONDS));
|
||||||
|
} finally {
|
||||||
|
service.onDisable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shutdownDrainsWorkerBeforeAllowingRestart() throws Exception {
|
public void shutdownDrainsWorkerBeforeAllowingRestart() throws Exception {
|
||||||
ModdedStudioHotloadService service = new ModdedStudioHotloadService();
|
ModdedStudioHotloadService service = new ModdedStudioHotloadService();
|
||||||
@@ -102,4 +214,11 @@ public class ModdedStudioHotloadServiceTest {
|
|||||||
field.set(service, executor);
|
field.set(service, executor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Engine studioEngine() {
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
when(engine.isStudio()).thenReturn(true);
|
||||||
|
when(engine.getDimension()).thenReturn(new IrisDimension());
|
||||||
|
return engine;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
package art.arcane.volmlib.nativelib.minecraft26_2.modded;
|
||||||
|
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeModdedBiomePolicy;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.mojang.serialization.DataResult;
|
||||||
|
import com.mojang.serialization.JsonOps;
|
||||||
|
import com.mojang.serialization.MapCodec;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.biome.BiomeSource;
|
||||||
|
import net.minecraft.world.level.biome.Climate;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class NativeModdedBiomeSourceCodecTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrap() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void runtimeSourceEncodeReportsErrorInsteadOfThrowing() {
|
||||||
|
DataResult<JsonElement> result = BiomeSource.CODEC.encodeStart(JsonOps.INSTANCE, runtimeSource("epoch:unused"));
|
||||||
|
assertTrue(result.isError());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void structureStateSourceEncodesItsGenerationKey() throws ReflectiveOperationException {
|
||||||
|
BiomeSource structureState = structureState(runtimeSource("epoch:example"));
|
||||||
|
MapCodec<BiomeSource> codec = (MapCodec<BiomeSource>) NativeModdedBiomeSource.STRUCTURE_STATE_CODEC;
|
||||||
|
|
||||||
|
JsonElement encoded = codec.codec().encodeStart(JsonOps.INSTANCE, structureState).getOrThrow();
|
||||||
|
|
||||||
|
assertEquals("epoch:example", encoded.getAsJsonObject().get("generation").getAsString());
|
||||||
|
assertTrue(codec.codec().parse(JsonOps.INSTANCE, encoded).isError());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BiomeSource structureState(NativeModdedBiomeSource<?> source) throws ReflectiveOperationException {
|
||||||
|
Class<?> type = Class.forName(NativeModdedBiomeSource.class.getName() + "$StructureStateBiomeSource");
|
||||||
|
Constructor<?> constructor = type.getDeclaredConstructor(NativeModdedBiomeSource.class, Set.class);
|
||||||
|
constructor.setAccessible(true);
|
||||||
|
return (BiomeSource) constructor.newInstance(source, Set.<Holder<Biome>>of());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static NativeModdedBiomeSource<NativeModdedBiomePolicy<Holder<Biome>, Climate.Sampler>> runtimeSource(String key) {
|
||||||
|
NativeModdedBiomePolicy<Holder<Biome>, Climate.Sampler> policy = mock(NativeModdedBiomePolicy.class);
|
||||||
|
when(policy.structureStateKey()).thenReturn(key);
|
||||||
|
return new NativeModdedBiomeSource<>(new NativeModdedBiomeSource.Options<>(
|
||||||
|
mock(BiomeSource.class), () -> null, access -> policy));
|
||||||
|
}
|
||||||
|
}
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
package art.arcane.volmlib.nativelib.minecraft26_2.modded;
|
||||||
|
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.IdMapper;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.biome.BiomeGenerationSettings;
|
||||||
|
import net.minecraft.world.level.biome.BiomeSpecialEffects;
|
||||||
|
import net.minecraft.world.level.biome.MobSpawnSettings;
|
||||||
|
import net.minecraft.world.level.block.Block;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.PalettedContainer;
|
||||||
|
import net.minecraft.world.level.chunk.PalettedContainerFactory;
|
||||||
|
import net.minecraft.world.level.chunk.PalettedContainerRO;
|
||||||
|
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||||
|
import net.minecraft.world.level.chunk.Strategy;
|
||||||
|
import net.minecraft.world.level.chunk.UpgradeData;
|
||||||
|
import org.junit.Assume;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class NativeModdedChunkGeneratorTerrainWriteTest {
|
||||||
|
private static final int MIN_Y = -64;
|
||||||
|
private static final int HEIGHT = 384;
|
||||||
|
private static final int SURFACE = 10;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrap() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void freshSectionsInstallWithoutSelfLockingTheirContainers() throws Exception {
|
||||||
|
BlockState stone = Blocks.STONE.defaultBlockState();
|
||||||
|
Assume.assumeTrue("block-state ids are not baked on this classpath",
|
||||||
|
Block.BLOCK_STATE_REGISTRY.getId(stone) >= 0);
|
||||||
|
NativeBlockState nativeStone = mock(NativeBlockState.class);
|
||||||
|
when(nativeStone.nativeHandle()).thenReturn(stone);
|
||||||
|
NativeModdedGeneratorPolicy.BlockBuffer blocks = (x, y, z) -> y + MIN_Y < SURFACE ? nativeStone : null;
|
||||||
|
ProtoChunk chunk = newChunk();
|
||||||
|
Method writeBlocks = NativeModdedChunkGenerator.class.getDeclaredMethod("writeBlocks", ChunkAccess.class,
|
||||||
|
NativeModdedGeneratorPolicy.BlockBuffer.class, int.class, int.class);
|
||||||
|
writeBlocks.setAccessible(true);
|
||||||
|
|
||||||
|
CompletableFuture.runAsync(() -> invoke(writeBlocks, chunk, blocks)).get(30, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
assertSame(stone, chunk.getBlockState(new BlockPos(3, SURFACE - 1, 5)));
|
||||||
|
assertSame(stone, chunk.getBlockState(new BlockPos(0, MIN_Y, 0)));
|
||||||
|
assertSame(Blocks.AIR.defaultBlockState(), chunk.getBlockState(new BlockPos(3, SURFACE, 5)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void invoke(Method writeBlocks, ChunkAccess chunk, NativeModdedGeneratorPolicy.BlockBuffer blocks) {
|
||||||
|
try {
|
||||||
|
writeBlocks.invoke(null, chunk, blocks, MIN_Y, HEIGHT);
|
||||||
|
} catch (IllegalAccessException | InvocationTargetException failure) {
|
||||||
|
throw new IllegalStateException(failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProtoChunk newChunk() {
|
||||||
|
return new ProtoChunk(new ChunkPos(0, 0), UpgradeData.EMPTY, LevelHeightAccessor.create(MIN_Y, HEIGHT),
|
||||||
|
palettedContainerFactory(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PalettedContainerFactory palettedContainerFactory() {
|
||||||
|
Strategy<BlockState> blockStrategy = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY);
|
||||||
|
Codec<PalettedContainer<BlockState>> blockCodec = PalettedContainer.codecRW(
|
||||||
|
BlockState.CODEC, blockStrategy, Blocks.AIR.defaultBlockState());
|
||||||
|
Biome biome = new Biome.BiomeBuilder()
|
||||||
|
.hasPrecipitation(false)
|
||||||
|
.temperature(0.8F)
|
||||||
|
.downfall(0.4F)
|
||||||
|
.specialEffects(new BiomeSpecialEffects.Builder().waterColor(0x3F76E4).build())
|
||||||
|
.mobSpawnSettings(MobSpawnSettings.EMPTY)
|
||||||
|
.generationSettings(BiomeGenerationSettings.EMPTY)
|
||||||
|
.build();
|
||||||
|
Holder<Biome> biomeHolder = Holder.direct(biome);
|
||||||
|
IdMapper<Holder<Biome>> biomeIds = new IdMapper<>(1);
|
||||||
|
biomeIds.add(biomeHolder);
|
||||||
|
Strategy<Holder<Biome>> biomeStrategy = Strategy.createForBiomes(biomeIds);
|
||||||
|
Codec<PalettedContainerRO<Holder<Biome>>> biomeCodec = PalettedContainer.codecRO(
|
||||||
|
Biome.CODEC, biomeStrategy, biomeHolder);
|
||||||
|
return new PalettedContainerFactory(blockStrategy, Blocks.AIR.defaultBlockState(), blockCodec,
|
||||||
|
biomeStrategy, biomeHolder, biomeCodec);
|
||||||
|
}
|
||||||
|
}
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
package art.arcane.volmlib.nativelib.minecraft26_2.modded;
|
||||||
|
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
|
import com.mojang.serialization.Lifecycle;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.MappedRegistry;
|
||||||
|
import net.minecraft.core.RegistrationInfo;
|
||||||
|
import net.minecraft.core.Registry;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.biome.BiomeGenerationSettings;
|
||||||
|
import net.minecraft.world.level.biome.BiomeSpecialEffects;
|
||||||
|
import net.minecraft.world.level.biome.MobSpawnSettings;
|
||||||
|
import net.minecraft.world.level.block.Block;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||||
|
import net.minecraft.world.level.chunk.UpgradeData;
|
||||||
|
import org.junit.Assume;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class NativeModdedChunkGeneratorTerrainWriteTest {
|
||||||
|
private static final int MIN_Y = -64;
|
||||||
|
private static final int HEIGHT = 384;
|
||||||
|
private static final int SURFACE = 10;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrap() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void freshSectionsInstallWithoutSelfLockingTheirContainers() throws Exception {
|
||||||
|
BlockState stone = Blocks.STONE.defaultBlockState();
|
||||||
|
Assume.assumeTrue("block-state ids are not baked on this classpath",
|
||||||
|
Block.BLOCK_STATE_REGISTRY.getId(stone) >= 0);
|
||||||
|
NativeBlockState nativeStone = mock(NativeBlockState.class);
|
||||||
|
when(nativeStone.nativeHandle()).thenReturn(stone);
|
||||||
|
NativeModdedGeneratorPolicy.BlockBuffer blocks = (x, y, z) -> y + MIN_Y < SURFACE ? nativeStone : null;
|
||||||
|
ProtoChunk chunk = newChunk();
|
||||||
|
Method writeBlocks = NativeModdedChunkGenerator.class.getDeclaredMethod("writeBlocks", ChunkAccess.class,
|
||||||
|
NativeModdedGeneratorPolicy.BlockBuffer.class, int.class, int.class);
|
||||||
|
writeBlocks.setAccessible(true);
|
||||||
|
|
||||||
|
CompletableFuture.runAsync(() -> invoke(writeBlocks, chunk, blocks)).get(30, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
assertSame(stone, chunk.getBlockState(new BlockPos(3, SURFACE - 1, 5)));
|
||||||
|
assertSame(stone, chunk.getBlockState(new BlockPos(0, MIN_Y, 0)));
|
||||||
|
assertSame(Blocks.AIR.defaultBlockState(), chunk.getBlockState(new BlockPos(3, SURFACE, 5)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void invoke(Method writeBlocks, ChunkAccess chunk, NativeModdedGeneratorPolicy.BlockBuffer blocks) {
|
||||||
|
try {
|
||||||
|
writeBlocks.invoke(null, chunk, blocks, MIN_Y, HEIGHT);
|
||||||
|
} catch (IllegalAccessException | InvocationTargetException failure) {
|
||||||
|
throw new IllegalStateException(failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProtoChunk newChunk() {
|
||||||
|
return new ProtoChunk(new ChunkPos(0, 0), UpgradeData.EMPTY, LevelHeightAccessor.create(MIN_Y, HEIGHT),
|
||||||
|
biomeRegistry(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Registry<Biome> biomeRegistry() {
|
||||||
|
Biome biome = new Biome.BiomeBuilder()
|
||||||
|
.hasPrecipitation(false)
|
||||||
|
.temperature(0.8F)
|
||||||
|
.downfall(0.4F)
|
||||||
|
.specialEffects(new BiomeSpecialEffects.Builder().fogColor(0).waterColor(0x3F76E4)
|
||||||
|
.waterFogColor(0).skyColor(0).build())
|
||||||
|
.mobSpawnSettings(MobSpawnSettings.EMPTY)
|
||||||
|
.generationSettings(BiomeGenerationSettings.EMPTY)
|
||||||
|
.build();
|
||||||
|
MappedRegistry<Biome> registry = new MappedRegistry<>(Registries.BIOME, Lifecycle.stable());
|
||||||
|
registry.register(ResourceKey.create(Registries.BIOME, ResourceLocation.withDefaultNamespace("plains")),
|
||||||
|
biome, RegistrationInfo.BUILT_IN);
|
||||||
|
return registry.freeze();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -60,7 +60,7 @@ apply from: file('../../gradle/java21-runtime-exclusions.gradle')
|
|||||||
|
|
||||||
Properties rootProperties = new Properties()
|
Properties rootProperties = new Properties()
|
||||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.5'))
|
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.7'))
|
||||||
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
|
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
|
||||||
String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse(irisModTarget.neoForge.toString())
|
String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse(irisModTarget.neoForge.toString())
|
||||||
Closure<String> irisArtifactName = { String platform, String targetVersion ->
|
Closure<String> irisArtifactName = { String platform, String targetVersion ->
|
||||||
@@ -132,6 +132,8 @@ configurations.named('bundle').configure {
|
|||||||
exclude(group: 'de.crazydev22.slimjar.helper')
|
exclude(group: 'de.crazydev22.slimjar.helper')
|
||||||
exclude(group: 'de.crazydev22.slimjar')
|
exclude(group: 'de.crazydev22.slimjar')
|
||||||
exclude(group: 'io.github.slimjar')
|
exclude(group: 'io.github.slimjar')
|
||||||
|
// The game ships its own LWJGL; without the modules it leaves out, the map's 3D views draw in software.
|
||||||
|
exclude(group: 'org.lwjgl')
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations.compileClasspath.extendsFrom(configurations.devBundle)
|
configurations.compileClasspath.extendsFrom(configurations.devBundle)
|
||||||
@@ -141,7 +143,7 @@ configurations.testRuntimeClasspath.extendsFrom(configurations.runtimeClasspath,
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation('junit:junit:4.13.2')
|
testImplementation('junit:junit:4.13.2')
|
||||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
testImplementation(libs.mockito.core)
|
||||||
testImplementation(testFixtures('art.arcane:core'))
|
testImplementation(testFixtures('art.arcane:core'))
|
||||||
testImplementation(libs.spigot) {
|
testImplementation(libs.spigot) {
|
||||||
transitive = false
|
transitive = false
|
||||||
@@ -173,7 +175,9 @@ dependencies {
|
|||||||
libs.caffeine,
|
libs.caffeine,
|
||||||
'com.fulcrumgenomics:jlibdeflate:0.2.0',
|
'com.fulcrumgenomics:jlibdeflate:0.2.0',
|
||||||
libs.dom4j,
|
libs.dom4j,
|
||||||
libs.jaxen
|
libs.jaxen,
|
||||||
|
libs.flatlaf,
|
||||||
|
libs.flatlaf.extras
|
||||||
]
|
]
|
||||||
add('bundle', libs.asm)
|
add('bundle', libs.asm)
|
||||||
add('devBundle', 'org.ow2.asm:asm') {
|
add('devBundle', 'org.ow2.asm:asm') {
|
||||||
@@ -261,6 +265,8 @@ tasks.named('compileJava', JavaCompile).configure {
|
|||||||
options.release.set((int) irisModTarget.java)
|
options.release.set((int) irisModTarget.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply from: file('../../gradle/mockito-agent.gradle')
|
||||||
|
|
||||||
tasks.named('test').configure {
|
tasks.named('test').configure {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
classpath = classpath.minus(developmentModLibraries).plus(sourceSets.modLibraries.output)
|
classpath = classpath.minus(developmentModLibraries).plus(sourceSets.modLibraries.output)
|
||||||
|
|||||||
+69
-7
@@ -55,7 +55,7 @@ apply from: file('gradle/runtime-relocations.gradle')
|
|||||||
apply from: file('gradle/volmlib-stamp.gradle')
|
apply from: file('gradle/volmlib-stamp.gradle')
|
||||||
|
|
||||||
group = 'art.arcane'
|
group = 'art.arcane'
|
||||||
version = providers.gradleProperty('irisVersion').getOrElse('4.3.5')
|
version = providers.gradleProperty('irisVersion').getOrElse('4.3.7')
|
||||||
|
|
||||||
project(':core') {
|
project(':core') {
|
||||||
group = 'art.arcane'
|
group = 'art.arcane'
|
||||||
@@ -89,7 +89,6 @@ Closure<String> irisArtifactName = { String platform, String targetVersion ->
|
|||||||
String bukkitArtifactName = irisArtifactName('CraftBukkit', bukkitMinecraftRange)
|
String bukkitArtifactName = irisArtifactName('CraftBukkit', bukkitMinecraftRange)
|
||||||
Map<String, String> runtimeDownloadRelocations = (Map<String, String>) ext.runtimeDownloadRelocations
|
Map<String, String> runtimeDownloadRelocations = (Map<String, String>) ext.runtimeDownloadRelocations
|
||||||
List<String> runtimeDownloadedPrefixes = (List<String>) ext.runtimeDownloadedPrefixes
|
List<String> runtimeDownloadedPrefixes = (List<String>) ext.runtimeDownloadedPrefixes
|
||||||
File bukkitArtifact = layout.buildDirectory.file("libs/${bukkitArtifactName}").get().asFile
|
|
||||||
apply plugin: ApiGenerator
|
apply plugin: ApiGenerator
|
||||||
|
|
||||||
List<String> nmsBindings = ['v26_2_R1', 'v26_3_R1']
|
List<String> nmsBindings = ['v26_2_R1', 'v26_3_R1']
|
||||||
@@ -127,7 +126,7 @@ nmsBindings.each { String key ->
|
|||||||
compileOnly(rootProject.libs.annotations)
|
compileOnly(rootProject.libs.annotations)
|
||||||
compileOnly(rootProject.libs.byteBuddy.core)
|
compileOnly(rootProject.libs.byteBuddy.core)
|
||||||
testImplementation('junit:junit:4.13.2')
|
testImplementation('junit:junit:4.13.2')
|
||||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
testImplementation(rootProject.libs.mockito.core)
|
||||||
if (key == 'v26_3_R1') {
|
if (key == 'v26_3_R1') {
|
||||||
testImplementation(project(':probe'))
|
testImplementation(project(':probe'))
|
||||||
}
|
}
|
||||||
@@ -194,6 +193,7 @@ tasks.named('jar', Jar).configure {
|
|||||||
includeEmptyDirs = false
|
includeEmptyDirs = false
|
||||||
from(jarJar)
|
from(jarJar)
|
||||||
from(provider { included.resolve().collect { zipTree(it) } }) {
|
from(provider { included.resolve().collect { zipTree(it) } }) {
|
||||||
|
exclude('META-INF/INDEX.LIST')
|
||||||
exclude('languages/**')
|
exclude('languages/**')
|
||||||
exclude('com/fren_gor/**')
|
exclude('com/fren_gor/**')
|
||||||
|
|
||||||
@@ -534,19 +534,20 @@ List<String> requiredBukkitArtifactEntries = [
|
|||||||
tasks.register('verifyBukkitArtifact') {
|
tasks.register('verifyBukkitArtifact') {
|
||||||
group = 'verification'
|
group = 'verification'
|
||||||
dependsOn('jar')
|
dependsOn('jar')
|
||||||
File artifact = bukkitArtifact
|
Provider<RegularFile> artifact = tasks.named('jar', Jar).flatMap { Jar task -> task.archiveFile }
|
||||||
inputs.file(artifact)
|
inputs.file(artifact)
|
||||||
inputs.files(nativeRuntime)
|
inputs.files(nativeRuntime)
|
||||||
inputs.files(commandHandlerSources)
|
inputs.files(commandHandlerSources)
|
||||||
inputs.files(mantleComponentSources)
|
inputs.files(mantleComponentSources)
|
||||||
doLast {
|
doLast {
|
||||||
|
File archive = artifact.get().asFile
|
||||||
BukkitArtifactVerifier.verify(
|
BukkitArtifactVerifier.verify(
|
||||||
artifact,
|
archive,
|
||||||
requiredBukkitArtifactEntries,
|
requiredBukkitArtifactEntries,
|
||||||
16,
|
16,
|
||||||
runtimeDownloadedPrefixes)
|
runtimeDownloadedPrefixes)
|
||||||
NativeRuntimeArtifacts.verify(artifact, nativeRuntimeModules, embeddedNatives())
|
NativeRuntimeArtifacts.verify(archive, nativeRuntimeModules, embeddedNatives())
|
||||||
logger.lifecycle("Verified ${artifact.name} packaging and class reference graph")
|
logger.lifecycle("Verified ${archive.name} packaging and class reference graph")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,11 +587,14 @@ allprojects {
|
|||||||
layout.buildDirectory.set(layout.projectDirectory.dir("build/${runtimeOutputVariant}"))
|
layout.buildDirectory.set(layout.projectDirectory.dir("build/${runtimeOutputVariant}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply from: rootProject.file('gradle/mockito-agent.gradle')
|
||||||
|
|
||||||
tasks.withType(Test).configureEach {
|
tasks.withType(Test).configureEach {
|
||||||
int moduleForks = project.path.startsWith(':adapters:bukkit:') ? Math.min(2, testForks)
|
int moduleForks = project.path.startsWith(':adapters:bukkit:') ? Math.min(2, testForks)
|
||||||
: project.path == ':probe' ? Math.min(4, testForks) : testForks
|
: project.path == ':probe' ? Math.min(4, testForks) : testForks
|
||||||
maxParallelForks = Math.min(gradle.startParameter.maxWorkerCount, moduleForks)
|
maxParallelForks = Math.min(gradle.startParameter.maxWorkerCount, moduleForks)
|
||||||
jvmArgs('-XX:ParallelGCThreads=2', '-XX:ConcGCThreads=1', '-XX:CICompilerCount=2')
|
jvmArgs('-XX:ParallelGCThreads=2', '-XX:ConcGCThreads=1', '-XX:CICompilerCount=2')
|
||||||
|
timeout = java.time.Duration.ofMinutes(60)
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
@@ -731,6 +735,7 @@ pluginPackaging {
|
|||||||
policyName = 'Iris Bukkit'
|
policyName = 'Iris Bukkit'
|
||||||
packed = true
|
packed = true
|
||||||
packedJarAccessors = ['art/arcane/iris/platform/bukkit/plugin/VolmitPlugin#getJarFile']
|
packedJarAccessors = ['art/arcane/iris/platform/bukkit/plugin/VolmitPlugin#getJarFile']
|
||||||
|
packedPayloadOnlyPrefixes = ['iris-language-guides/']
|
||||||
stripDirectories = true
|
stripDirectories = true
|
||||||
stripLocalVariables = true
|
stripLocalVariables = true
|
||||||
releaseCompression = providers.gradleProperty('compactRelease')
|
releaseCompression = providers.gradleProperty('compactRelease')
|
||||||
@@ -816,6 +821,13 @@ project(':adapters:bukkit:nms:v26_3_R1') {
|
|||||||
}
|
}
|
||||||
sourceSets.test.compileClasspath += sourceSets.offline.output
|
sourceSets.test.compileClasspath += sourceSets.offline.output
|
||||||
sourceSets.test.runtimeClasspath += sourceSets.offline.output
|
sourceSets.test.runtimeClasspath += sourceSets.offline.output
|
||||||
|
tasks.named('test').configure {
|
||||||
|
String exportProbePack = providers.gradleProperty('iris.exportProbePack').orNull
|
||||||
|
if (exportProbePack != null) {
|
||||||
|
systemProperty('iris.exportProbePack', exportProbePack)
|
||||||
|
maxHeapSize = '6g'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TaskProvider<Jar> offlineJar = tasks.register('offlineJar', Jar) {
|
TaskProvider<Jar> offlineJar = tasks.register('offlineJar', Jar) {
|
||||||
dependsOn(offlineClasses)
|
dependsOn(offlineClasses)
|
||||||
@@ -843,6 +855,30 @@ project(':adapters:bukkit:nms:v26_3_R1') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.register('datapackCostProbe', JavaExec) {
|
||||||
|
group = 'verification'
|
||||||
|
description = 'Exports an Iris dimension as a 26.3 worldgen datapack and measures its load, chunk and density function cost against vanilla.'
|
||||||
|
dependsOn(offlineClasses)
|
||||||
|
mainClass = 'art.arcane.iris.probe.DatapackCostProbe'
|
||||||
|
classpath = sourceSets.offline.runtimeClasspath
|
||||||
|
maxHeapSize = providers.gradleProperty('probeHeap').getOrElse('24G')
|
||||||
|
jvmArgs('--add-modules', 'jdk.incubator.vector', '-Djava.awt.headless=true')
|
||||||
|
doFirst {
|
||||||
|
String dimension = providers.gradleProperty('probeDimension').getOrElse('overworld')
|
||||||
|
String output = providers.gradleProperty('probeOutput')
|
||||||
|
.getOrElse(layout.buildDirectory.file("reports/datapack-cost/${dimension}.json").get().asFile.absolutePath)
|
||||||
|
args([rootProject.file(providers.gradleProperty('probePack').get()).absolutePath, dimension,
|
||||||
|
providers.gradleProperty('probeSeed').getOrElse('1337'),
|
||||||
|
providers.gradleProperty('probeChunkSide').getOrElse('6'),
|
||||||
|
providers.gradleProperty('probeRounds').getOrElse('4'),
|
||||||
|
providers.gradleProperty('probeWarmupSide').getOrElse('6'),
|
||||||
|
providers.gradleProperty('probeCalls').getOrElse('256'),
|
||||||
|
providers.gradleProperty('probeCallSeconds').getOrElse('60'),
|
||||||
|
rootProject.file(output).absolutePath,
|
||||||
|
layout.buildDirectory.dir('tmp/datapack-cost').get().asFile.absolutePath])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tasks.register('regionMantleInspection', JavaExec) {
|
tasks.register('regionMantleInspection', JavaExec) {
|
||||||
group = 'verification'
|
group = 'verification'
|
||||||
dependsOn(testClasses)
|
dependsOn(testClasses)
|
||||||
@@ -872,6 +908,32 @@ project(':adapters:bukkit:nms:v26_3_R1') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.register('datapackFidelityProbe', JavaExec) {
|
||||||
|
group = 'verification'
|
||||||
|
description = 'Exports an Iris dimension as a 26.3 worldgen datapack and compares both generators on the same seed.'
|
||||||
|
dependsOn(offlineClasses)
|
||||||
|
mainClass = 'art.arcane.iris.probe.DatapackFidelityProbe'
|
||||||
|
classpath = sourceSets.offline.runtimeClasspath
|
||||||
|
maxHeapSize = '8G'
|
||||||
|
jvmArgs('--add-modules', 'jdk.incubator.vector', '-Djava.awt.headless=true')
|
||||||
|
doFirst {
|
||||||
|
String dimension = providers.gradleProperty('probeDimension').get()
|
||||||
|
String seed = providers.gradleProperty('probeSeed').get()
|
||||||
|
String mode = providers.gradleProperty('probeMode').getOrElse('full')
|
||||||
|
boolean biomes = mode == 'biomes'
|
||||||
|
String output = providers.gradleProperty('probeOutput')
|
||||||
|
.getOrElse(layout.buildDirectory.file("reports/datapack-fidelity/${dimension}-${seed}${biomes ? '-biomes' : ''}.json").get().asFile.absolutePath)
|
||||||
|
args([rootProject.file(providers.gradleProperty('probePack').get()).absolutePath, dimension, seed,
|
||||||
|
biomes ? providers.gradleProperty('probeRadiusChunks').getOrElse('0') : providers.gradleProperty('probeRadiusChunks').get(),
|
||||||
|
biomes ? providers.gradleProperty('probeStride').getOrElse('1') : providers.gradleProperty('probeStride').get(),
|
||||||
|
rootProject.file(output).absolutePath,
|
||||||
|
providers.gradleProperty('probeSurveyBlocks').getOrElse('65536'),
|
||||||
|
providers.gradleProperty('probeSurveyStride').getOrElse('256'),
|
||||||
|
providers.gradleProperty('probeSurveySeeds').getOrElse(biomes ? '7' : '1'),
|
||||||
|
mode])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+20
-8
@@ -108,6 +108,10 @@ dependencies {
|
|||||||
compileOnly(libs.log4j.api)
|
compileOnly(libs.log4j.api)
|
||||||
compileOnly(libs.log4j.core)
|
compileOnly(libs.log4j.core)
|
||||||
compileOnly(libs.guava)
|
compileOnly(libs.guava)
|
||||||
|
compileOnly(libs.lwjgl)
|
||||||
|
compileOnly(libs.lwjgl.opengl)
|
||||||
|
compileOnly(libs.lwjgl.egl)
|
||||||
|
compileOnly(libs.lwjgl.jawt)
|
||||||
|
|
||||||
// Third Party Integrations
|
// Third Party Integrations
|
||||||
compileOnly(libs.nexo)
|
compileOnly(libs.nexo)
|
||||||
@@ -161,25 +165,38 @@ dependencies {
|
|||||||
slim(libs.byteBuddy.agent)
|
slim(libs.byteBuddy.agent)
|
||||||
slim(libs.dom4j)
|
slim(libs.dom4j)
|
||||||
slim(libs.jaxen)
|
slim(libs.jaxen)
|
||||||
|
slim(libs.flatlaf)
|
||||||
|
slim(libs.flatlaf.extras)
|
||||||
|
|
||||||
testFixturesImplementation('junit:junit:4.13.2')
|
testFixturesImplementation('junit:junit:4.13.2')
|
||||||
testFixturesImplementation('org.mockito:mockito-core:5.23.0')
|
testFixturesImplementation(libs.mockito.core)
|
||||||
testFixturesCompileOnly(libs.paper.api)
|
testFixturesCompileOnly(libs.paper.api)
|
||||||
|
|
||||||
testImplementation('junit:junit:4.13.2')
|
testImplementation('junit:junit:4.13.2')
|
||||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
testImplementation(libs.mockito.core)
|
||||||
testImplementation(libs.paper.api)
|
testImplementation(libs.paper.api)
|
||||||
testImplementation(libs.log4j.core)
|
testImplementation(libs.log4j.core)
|
||||||
testRuntimeOnly(libs.paper.api)
|
testRuntimeOnly(libs.paper.api)
|
||||||
testImplementation(libs.multiverseCore)
|
testImplementation(libs.multiverseCore)
|
||||||
testImplementation(libs.annotations)
|
testImplementation(libs.annotations)
|
||||||
testImplementation(libs.mythic)
|
testImplementation(libs.mythic)
|
||||||
|
testRuntimeOnly(libs.lwjgl)
|
||||||
|
testRuntimeOnly(libs.lwjgl.opengl)
|
||||||
|
testRuntimeOnly(libs.lwjgl.egl)
|
||||||
|
testRuntimeOnly(libs.lwjgl.jawt)
|
||||||
|
testRuntimeOnly(variantOf(libs.lwjgl) { classifier('natives-linux') })
|
||||||
|
testRuntimeOnly(variantOf(libs.lwjgl.opengl) { classifier('natives-linux') })
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named('test').configure {
|
tasks.named('test').configure {
|
||||||
maxHeapSize = '1g'
|
timeout = java.time.Duration.ofMinutes(60)
|
||||||
|
maxHeapSize = '2g'
|
||||||
jvmArgs('-XX:+ExitOnOutOfMemoryError')
|
jvmArgs('-XX:+ExitOnOutOfMemoryError')
|
||||||
systemProperty('iris.generation.counters', 'true')
|
systemProperty('iris.generation.counters', 'true')
|
||||||
|
String exportProbePack = providers.gradleProperty('iris.exportProbePack').orNull
|
||||||
|
if (exportProbePack != null) {
|
||||||
|
systemProperty('iris.exportProbePack', exportProbePack)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
@@ -347,14 +364,9 @@ List<String> unusedVolmLibPackages = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
List<String> unusedVolmLibClasses = [
|
List<String> unusedVolmLibClasses = [
|
||||||
'art/arcane/volmlib/util/nbt/io/ParseException',
|
|
||||||
'art/arcane/volmlib/util/nbt/io/SNBTDeserializer',
|
|
||||||
'art/arcane/volmlib/util/nbt/io/SNBTParser',
|
|
||||||
'art/arcane/volmlib/util/nbt/io/SNBTSerializer',
|
'art/arcane/volmlib/util/nbt/io/SNBTSerializer',
|
||||||
'art/arcane/volmlib/util/nbt/io/SNBTUtil',
|
'art/arcane/volmlib/util/nbt/io/SNBTUtil',
|
||||||
'art/arcane/volmlib/util/nbt/io/SNBTWriter',
|
'art/arcane/volmlib/util/nbt/io/SNBTWriter',
|
||||||
'art/arcane/volmlib/util/nbt/io/StringPointer',
|
|
||||||
'art/arcane/volmlib/util/io/StringDeserializer',
|
|
||||||
'art/arcane/volmlib/util/io/StringSerializer',
|
'art/arcane/volmlib/util/io/StringSerializer',
|
||||||
'art/arcane/volmlib/util/json/EnumType',
|
'art/arcane/volmlib/util/json/EnumType',
|
||||||
'art/arcane/volmlib/util/json/HTTP',
|
'art/arcane/volmlib/util/json/HTTP',
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ art/arcane/iris/generation/decoration/IrisSpeleothems.java
|
|||||||
art/arcane/iris/generation/decoration/IrisSurface.java
|
art/arcane/iris/generation/decoration/IrisSurface.java
|
||||||
art/arcane/iris/generation/decoration/tree/IrisTree.java
|
art/arcane/iris/generation/decoration/tree/IrisTree.java
|
||||||
art/arcane/iris/generation/geometry/AxisAlignedBB.java
|
art/arcane/iris/generation/geometry/AxisAlignedBB.java
|
||||||
art/arcane/iris/generation/mantle/MantleWriter.java
|
|
||||||
art/arcane/iris/generation/runtime/BukkitEngineWorldManager.java
|
art/arcane/iris/generation/runtime/BukkitEngineWorldManager.java
|
||||||
art/arcane/iris/generation/runtime/EngineAssignedWorldManager.java
|
art/arcane/iris/generation/runtime/EngineAssignedWorldManager.java
|
||||||
art/arcane/iris/generation/runtime/EnginePlayer.java
|
art/arcane/iris/generation/runtime/EnginePlayer.java
|
||||||
@@ -57,7 +56,6 @@ art/arcane/iris/integration/data/NexoDataProvider.java
|
|||||||
art/arcane/iris/integration/data/OraxenDataProvider.java
|
art/arcane/iris/integration/data/OraxenDataProvider.java
|
||||||
art/arcane/iris/localization/C.java
|
art/arcane/iris/localization/C.java
|
||||||
art/arcane/iris/pack/IrisConverter.java
|
art/arcane/iris/pack/IrisConverter.java
|
||||||
art/arcane/iris/pack/IrisPack.java
|
|
||||||
art/arcane/iris/pack/IrisPackBenchmarking.java
|
art/arcane/iris/pack/IrisPackBenchmarking.java
|
||||||
art/arcane/iris/pack/datapack/DatapackIngestService.java
|
art/arcane/iris/pack/datapack/DatapackIngestService.java
|
||||||
art/arcane/iris/pack/datapack/ServerConfigurator.java
|
art/arcane/iris/pack/datapack/ServerConfigurator.java
|
||||||
@@ -121,13 +119,17 @@ art/arcane/iris/studio/StudioSVC.java
|
|||||||
art/arcane/iris/studio/edit/BlockSignal.java
|
art/arcane/iris/studio/edit/BlockSignal.java
|
||||||
art/arcane/iris/studio/edit/DustRevealer.java
|
art/arcane/iris/studio/edit/DustRevealer.java
|
||||||
art/arcane/iris/studio/generation/JigsawStudioGenerator.java
|
art/arcane/iris/studio/generation/JigsawStudioGenerator.java
|
||||||
|
art/arcane/iris/studio/jigsaw/JigsawStudioConnectorTool.java
|
||||||
|
art/arcane/iris/studio/jigsaw/JigsawStudioFloorPainter.java
|
||||||
art/arcane/iris/studio/generation/ObjectStudioGenerator.java
|
art/arcane/iris/studio/generation/ObjectStudioGenerator.java
|
||||||
art/arcane/iris/studio/jigsaw/JigsawStudioAutosaveScheduler.java
|
art/arcane/iris/studio/jigsaw/JigsawStudioAutosaveScheduler.java
|
||||||
art/arcane/iris/studio/jigsaw/JigsawStudioCapture.java
|
art/arcane/iris/studio/jigsaw/JigsawStudioCapture.java
|
||||||
art/arcane/iris/studio/jigsaw/JigsawStudioCaptureScheduler.java
|
art/arcane/iris/studio/jigsaw/JigsawStudioCaptureScheduler.java
|
||||||
art/arcane/iris/studio/jigsaw/JigsawStudioChunkWriter.java
|
art/arcane/iris/studio/jigsaw/JigsawStudioChunkWriter.java
|
||||||
|
art/arcane/iris/studio/jigsaw/JigsawStudioConnectorTool.java
|
||||||
art/arcane/iris/studio/jigsaw/JigsawStudioDetailsMenu.java
|
art/arcane/iris/studio/jigsaw/JigsawStudioDetailsMenu.java
|
||||||
art/arcane/iris/studio/jigsaw/JigsawStudioDirtyTracker.java
|
art/arcane/iris/studio/jigsaw/JigsawStudioDirtyTracker.java
|
||||||
|
art/arcane/iris/studio/jigsaw/JigsawStudioFloorPainter.java
|
||||||
art/arcane/iris/studio/jigsaw/JigsawStudioGraphMutations.java
|
art/arcane/iris/studio/jigsaw/JigsawStudioGraphMutations.java
|
||||||
art/arcane/iris/studio/jigsaw/JigsawStudioMainMenu.java
|
art/arcane/iris/studio/jigsaw/JigsawStudioMainMenu.java
|
||||||
art/arcane/iris/studio/jigsaw/JigsawStudioMarkerParser.java
|
art/arcane/iris/studio/jigsaw/JigsawStudioMarkerParser.java
|
||||||
|
|||||||
+7
-23
@@ -1,25 +1,14 @@
|
|||||||
package art.arcane.iris.command.specialhandlers;
|
package art.arcane.iris.command.specialhandlers;
|
||||||
|
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureQuery;
|
|
||||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
|
|
||||||
import art.arcane.iris.command.DirectorParameterHandler;
|
import art.arcane.iris.command.DirectorParameterHandler;
|
||||||
|
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
|
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public final class HydrologyTypeHandler implements DirectorParameterHandler<String> {
|
public final class HydrologyTypeHandler implements DirectorParameterHandler<String> {
|
||||||
@Override
|
@Override
|
||||||
public KList<String> getPossibilities() {
|
public KList<String> getPossibilities() {
|
||||||
Engine activeEngine = engine();
|
return new KList<>(HydrologyFeatureType.keys());
|
||||||
if (activeEngine == null) {
|
|
||||||
return new KList<>(HydrologyFeatureQuery.suggestions(List.of()));
|
|
||||||
}
|
|
||||||
IrisHydrologyRuntime runtime = activeEngine.getComplex().getHydrologyRuntime();
|
|
||||||
return new KList<>(runtime == null
|
|
||||||
? HydrologyFeatureQuery.suggestions(List.of())
|
|
||||||
: runtime.featureQueryKeys());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -29,16 +18,11 @@ public final class HydrologyTypeHandler implements DirectorParameterHandler<Stri
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String parse(String input, boolean force) throws DirectorParsingException {
|
public String parse(String input, boolean force) throws DirectorParsingException {
|
||||||
String normalized = input == null ? "" : input.trim();
|
try {
|
||||||
if (normalized.isEmpty()) {
|
return HydrologyFeatureType.parse(input).key();
|
||||||
throw new DirectorParsingException("Hydrology feature type must not be blank.");
|
} catch (IllegalArgumentException error) {
|
||||||
|
throw new DirectorParsingException(error.getMessage());
|
||||||
}
|
}
|
||||||
for (String possibility : getPossibilities(normalized)) {
|
|
||||||
if (possibility.equalsIgnoreCase(normalized)) {
|
|
||||||
return possibility;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return normalized;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -48,6 +32,6 @@ public final class HydrologyTypeHandler implements DirectorParameterHandler<Stri
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getRandomDefault() {
|
public String getRandomDefault() {
|
||||||
return "surface";
|
return HydrologyFeatureType.RIVER.key();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -373,6 +373,7 @@ public class IrisSettings {
|
|||||||
public boolean useServerLaunchedGuis = true;
|
public boolean useServerLaunchedGuis = true;
|
||||||
public boolean maximumPregenGuiFPS = false;
|
public boolean maximumPregenGuiFPS = false;
|
||||||
public boolean colorMode = true;
|
public boolean colorMode = true;
|
||||||
|
public String theme = "auto";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
|||||||
Loaded 100 of 1474 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user