This commit is contained in:
Brian Neumann-Fopiano
2026-04-17 16:14:49 -04:00
parent 8c05f1bf1d
commit b82472d521
42 changed files with 2035 additions and 4130 deletions
+1 -1
View File
@@ -1 +1 @@
1982643195 699705819
@@ -32,7 +32,6 @@ import art.arcane.iris.core.link.IrisPapiExpansion;
import art.arcane.iris.core.link.MultiverseCoreLink; import art.arcane.iris.core.link.MultiverseCoreLink;
import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.INMS; import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.pregenerator.LazyPregenerator;
import art.arcane.iris.core.service.StudioSVC; import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.EnginePanic; import art.arcane.iris.engine.EnginePanic;
@@ -565,7 +564,6 @@ public class Iris extends VolmitPlugin implements Listener {
J.s(() -> { J.s(() -> {
J.a(() -> IO.delete(getTemp())); J.a(() -> IO.delete(getTemp()));
J.a(LazyPregenerator::loadLazyGenerators, 100);
J.a(this::bstats); J.a(this::bstats);
J.ar(this::checkConfigHotload, 60); J.ar(this::checkConfigHotload, 60);
J.sr(this::tickQueue, 0); J.sr(this::tickQueue, 0);
@@ -979,6 +979,11 @@ public final class ExternalDataPackPipeline {
packSourceFolder.mkdirs(); packSourceFolder.mkdirs();
cacheFolder.mkdirs(); cacheFolder.mkdirs();
RequestSyncResult metadataRestore = restoreRequestFromMetadata(packSourceFolder, request);
if (metadataRestore.success()) {
return metadataRestore;
}
try { try {
ResolvedRemoteFile remoteFile = resolveRemoteFile(url); ResolvedRemoteFile remoteFile = resolveRemoteFile(url);
File output = new File(packSourceFolder, remoteFile.outputFileName()); File output = new File(packSourceFolder, remoteFile.outputFileName());
@@ -1012,10 +1017,6 @@ public final class ExternalDataPackPipeline {
writeRequestMetadata(packSourceFolder, request, output.getName(), remoteFile.sha1()); writeRequestMetadata(packSourceFolder, request, output.getName(), remoteFile.sha1());
return RequestSyncResult.downloaded(output); return RequestSyncResult.downloaded(output);
} catch (Throwable e) { } catch (Throwable e) {
RequestSyncResult restored = restoreRequestFromMetadata(packSourceFolder, request);
if (restored.success()) {
return restored;
}
String message = e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage(); String message = e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage();
return RequestSyncResult.failure(message); return RequestSyncResult.failure(message);
} }
@@ -1194,6 +1195,18 @@ public final class ExternalDataPackPipeline {
return ProjectionResult.success(managedName, 0, 0, Set.copyOf(request.resolvedLocateStructures()), 0, Set.of(), 0, 0, 0); return ProjectionResult.success(managedName, 0, 0, Set.copyOf(request.resolvedLocateStructures()), 0, Set.of(), 0, 0, 0);
} }
String projectionCacheKey = buildProjectionCacheKey(sourceDescriptor.fingerprint(), request);
File projectionCacheDir = Iris.instance.getDataFolder("cache", "projected-datapacks");
File cachedZip = new File(projectionCacheDir, projectionCacheKey + ".zip");
File cachedMeta = new File(projectionCacheDir, projectionCacheKey + ".json");
if (cachedZip.exists() && cachedZip.length() > 0 && cachedMeta.exists()) {
ProjectionResult cachedResult = restoreCachedProjection(cachedZip, cachedMeta, managedName, worldDatapackFolders);
if (cachedResult != null) {
return cachedResult;
}
}
ProjectionAssetSummary projectionAssetSummary; ProjectionAssetSummary projectionAssetSummary;
try { try {
projectionAssetSummary = buildProjectedAssets(source, sourceDescriptor, request); projectionAssetSummary = buildProjectedAssets(source, sourceDescriptor, request);
@@ -1219,6 +1232,7 @@ public final class ExternalDataPackPipeline {
int installedDatapacks = 0; int installedDatapacks = 0;
int installedAssets = 0; int installedAssets = 0;
boolean firstWrite = true;
for (File worldDatapackFolder : worldDatapackFolders) { for (File worldDatapackFolder : worldDatapackFolders) {
if (worldDatapackFolder == null) { if (worldDatapackFolder == null) {
continue; continue;
@@ -1242,6 +1256,11 @@ public final class ExternalDataPackPipeline {
} }
installedDatapacks++; installedDatapacks++;
installedAssets += copiedAssets; installedAssets += copiedAssets;
if (firstWrite && managedZip.exists()) {
cacheProjection(managedZip, cachedZip, cachedMeta, projectionAssetSummary);
firstWrite = false;
}
} catch (Throwable e) { } catch (Throwable e) {
Iris.warn("Failed to project external datapack source " + sourceDescriptor.sourceName() + " into " + worldDatapackFolder.getPath()); Iris.warn("Failed to project external datapack source " + sourceDescriptor.sourceName() + " into " + worldDatapackFolder.getPath());
Iris.reportError(e); Iris.reportError(e);
@@ -4091,6 +4110,112 @@ public final class ExternalDataPackPipeline {
return builder.toString(); return builder.toString();
} }
private static String buildProjectionCacheKey(String sourceFingerprint, DatapackRequest request) {
StringBuilder keyBuilder = new StringBuilder();
keyBuilder.append(sourceFingerprint);
keyBuilder.append('|').append(request.getDedupeKey());
keyBuilder.append('|').append(request.alongsideMode());
keyBuilder.append('|').append(request.templateAliases());
keyBuilder.append('|').append(request.structureStartHeights());
keyBuilder.append('|').append(request.structureSetAliases());
keyBuilder.append('|').append(request.structureAliases());
keyBuilder.append('|').append(request.structures());
keyBuilder.append('|').append(request.structureSets());
keyBuilder.append('|').append(request.templatePools());
keyBuilder.append('|').append(request.processorLists());
keyBuilder.append('|').append(request.configuredFeatures());
keyBuilder.append('|').append(request.placedFeatures());
keyBuilder.append('|').append(request.biomeHasStructureTags());
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
byte[] bytes = digest.digest(keyBuilder.toString().getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
hex.append(String.format("%02x", b));
}
return hex.toString();
} catch (Throwable e) {
return shortHash(keyBuilder.toString());
}
}
private static ProjectionResult restoreCachedProjection(File cachedZip, File cachedMeta, String managedName, KList<File> worldDatapackFolders) {
try {
JSONObject meta = new JSONObject(Files.readString(cachedMeta.toPath(), StandardCharsets.UTF_8));
int installedDatapacks = 0;
int installedAssets = 0;
for (File worldDatapackFolder : worldDatapackFolders) {
if (worldDatapackFolder == null) {
continue;
}
worldDatapackFolder.mkdirs();
String baseManagedName = managedName.endsWith(".zip") ? managedName.substring(0, managedName.length() - 4) : managedName;
deleteFolder(new File(worldDatapackFolder, baseManagedName));
File managedZip = new File(worldDatapackFolder, managedName);
if (managedZip.exists()) {
managedZip.delete();
}
Files.copy(cachedZip.toPath(), managedZip.toPath(), StandardCopyOption.REPLACE_EXISTING);
if (managedZip.exists() && managedZip.length() > 0) {
installedDatapacks++;
installedAssets += meta.optInt("installedAssets", 0);
}
}
Set<String> resolvedLocateStructures = readJsonStringSet(meta, "resolvedLocateStructures");
Set<String> projectedStructureKeys = readJsonStringSet(meta, "projectedStructureKeys");
return ProjectionResult.success(
managedName,
installedDatapacks,
installedAssets,
resolvedLocateStructures,
meta.optInt("syntheticStructureSets", 0),
projectedStructureKeys,
meta.optInt("templateAliasesApplied", 0),
meta.optInt("emptyElementConversions", 0),
meta.optInt("unresolvedTemplateRefs", 0)
);
} catch (Throwable e) {
Iris.verbose("Projection cache miss: " + e.getMessage());
return null;
}
}
private static void cacheProjection(File sourceZip, File cachedZip, File cachedMeta, ProjectionAssetSummary summary) {
try {
File parent = cachedZip.getParentFile();
if (parent != null) {
parent.mkdirs();
}
Files.copy(sourceZip.toPath(), cachedZip.toPath(), StandardCopyOption.REPLACE_EXISTING);
JSONObject meta = new JSONObject();
meta.put("installedAssets", summary.assets().size());
meta.put("syntheticStructureSets", summary.syntheticStructureSets());
meta.put("templateAliasesApplied", summary.templateAliasesApplied());
meta.put("emptyElementConversions", summary.emptyElementConversions());
meta.put("unresolvedTemplateRefs", summary.unresolvedTemplateRefs());
meta.put("resolvedLocateStructures", new JSONArray(summary.resolvedLocateStructures()));
meta.put("projectedStructureKeys", new JSONArray(summary.projectedStructureKeys()));
Files.writeString(cachedMeta.toPath(), meta.toString(2), StandardCharsets.UTF_8);
} catch (Throwable e) {
Iris.verbose("Failed to cache projection: " + e.getMessage());
}
}
private static Set<String> readJsonStringSet(JSONObject json, String key) {
JSONArray array = json.optJSONArray(key);
if (array == null) {
return Set.of();
}
LinkedHashSet<String> result = new LinkedHashSet<>();
for (int i = 0; i < array.length(); i++) {
String value = array.optString(i, "");
if (!value.isBlank()) {
result.add(value);
}
}
return Set.copyOf(result);
}
private static String shortHash(String value) { private static String shortHash(String value) {
try { try {
MessageDigest digest = MessageDigest.getInstance("SHA-1"); MessageDigest digest = MessageDigest.getInstance("SHA-1");
@@ -25,9 +25,7 @@ import art.arcane.volmlib.util.json.JSONException;
import art.arcane.volmlib.util.json.JSONObject; import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.iris.util.common.misc.getHardware; import art.arcane.iris.util.common.misc.getHardware;
import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.plugin.VolmitSender;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -44,7 +42,6 @@ public class IrisSettings {
private IrisSettingsConcurrency concurrency = new IrisSettingsConcurrency(); private IrisSettingsConcurrency concurrency = new IrisSettingsConcurrency();
private IrisSettingsStudio studio = new IrisSettingsStudio(); private IrisSettingsStudio studio = new IrisSettingsStudio();
private IrisSettingsPerformance performance = new IrisSettingsPerformance(); private IrisSettingsPerformance performance = new IrisSettingsPerformance();
private IrisSettingsUpdater updater = new IrisSettingsUpdater();
private IrisSettingsPregen pregen = new IrisSettingsPregen(); private IrisSettingsPregen pregen = new IrisSettingsPregen();
private IrisSettingsSentry sentry = new IrisSettingsSentry(); private IrisSettingsSentry sentry = new IrisSettingsSentry();
@@ -158,7 +155,7 @@ public class IrisSettings {
public int foliaMaxConcurrency = 32; public int foliaMaxConcurrency = 32;
public int chunkLoadTimeoutSeconds = 15; public int chunkLoadTimeoutSeconds = 15;
public int timeoutWarnIntervalMs = 500; public int timeoutWarnIntervalMs = 500;
public int saveIntervalMs = 120_000; public int saveIntervalMs = 30_000;
public boolean enablePregenPerformanceProfile = true; public boolean enablePregenPerformanceProfile = true;
public int pregenProfileNoiseCacheSize = 4_096; public int pregenProfileNoiseCacheSize = 4_096;
public boolean pregenProfileEnableFastCache = true; public boolean pregenProfileEnableFastCache = true;
@@ -212,37 +209,6 @@ public class IrisSettings {
} }
} }
@Data
public static class IrisSettingsUpdater {
public int maxConcurrency = 256;
public boolean nativeThreads = false;
public double threadMultiplier = 2;
public double chunkLoadSensitivity = 0.7;
public MsRange emptyMsRange = new MsRange(80, 100);
public MsRange defaultMsRange = new MsRange(20, 40);
public int getMaxConcurrency() {
return Math.max(Math.abs(maxConcurrency), 1);
}
public double getThreadMultiplier() {
return Math.min(Math.abs(threadMultiplier), 0.1);
}
public double getChunkLoadSensitivity() {
return Math.min(chunkLoadSensitivity, 0.9);
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class MsRange {
public int min = 20;
public int max = 40;
}
@Data @Data
public static class IrisSettingsGeneral { public static class IrisSettingsGeneral {
public boolean commandSounds = true; public boolean commandSounds = true;
@@ -256,6 +222,22 @@ public class IrisSettings {
public boolean adjustVanillaHeight = false; public boolean adjustVanillaHeight = false;
public boolean importExternalDatapacks = true; public boolean importExternalDatapacks = true;
public boolean autoGenerateIntrinsicStructures = true; public boolean autoGenerateIntrinsicStructures = true;
public boolean intrinsicStructureFoundations = true;
public int intrinsicFoundationMaxDepth = 96;
public java.util.List<String> intrinsicStructureAllowlist = new java.util.ArrayList<>(java.util.List.of(
"minecraft:village_plains",
"minecraft:village_desert",
"minecraft:village_savanna",
"minecraft:village_snowy",
"minecraft:village_taiga",
"minecraft:pillager_outpost",
"minecraft:desert_pyramid",
"minecraft:jungle_temple",
"minecraft:swamp_hut",
"minecraft:igloo",
"minecraft:mansion",
"minecraft:ruined_portal*"
));
public String forceMainWorld = ""; public String forceMainWorld = "";
public int spinh = -20; public int spinh = -20;
public int spins = 7; public int spins = 7;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,127 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.pregenerator.LazyPregenerator;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.math.Position2;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.util.Vector;
import java.io.File;
import java.io.IOException;
@Director(name = "lazypregen", aliases = "lazy", description = "Pregenerate your Iris worlds!")
public class CommandLazyPregen implements DirectorExecutor {
public String worldName;
@Director(description = "Pregenerate a world")
public void start(
@Param(description = "The radius of the pregen in blocks", aliases = "size")
int radius,
@Param(description = "The world to pregen", contextual = true)
World world,
@Param(aliases = "middle", description = "The center location of the pregen. Use \"me\" for your current location", defaultValue = "0,0")
Vector center,
@Param(aliases = "maxcpm", description = "Limit the chunks per minute the pregen will generate", defaultValue = "999999999")
int cpm,
@Param(aliases = "silent", description = "Silent generation", defaultValue = "false")
boolean silent
) {
worldName = world.getName();
File worldDirectory = new File(Bukkit.getWorldContainer(), world.getName());
File lazyFile = new File(worldDirectory, "lazygen.json");
if (lazyFile.exists()) {
sender().sendMessage(C.BLUE + "Lazy pregen is already in progress");
Iris.info(C.YELLOW + "Lazy pregen is already in progress");
return;
}
try {
if (sender().isPlayer() && access() == null) {
sender().sendMessage(C.RED + "The engine access for this world is null!");
sender().sendMessage(C.RED + "Please make sure the world is loaded & the engine is initialized. Generate a new chunk, for example.");
}
PlatformChunkGenerator platform = IrisToolbelt.access(world);
if (platform != null) {
IrisToolbelt.applyPregenPerformanceProfile(platform.getEngine());
}
LazyPregenerator.LazyPregenJob pregenJob = LazyPregenerator.LazyPregenJob.builder()
.world(worldName)
.healingPosition(0)
.healing(false)
.chunksPerMinute(cpm)
.radiusBlocks(radius)
.position(0)
.silent(silent)
.build();
File lazyGenFile = new File(worldDirectory, "lazygen.json");
LazyPregenerator pregenerator = new LazyPregenerator(pregenJob, lazyGenFile);
pregenerator.start();
String msg = C.GREEN + "LazyPregen started in " + C.GOLD + worldName + C.GREEN + " of " + C.GOLD + (radius * 2) + C.GREEN + " by " + C.GOLD + (radius * 2) + C.GREEN + " blocks from " + C.GOLD + center.getX() + "," + center.getZ();
sender().sendMessage(msg);
Iris.info(msg);
} catch (Throwable e) {
sender().sendMessage(C.RED + "Epic fail. See console.");
Iris.reportError(e);
e.printStackTrace();
}
}
@Director(description = "Stop the active pregeneration task", aliases = "x")
public void stop(
@Param(aliases = "world", description = "The world to pause")
World world
) throws IOException {
if (LazyPregenerator.getInstance() != null) {
LazyPregenerator.getInstance().shutdownInstance(world);
sender().sendMessage(C.LIGHT_PURPLE + "Closed lazygen instance for " + world.getName());
} else {
sender().sendMessage(C.YELLOW + "No active pregeneration tasks to stop");
}
}
@Director(description = "Pause / continue the active pregeneration task", aliases = {"t", "resume", "unpause"})
public void pause(
@Param(aliases = "world", description = "The world to pause")
World world
) {
if (LazyPregenerator.getInstance() != null) {
LazyPregenerator.getInstance().setPausedLazy(world);
sender().sendMessage(C.GREEN + "Paused/unpaused Lazy Pregen, now: " + (LazyPregenerator.getInstance().isPausedLazy(world) ? "Paused" : "Running") + ".");
} else {
sender().sendMessage(C.YELLOW + "No active Lazy Pregen tasks to pause/unpause.");
}
}
}
@@ -1,186 +0,0 @@
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.runtime.DatapackReadinessResult;
import art.arcane.iris.core.runtime.SmokeDiagnosticsService;
import art.arcane.iris.core.runtime.SmokeTestService;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.volmlib.util.format.Form;
import java.util.List;
@Director(name = "smoke", description = "Run Iris developer smoke diagnostics")
public class CommandSmoke implements DirectorExecutor {
@Director(description = "Run the full smoke suite", sync = true)
public void full(
@Param(name = "dimension", aliases = {"pack"}, description = "The dimension/pack key to validate")
String dimension,
@Param(description = "The seed to use", defaultValue = "1337")
long seed,
@Param(description = "Optional player validation target or none", defaultValue = "none")
String player,
@Param(name = "retain-on-failure", aliases = {"retain"}, description = "Retain the temp world after failure", defaultValue = "false")
boolean retainOnFailure
) {
String runId = SmokeTestService.get().startFullSmoke(sender(), dimension, seed, player, retainOnFailure);
announceRun(runId, "full");
}
@Director(description = "Run the studio smoke flow", sync = true)
public void studio(
@Param(name = "dimension", aliases = {"pack"}, description = "The dimension/pack key to validate")
String dimension,
@Param(description = "The seed to use", defaultValue = "1337")
long seed,
@Param(description = "Optional player validation target or none", defaultValue = "none")
String player,
@Param(name = "retain-on-failure", aliases = {"retain"}, description = "Retain the temp world after failure", defaultValue = "false")
boolean retainOnFailure
) {
String runId = SmokeTestService.get().startStudioSmoke(sender(), dimension, seed, player, retainOnFailure);
announceRun(runId, "studio");
}
@Director(description = "Run the create/unload smoke flow", sync = true)
public void create(
@Param(name = "dimension", aliases = {"pack"}, description = "The dimension/pack key to validate")
String dimension,
@Param(description = "The seed to use", defaultValue = "1337")
long seed,
@Param(name = "retain-on-failure", aliases = {"retain"}, description = "Retain the temp world after failure", defaultValue = "false")
boolean retainOnFailure
) {
String runId = SmokeTestService.get().startCreateSmoke(sender(), dimension, seed, retainOnFailure);
announceRun(runId, "create");
}
@Director(description = "Run the benchmark create/unload smoke flow", sync = true)
public void benchmark(
@Param(name = "dimension", aliases = {"pack"}, description = "The dimension/pack key to validate")
String dimension,
@Param(description = "The seed to use", defaultValue = "1337")
long seed,
@Param(name = "retain-on-failure", aliases = {"retain"}, description = "Retain the temp world after failure", defaultValue = "false")
boolean retainOnFailure
) {
String runId = SmokeTestService.get().startBenchmarkSmoke(sender(), dimension, seed, retainOnFailure);
announceRun(runId, "benchmark");
}
@Director(description = "Show live or persisted smoke status", sync = true)
public void status(
@Param(description = "Use latest or a specific run id", defaultValue = "latest")
String run
) {
SmokeDiagnosticsService.SmokeRunReport report = resolveReport(run);
if (report == null) {
sender().sendMessage(C.RED + "No smoke report found for \"" + run + "\".");
return;
}
sendReport(report);
}
@Director(description = "Inspect a currently loaded smoke/studio world", sync = true)
public void inspect(
@Param(description = "The loaded world name to inspect")
String world
) {
SmokeTestService.WorldInspection inspection = SmokeTestService.get().inspectWorld(world);
if (inspection == null) {
sender().sendMessage(C.RED + "World \"" + world + "\" is not currently loaded.");
return;
}
sender().sendMessage(C.GREEN + "Smoke inspection for " + C.GOLD + inspection.worldName());
sender().sendMessage(C.GRAY + "Lifecycle backend: " + C.WHITE + inspection.lifecycleBackend());
sender().sendMessage(C.GRAY + "Runtime backend: " + C.WHITE + inspection.runtimeBackend());
sender().sendMessage(C.GRAY + "Studio: " + C.WHITE + inspection.studio() + C.GRAY + " | Maintenance active: " + C.WHITE + inspection.maintenanceActive());
sender().sendMessage(C.GRAY + "Engine closed: " + C.WHITE + inspection.engineClosed() + C.GRAY + " | Engine failing: " + C.WHITE + inspection.engineFailing());
sender().sendMessage(C.GRAY + "Generation session: " + C.WHITE + inspection.generationSessionId() + C.GRAY + " | Active leases: " + C.WHITE + inspection.activeLeaseCount());
sender().sendMessage(C.GRAY + "Datapack folders: " + C.WHITE + joinList(inspection.datapackFolders()));
}
private void announceRun(String runId, String mode) {
sender().sendMessage(C.GREEN + "Started " + C.GOLD + mode + C.GREEN + " smoke run " + C.GOLD + runId + C.GREEN + ".");
sender().sendMessage(C.GREEN + "Use " + C.GOLD + "/iris developer smoke status run=" + runId + C.GREEN + " to monitor progress.");
sender().sendMessage(C.GREEN + "Latest report: " + C.GOLD + latestReportPath());
}
private SmokeDiagnosticsService.SmokeRunReport resolveReport(String run) {
if (run == null || run.isBlank() || run.equalsIgnoreCase("latest")) {
return SmokeTestService.get().latest();
}
return SmokeTestService.get().get(run);
}
private void sendReport(SmokeDiagnosticsService.SmokeRunReport report) {
String elapsed = Form.duration(Math.max(0L, report.getElapsedMs()), 0);
sender().sendMessage(C.GREEN + "Smoke run " + C.GOLD + report.getRunId() + C.GREEN + " (" + C.GOLD + report.getMode() + C.GREEN + ")");
sender().sendMessage(C.GRAY + "World: " + C.WHITE + fallback(report.getWorldName()) + C.GRAY + " | Outcome: " + C.WHITE + fallback(report.getOutcome()));
sender().sendMessage(C.GRAY + "Stage: " + C.WHITE + fallback(report.getStage()) + C.GRAY + " | Elapsed: " + C.WHITE + elapsed);
if (report.getStageDetail() != null && !report.getStageDetail().isBlank()) {
sender().sendMessage(C.GRAY + "Stage detail: " + C.WHITE + report.getStageDetail());
}
sender().sendMessage(C.GRAY + "Lifecycle backend: " + C.WHITE + fallback(report.getLifecycleBackend()));
sender().sendMessage(C.GRAY + "Runtime backend: " + C.WHITE + fallback(report.getRuntimeBackend()));
sender().sendMessage(C.GRAY + "Generation session: " + C.WHITE + report.getGenerationSessionId() + C.GRAY + " | Active leases: " + C.WHITE + report.getGenerationActiveLeases());
if (report.getEntryChunkX() != null && report.getEntryChunkZ() != null) {
sender().sendMessage(C.GRAY + "Entry chunk: " + C.WHITE + report.getEntryChunkX() + "," + report.getEntryChunkZ());
}
sender().sendMessage(C.GRAY + "Headless: " + C.WHITE + report.isHeadless() + C.GRAY + " | Player: " + C.WHITE + fallback(report.getPlayerName()));
sender().sendMessage(C.GRAY + "Retain on failure: " + C.WHITE + report.isRetainOnFailure() + C.GRAY + " | Cleanup applied: " + C.WHITE + report.isCleanupApplied());
sendDatapackReadiness(report.getDatapackReadiness());
if (!report.getNotes().isEmpty()) {
sender().sendMessage(C.GRAY + "Notes: " + C.WHITE + joinList(report.getNotes()));
}
if (report.getFailureType() != null && !report.getFailureType().isBlank()) {
sender().sendMessage(C.RED + "Failure: " + report.getFailureType() + C.GRAY + " - " + C.WHITE + fallback(report.getFailureMessage()));
if (!report.getFailureChain().isEmpty()) {
sender().sendMessage(C.RED + "Failure chain: " + C.WHITE + joinList(report.getFailureChain()));
}
}
}
private void sendDatapackReadiness(DatapackReadinessResult readiness) {
if (readiness == null) {
return;
}
sender().sendMessage(C.GRAY + "Datapack pack key: " + C.WHITE + fallback(readiness.getRequestedPackKey()));
sender().sendMessage(C.GRAY + "Datapack folders: " + C.WHITE + joinList(readiness.getResolvedDatapackFolders()));
sender().sendMessage(C.GRAY + "External datapack result: " + C.WHITE + fallback(readiness.getExternalDatapackInstallResult()));
sender().sendMessage(C.GRAY + "Verification passed: " + C.WHITE + readiness.isVerificationPassed() + C.GRAY + " | Restart required: " + C.WHITE + readiness.isRestartRequired());
if (!readiness.getMissingPaths().isEmpty()) {
sender().sendMessage(C.RED + "Missing datapack paths: " + C.WHITE + joinList(readiness.getMissingPaths()));
}
}
private String latestReportPath() {
if (Iris.instance == null) {
return "plugins/Iris/diagnostics/smoke/latest.json";
}
return Iris.instance.getDataFile("diagnostics", "smoke", "latest.json").getAbsolutePath();
}
private String joinList(List<String> values) {
if (values == null || values.isEmpty()) {
return "none";
}
return String.join(", ", values);
}
private String fallback(String value) {
if (value == null || value.isBlank()) {
return "none";
}
return value;
}
}
@@ -186,113 +186,6 @@ public class CommandStudio implements DirectorExecutor {
sender().sendMessage(C.GREEN + "The \"" + dimension.getName() + "\" pack has version: " + dimension.getVersion()); sender().sendMessage(C.GREEN + "The \"" + dimension.getName() + "\" pack has version: " + dimension.getVersion());
} }
@Director(name = "regen", description = "Regenerate nearby chunks.", aliases = "rg", sync = true, origin = DirectorOrigin.PLAYER)
public void regen(
@Param(name = "radius", description = "The radius of nearby cunks", defaultValue = "5")
int radius
) {
World world = player().getWorld();
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(C.RED + "You must be in an Iris World to use regen!");
}
VolmitSender sender = sender();
var loc = player().getLocation().clone();
final int threadCount = J.isFolia() ? 1 : Runtime.getRuntime().availableProcessors();
String orchestratorName = "Iris-Studio-Regen-Orchestrator-" + world.getName() + "-" + System.nanoTime();
Thread orchestrator = new Thread(() -> {
PlatformChunkGenerator plat = IrisToolbelt.access(world);
Engine engine = plat.getEngine();
DirectorContext.touch(sender);
IrisToolbelt.beginWorldMaintenance(world, "studio-regen");
try (SyncExecutor executor = new SyncExecutor(20);
var service = Executors.newFixedThreadPool(threadCount)
) {
int x = loc.getBlockX() >> 4;
int z = loc.getBlockZ() >> 4;
int rad = 0;
var chunkMap = new KMap<Position2, MantleChunk>();
boolean foliaFastRegen = J.isFolia();
if (foliaFastRegen) {
sender.sendMessage(C.YELLOW + "Folia safe default: using 1 regen worker in studio.");
}
if (!foliaFastRegen) {
rad = engine.getMantle().getRadius();
final var mantle = engine.getMantle().getMantle();
ParallelRadiusJob prep = new ParallelRadiusJob(threadCount, service) {
@Override
protected void execute(int rX, int rZ) {
if (Math.abs(rX) <= radius && Math.abs(rZ) <= radius) {
mantle.deleteChunk(rX + x, rZ + z);
return;
}
rX += x;
rZ += z;
chunkMap.put(new Position2(rX, rZ), mantle.getChunk(rX, rZ));
mantle.deleteChunk(rX, rZ);
}
@Override
public String getName() {
return "Preparing Mantle";
}
}.retarget(radius + rad, 0, 0);
sender.sendMessage(C.YELLOW + "Preparing mantle data for studio regen...");
prep.execute();
} else {
sender.sendMessage(C.YELLOW + "Folia fast regen: skipping outer mantle preservation stage.");
}
final String runId = "studio-regen-" + world.getName() + "-" + System.currentTimeMillis();
ParallelRadiusJob job = new ParallelRadiusJob(threadCount, service) {
@Override
protected void execute(int x, int z) {
if (foliaFastRegen) {
Iris.verbose("Folia fast studio regen skipping mantle delete for " + x + "," + z + ".");
}
plat.injectChunkReplacement(
world,
x,
z,
executor,
ChunkReplacementOptions.terrain(runId, IrisSettings.get().getGeneral().isDebug()),
ChunkReplacementListener.NO_OP
);
}
@Override
public String getName() {
return "Regenerating";
}
}.retarget(radius, x, z);
job.execute();
if (!foliaFastRegen) {
var mantle = engine.getMantle().getMantle();
chunkMap.forEach((pos, chunk) ->
mantle.getChunk(pos.getX(), pos.getZ()).copyFrom(chunk));
}
} catch (Throwable e) {
sender().sendMessage("Error while regenerating chunks");
e.printStackTrace();
} finally {
IrisToolbelt.endWorldMaintenance(world, "studio-regen");
DirectorContext.remove();
}
}, orchestratorName);
orchestrator.setDaemon(true);
try {
orchestrator.start();
Iris.info("Studio regen worker dispatched on dedicated thread=" + orchestratorName + ".");
} catch (Throwable e) {
sender.sendMessage(C.RED + "Failed to start studio regen worker thread. See console.");
Iris.reportError(e);
}
}
@Director(description = "Open the noise explorer (External GUI)", aliases = {"nmap", "n"}) @Director(description = "Open the noise explorer (External GUI)", aliases = {"nmap", "n"})
public void noise() { public void noise() {
if (noGUI()) return; if (noGUI()) return;
@@ -331,16 +224,6 @@ public class CommandStudio implements DirectorExecutor {
NoiseExplorerGUI.launch(l, "Custom Generator"); NoiseExplorerGUI.launch(l, "Custom Generator");
} }
@Director(description = "Hotload a studio", aliases = {"reload", "h"})
public void hotload() {
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
sender().sendMessage(C.RED + "No studio world open!");
return;
}
Iris.service(StudioSVC.class).getActiveProject().getActiveProvider().getEngine().hotload();
sender().sendMessage(C.GREEN + "Hotloaded");
}
@Director(description = "Show loot if a chest were right here", origin = DirectorOrigin.PLAYER, sync = true) @Director(description = "Show loot if a chest were right here", origin = DirectorOrigin.PLAYER, sync = true)
public void loot( public void loot(
@Param(description = "Fast insertion of items in virtual inventory (may cause performance drop)", defaultValue = "false") @Param(description = "Fast insertion of items in virtual inventory (may cause performance drop)", defaultValue = "false")
@@ -1,137 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.pregenerator.LazyPregenerator;
import art.arcane.iris.core.pregenerator.TurboPregenerator;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.format.C;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.util.Vector;
import java.io.File;
import java.io.IOException;
@Director(name = "turbopregen", aliases = "turbo", description = "Pregenerate your Iris worlds!")
public class CommandTurboPregen implements DirectorExecutor {
public String worldName;
@Director(description = "Pregenerate a world")
public void start(
@Param(description = "The radius of the pregen in blocks", aliases = "size")
int radius,
@Param(description = "The world to pregen", contextual = true)
World world,
@Param(aliases = "middle", description = "The center location of the pregen. Use \"me\" for your current location", defaultValue = "0,0")
Vector center
) {
worldName = world.getName();
File worldDirectory = new File(Bukkit.getWorldContainer(), world.getName());
File TurboFile = new File(worldDirectory, "turbogen.json");
if (TurboFile.exists()) {
if (TurboPregenerator.getInstance() != null) {
sender().sendMessage(C.BLUE + "Turbo pregen is already in progress");
Iris.info(C.YELLOW + "Turbo pregen is already in progress");
return;
} else {
try {
TurboFile.delete();
} catch (Exception e){
Iris.error("Failed to delete the old instance file of Turbo Pregen!");
return;
}
}
}
try {
if (sender().isPlayer() && access() == null) {
sender().sendMessage(C.RED + "The engine access for this world is null!");
sender().sendMessage(C.RED + "Please make sure the world is loaded & the engine is initialized. Generate a new chunk, for example.");
}
PlatformChunkGenerator platform = IrisToolbelt.access(world);
if (platform != null) {
IrisToolbelt.applyPregenPerformanceProfile(platform.getEngine());
}
TurboPregenerator.TurboPregenJob pregenJob = TurboPregenerator.TurboPregenJob.builder()
.world(worldName)
.radiusBlocks(radius)
.position(0)
.build();
File TurboGenFile = new File(worldDirectory, "turbogen.json");
TurboPregenerator pregenerator = new TurboPregenerator(pregenJob, TurboGenFile);
pregenerator.start();
String msg = C.GREEN + "TurboPregen started in " + C.GOLD + worldName + C.GREEN + " of " + C.GOLD + (radius * 2) + C.GREEN + " by " + C.GOLD + (radius * 2) + C.GREEN + " blocks from " + C.GOLD + center.getX() + "," + center.getZ();
sender().sendMessage(msg);
Iris.info(msg);
} catch (Throwable e) {
sender().sendMessage(C.RED + "Epic fail. See console.");
Iris.reportError(e);
e.printStackTrace();
}
}
@Director(description = "Stop the active pregeneration task", aliases = "x")
public void stop(@Param(aliases = "world", description = "The world to pause") World world) throws IOException {
TurboPregenerator turboPregenInstance = TurboPregenerator.getInstance();
File worldDirectory = new File(Bukkit.getWorldContainer(), world.getName());
File turboFile = new File(worldDirectory, "turbogen.json");
if (turboPregenInstance != null) {
turboPregenInstance.shutdownInstance(world);
sender().sendMessage(C.LIGHT_PURPLE + "Closed Turbogen instance for " + world.getName());
} else if (turboFile.exists() && turboFile.delete()) {
sender().sendMessage(C.LIGHT_PURPLE + "Closed Turbogen instance for " + world.getName());
} else if (turboFile.exists()) {
Iris.error("Failed to delete the old instance file of Turbo Pregen!");
} else {
sender().sendMessage(C.YELLOW + "No active pregeneration tasks to stop");
}
}
@Director(description = "Pause / continue the active pregeneration task", aliases = {"t", "resume", "unpause"})
public void pause(
@Param(aliases = "world", description = "The world to pause")
World world
) {
if (TurboPregenerator.getInstance() != null) {
TurboPregenerator.setPausedTurbo(world);
sender().sendMessage(C.GREEN + "Paused/unpaused Turbo Pregen, now: " + (TurboPregenerator.isPausedTurbo(world) ? "Paused" : "Running") + ".");
} else {
File worldDirectory = new File(Bukkit.getWorldContainer(), world.getName());
File TurboFile = new File(worldDirectory, "turbogen.json");
if (TurboFile.exists()){
TurboPregenerator.loadTurboGenerator(world.getName());
sender().sendMessage(C.YELLOW + "Started Turbo Pregen back up!");
} else {
sender().sendMessage(C.YELLOW + "No active Turbo Pregen tasks to pause/unpause.");
}
}
}
}
@@ -1,102 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import lombok.Synchronized;
import org.bukkit.World;
import art.arcane.iris.Iris;
import art.arcane.iris.core.pregenerator.ChunkUpdater;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form;
@Director(name = "updater", origin = DirectorOrigin.BOTH, description = "Iris World Updater")
public class CommandUpdater implements DirectorExecutor {
private final Object lock = new Object();
private transient ChunkUpdater chunkUpdater;
@Director(description = "Updates all chunk in the specified world")
public void start(
@Param(description = "World to update chunks at", contextual = true)
World world
) {
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(C.GOLD + "This is not an Iris world");
return;
}
synchronized (lock) {
if (chunkUpdater != null) {
chunkUpdater.stop();
}
chunkUpdater = new ChunkUpdater(world);
if (sender().isPlayer()) {
sender().sendMessage(C.GREEN + "Updating " + world.getName() + C.GRAY + " Total chunks: " + Form.f(chunkUpdater.getChunks()));
} else {
Iris.info(C.GREEN + "Updating " + world.getName() + C.GRAY + " Total chunks: " + Form.f(chunkUpdater.getChunks()));
}
chunkUpdater.start();
}
}
@Synchronized("lock")
@Director(description = "Pause the updater")
public void pause( ) {
if (chunkUpdater == null) {
sender().sendMessage(C.GOLD + "You cant pause something that doesnt exist?");
return;
}
boolean status = chunkUpdater.pause();
if (sender().isPlayer()) {
if (status) {
sender().sendMessage(C.IRIS + "Paused task for: " + C.GRAY + chunkUpdater.getName());
} else {
sender().sendMessage(C.IRIS + "Unpause task for: " + C.GRAY + chunkUpdater.getName());
}
} else {
if (status) {
Iris.info(C.IRIS + "Paused task for: " + C.GRAY + chunkUpdater.getName());
} else {
Iris.info(C.IRIS + "Unpause task for: " + C.GRAY + chunkUpdater.getName());
}
}
}
@Synchronized("lock")
@Director(description = "Stops the updater")
public void stop() {
if (chunkUpdater == null) {
sender().sendMessage(C.GOLD + "You cant stop something that doesnt exist?");
return;
}
if (sender().isPlayer()) {
sender().sendMessage("Stopping Updater for: " + C.GRAY + chunkUpdater.getName());
} else {
Iris.info("Stopping Updater for: " + C.GRAY + chunkUpdater.getName());
}
chunkUpdater.stop();
}
}
@@ -21,6 +21,7 @@ package art.arcane.iris.core.edit;
import art.arcane.iris.Iris; import art.arcane.iris.Iris;
import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.BlockPosition; import art.arcane.volmlib.util.math.BlockPosition;
import art.arcane.volmlib.util.math.M; import art.arcane.volmlib.util.math.M;
@@ -103,7 +104,11 @@ public class DustRevealer {
public static void spawn(Block block, VolmitSender sender) { public static void spawn(Block block, VolmitSender sender) {
World world = block.getWorld(); World world = block.getWorld();
Engine access = IrisToolbelt.access(world).getEngine(); PlatformChunkGenerator generator = IrisToolbelt.access(world);
if (generator == null) {
return;
}
Engine access = generator.getEngine();
if (access != null) { if (access != null) {
String a = access.getObjectPlacementKey(block.getX(), block.getY() - block.getWorld().getMinHeight(), block.getZ()); String a = access.getObjectPlacementKey(block.getX(), block.getY() - block.getWorld().getMinHeight(), block.getZ());
@@ -1,324 +0,0 @@
package art.arcane.iris.core.pregenerator;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.service.PreservationSVC;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.util.project.profile.LoadBalancer;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.math.RollingSequence;
import art.arcane.iris.util.common.plugin.chunk.TicketHolder;
import art.arcane.iris.util.common.scheduling.J;
import io.papermc.lib.PaperLib;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.World;
import java.io.File;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
public class ChunkUpdater {
private static final String REGION_PATH = "region" + File.separator + "r.";
private final AtomicBoolean paused = new AtomicBoolean();
private final AtomicBoolean cancelled = new AtomicBoolean();
private final TicketHolder holder;
private final RollingSequence chunksPerSecond = new RollingSequence(5);
private final AtomicInteger totalMaxChunks = new AtomicInteger();
private final AtomicInteger chunksProcessed = new AtomicInteger();
private final AtomicInteger chunksProcessedLast = new AtomicInteger();
private final AtomicInteger chunksUpdated = new AtomicInteger();
private final AtomicBoolean serverEmpty = new AtomicBoolean(true);
private final AtomicLong lastCpsTime = new AtomicLong(M.ms());
private final int maxConcurrency = IrisSettings.get().getUpdater().getMaxConcurrency();
private final int coreLimit = (int) Math.max(Runtime.getRuntime().availableProcessors() * IrisSettings.get().getUpdater().getThreadMultiplier(), 1);
private final Semaphore semaphore = new Semaphore(maxConcurrency);
private final LoadBalancer loadBalancer = new LoadBalancer(semaphore, maxConcurrency, IrisSettings.get().getUpdater().emptyMsRange);
private final AtomicLong startTime = new AtomicLong();
private final Dimensions dimensions;
private final PregenTask task;
private final ExecutorService chunkExecutor = IrisSettings.get().getUpdater().isNativeThreads() ? Executors.newFixedThreadPool(coreLimit) : Executors.newVirtualThreadPerTaskExecutor();
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private final CountDownLatch latch;
private final Engine engine;
private final World world;
public ChunkUpdater(World world) {
this.engine = IrisToolbelt.access(world).getEngine();
this.world = world;
this.holder = Iris.tickets.getHolder(world);
this.dimensions = calculateWorldDimensions(new File(world.getWorldFolder(), "region"));
this.task = dimensions.task();
this.totalMaxChunks.set(dimensions.count * 1024);
this.latch = new CountDownLatch(totalMaxChunks.get());
}
public String getName() {
return world.getName();
}
public int getChunks() {
return totalMaxChunks.get();
}
public void start() {
unloadAndSaveAllChunks();
update();
}
public boolean pause() {
unloadAndSaveAllChunks();
if (paused.get()) {
paused.set(false);
return false;
} else {
paused.set(true);
return true;
}
}
public void stop() {
unloadAndSaveAllChunks();
cancelled.set(true);
}
private void update() {
Iris.info("Updating..");
try {
startTime.set(System.currentTimeMillis());
scheduler.scheduleAtFixedRate(() -> {
try {
if (!paused.get()) {
long eta = computeETA();
int processed = chunksProcessed.get();
double last = processed - chunksProcessedLast.getAndSet(processed);
double cps = last / ((M.ms() - lastCpsTime.getAndSet(M.ms())) / 1000d);
chunksPerSecond.put(cps);
double percentage = ((double) processed / (double) totalMaxChunks.get()) * 100;
if (!cancelled.get()) {
Iris.info("Updated: " + Form.f(processed) + " of " + Form.f(totalMaxChunks.get()) + " (%.0f%%) " + Form.f(chunksPerSecond.getAverage()) + "/s, ETA: " + Form.duration(eta,
2), percentage);
}
}
} catch (Exception e) {
Iris.reportError(e);
e.printStackTrace();
}
}, 0, 3, TimeUnit.SECONDS);
scheduler.scheduleAtFixedRate(() -> {
boolean empty = Bukkit.getOnlinePlayers().isEmpty();
if (serverEmpty.getAndSet(empty) == empty)
return;
loadBalancer.setRange(empty ? IrisSettings.get().getUpdater().emptyMsRange : IrisSettings.get().getUpdater().defaultMsRange);
}, 0, 10, TimeUnit.SECONDS);
var t = new Thread(() -> {
run();
close();
}, "Iris Chunk Updater - " + world.getName());
t.setPriority(Thread.MAX_PRIORITY);
t.start();
Iris.service(PreservationSVC.class).register(t);
} catch (Exception e) {
e.printStackTrace();
}
}
public void close() {
try {
loadBalancer.close();
semaphore.acquire(256);
chunkExecutor.shutdown();
chunkExecutor.awaitTermination(5, TimeUnit.SECONDS);
scheduler.shutdownNow();
unloadAndSaveAllChunks();
} catch (Exception ignored) {}
if (cancelled.get()) {
Iris.info("Updated: " + Form.f(chunksUpdated.get()) + " Chunks");
Iris.info("Irritated: " + Form.f(chunksProcessed.get()) + " of " + Form.f(totalMaxChunks.get()));
Iris.info("Stopped updater.");
} else {
Iris.info("Processed: " + Form.f(chunksProcessed.get()) + " Chunks");
Iris.info("Finished Updating: " + Form.f(chunksUpdated.get()) + " Chunks");
}
}
private void run() {
task.iterateRegions((rX, rZ) -> {
if (cancelled.get())
return;
while (paused.get()) {
J.sleep(50);
}
if (rX < dimensions.min.getX() ||
rX > dimensions.max.getX() ||
rZ < dimensions.min.getZ() ||
rZ > dimensions.max.getZ() ||
!new File(world.getWorldFolder(), REGION_PATH + rX + "." + rZ + ".mca").exists()
) return;
task.iterateChunks(rX, rZ, (x, z) -> {
while (paused.get() && !cancelled.get()) {
J.sleep(50);
}
try {
semaphore.acquire();
} catch (InterruptedException ignored) {
return;
}
chunkExecutor.submit(() -> {
try {
if (!cancelled.get())
processChunk(x, z);
} finally {
latch.countDown();
semaphore.release();
}
});
});
});
}
private void processChunk(int x, int z) {
if (!loadChunksIfGenerated(x, z)) {
chunksProcessed.getAndIncrement();
return;
}
var mc = engine.getMantle().getMantle().getChunk(x, z).use();
try {
Chunk c = world.getChunkAt(x, z);
engine.updateChunk(c);
removeTickets(x, z);
} finally {
chunksUpdated.incrementAndGet();
chunksProcessed.getAndIncrement();
mc.release();
}
}
private boolean loadChunksIfGenerated(int x, int z) {
if (engine.getMantle().getMantle().hasFlag(x, z, MantleFlag.ETCHED))
return false;
for (int dx = -1; dx <= 1; dx++) {
for (int dz = -1; dz <= 1; dz++) {
if (!PaperLib.isChunkGenerated(world, x + dx, z + dz)) {
return false;
}
}
}
AtomicBoolean generated = new AtomicBoolean(true);
CountDownLatch latch = new CountDownLatch(9);
for (int dx = -1; dx <= 1; dx++) {
for (int dz = -1; dz <= 1; dz++) {
int xx = x + dx;
int zz = z + dz;
PaperLib.getChunkAtAsync(world, xx, zz, false, true)
.thenAccept(chunk -> {
if (chunk == null || !chunk.isGenerated()) {
latch.countDown();
generated.set(false);
return;
}
holder.addTicket(chunk);
latch.countDown();
});
}
}
try {
latch.await();
} catch (InterruptedException e) {
Iris.info("Interrupted while waiting for chunks to load");
}
if (generated.get()) return true;
removeTickets(x, z);
return false;
}
private void removeTickets(int x, int z) {
for (int xx = -1; xx <= 1; xx++) {
for (int zz = -1; zz <= 1; zz++) {
holder.removeTicket(x + xx, z + zz);
}
}
}
private void unloadAndSaveAllChunks() {
if (J.isFolia()) {
return;
}
try {
J.sfut(() -> {
if (world == null) {
Iris.warn("World was null somehow...");
return;
}
world.save();
}).get();
} catch (Throwable e) {
Iris.reportError(e);
e.printStackTrace();
}
}
private long computeETA() {
return (long) (totalMaxChunks.get() > 1024 ? // Generated chunks exceed 1/8th of total?
// If yes, use smooth function (which gets more accurate over time since its less sensitive to outliers)
((totalMaxChunks.get() - chunksProcessed.get()) * ((double) (M.ms() - startTime.get()) / (double) chunksProcessed.get())) :
// If no, use quick function (which is less accurate over time but responds better to the initial delay)
((totalMaxChunks.get() - chunksProcessed.get()) / chunksPerSecond.getAverage()) * 1000
);
}
private Dimensions calculateWorldDimensions(File regionDir) {
File[] files = regionDir.listFiles((dir, name) -> name.endsWith(".mca"));
int minX = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int minZ = Integer.MAX_VALUE;
int maxZ = Integer.MIN_VALUE;
for (File file : files) {
String[] parts = file.getName().split("\\.");
int x = Integer.parseInt(parts[1]);
int z = Integer.parseInt(parts[2]);
minX = Math.min(minX, x);
maxX = Math.max(maxX, x);
minZ = Math.min(minZ, z);
maxZ = Math.max(maxZ, z);
}
int oX = minX + ((maxX - minX) / 2);
int oZ = minZ + ((maxZ - minZ) / 2);
int height = maxX - minX + 1;
int width = maxZ - minZ + 1;
return new Dimensions(new Position2(minX, minZ), new Position2(maxX, maxZ), height * width, PregenTask.builder()
.radiusZ((int) Math.ceil(width / 2d * 512))
.radiusX((int) Math.ceil(height / 2d * 512))
.center(new Position2(oX, oZ))
.build());
}
private record Dimensions(Position2 min, Position2 max, int count, PregenTask task) { }
}
@@ -1,295 +0,0 @@
package art.arcane.iris.core.pregenerator;
import com.google.gson.Gson;
import art.arcane.iris.Iris;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.math.RollingSequence;
import art.arcane.volmlib.util.math.Spiraler;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.iris.util.common.scheduling.J;
import io.papermc.lib.PaperLib;
import lombok.Data;
import lombok.Getter;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.world.WorldUnloadEvent;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.HashMap;
import java.util.Map;
public class LazyPregenerator extends Thread implements Listener {
@Getter
private static LazyPregenerator instance;
private final LazyPregenJob job;
private final File destination;
private final int maxPosition;
private World world;
private final long rate;
private final ChronoLatch latch;
private static AtomicInteger lazyGeneratedChunks;
private final AtomicInteger generatedLast;
private final AtomicInteger lazyTotalChunks;
private final AtomicLong startTime;
private final RollingSequence chunksPerSecond;
private final RollingSequence chunksPerMinute;
private static final Map<String, LazyPregenJob> jobs = new HashMap<>();
public LazyPregenerator(LazyPregenJob job, File destination) {
this.job = job;
this.destination = destination;
this.maxPosition = new Spiraler(job.getRadiusBlocks() * 2, job.getRadiusBlocks() * 2, (x, z) -> {
}).count();
this.world = Bukkit.getWorld(job.getWorld());
this.rate = Math.round((1D / (job.getChunksPerMinute() / 60D)) * 1000D);
this.latch = new ChronoLatch(15000);
this.startTime = new AtomicLong(M.ms());
this.chunksPerSecond = new RollingSequence(10);
this.chunksPerMinute = new RollingSequence(10);
lazyGeneratedChunks = new AtomicInteger(0);
this.generatedLast = new AtomicInteger(0);
this.lazyTotalChunks = new AtomicInteger((int) Math.ceil(Math.pow((2.0 * job.getRadiusBlocks()) / 16, 2)));
jobs.put(job.getWorld(), job);
LazyPregenerator.instance = this;
}
public LazyPregenerator(File file) throws IOException {
this(new Gson().fromJson(IO.readAll(file), LazyPregenJob.class), file);
}
public static void loadLazyGenerators() {
for (World i : Bukkit.getWorlds()) {
File lazygen = new File(i.getWorldFolder(), "lazygen.json");
if (lazygen.exists()) {
try {
LazyPregenerator p = new LazyPregenerator(lazygen);
p.start();
Iris.info("Started Lazy Pregenerator: " + p.job);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
@EventHandler
public void on(WorldUnloadEvent e) {
if (e.getWorld().equals(world)) {
interrupt();
}
}
public void run() {
while (!interrupted()) {
J.sleep(rate);
tick();
}
try {
saveNow();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void tick() {
LazyPregenJob job = jobs.get(world.getName());
if (latch.flip() && !job.paused) {
long eta = computeETA();
save();
int secondGenerated = lazyGeneratedChunks.get() - generatedLast.get();
generatedLast.set(lazyGeneratedChunks.get());
secondGenerated = secondGenerated / 15;
chunksPerSecond.put(secondGenerated);
chunksPerMinute.put(secondGenerated * 60);
if (!job.isSilent()) {
Iris.info("LazyGen: " + C.IRIS + world.getName() + C.RESET + " RTT: " + Form.f(lazyGeneratedChunks.get()) + " of " + Form.f(lazyTotalChunks.get()) + " " + Form.f((int) chunksPerMinute.getAverage()) + "/m ETA: " + Form.duration((double) eta, 2));
}
}
if (lazyGeneratedChunks.get() >= lazyTotalChunks.get()) {
if (job.isHealing()) {
Iris.warn("LazyGen healing mode is not supported on 1.21.11; ending lazy generation for " + world.getName() + ".");
job.setHealing(false);
}
Iris.info("Completed Lazy Gen!");
interrupt();
} else {
int pos = job.getPosition() + 1;
job.setPosition(pos);
if (!job.paused) {
tickGenerate(getChunk(pos));
}
}
}
private long computeETA() {
return (long) ((lazyTotalChunks.get() - lazyGeneratedChunks.get()) / chunksPerMinute.getAverage()) * 1000;
// todo broken
}
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
private void tickGenerate(Position2 chunk) {
executorService.submit(() -> {
CountDownLatch latch = new CountDownLatch(1);
if (PaperLib.isPaper()) {
PaperLib.getChunkAtAsync(world, chunk.getX(), chunk.getZ(), true)
.thenAccept((i) -> {
Iris.verbose("Generated Async " + chunk);
latch.countDown();
});
} else {
J.s(() -> {
world.getChunkAt(chunk.getX(), chunk.getZ());
Iris.verbose("Generated " + chunk);
latch.countDown();
});
}
try {
latch.await();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
Iris.verbose("Lazy pregenerator worker interrupted while waiting for chunk " + chunk + ".");
}
lazyGeneratedChunks.addAndGet(1);
});
}
public Position2 getChunk(int position) {
int p = -1;
AtomicInteger xx = new AtomicInteger();
AtomicInteger zz = new AtomicInteger();
Spiraler s = new Spiraler(job.getRadiusBlocks() * 2, job.getRadiusBlocks() * 2, (x, z) -> {
xx.set(x);
zz.set(z);
});
while (s.hasNext() && p++ < position) {
s.next();
}
return new Position2(xx.get(), zz.get());
}
public void save() {
J.a(() -> {
try {
saveNow();
} catch (Throwable e) {
e.printStackTrace();
}
});
}
public static void setPausedLazy(World world) {
LazyPregenJob job = jobs.get(world.getName());
if (isPausedLazy(world)){
job.paused = false;
} else {
job.paused = true;
}
if ( job.paused) {
Iris.info(C.BLUE + "LazyGen: " + C.IRIS + world.getName() + C.BLUE + " Paused");
} else {
Iris.info(C.BLUE + "LazyGen: " + C.IRIS + world.getName() + C.BLUE + " Resumed");
}
}
public static boolean isPausedLazy(World world) {
LazyPregenJob job = jobs.get(world.getName());
return job != null && job.isPaused();
}
public static long remainingChunks() {
LazyPregenerator local = instance;
AtomicInteger generated = lazyGeneratedChunks;
if (local == null || generated == null) {
return -1L;
}
return Math.max(0L, local.lazyTotalChunks.get() - generated.get());
}
public static double chunksPerSecond() {
LazyPregenerator local = instance;
if (local == null) {
return 0D;
}
return Math.max(0D, local.chunksPerMinute.getAverage() / 60D);
}
public void shutdownInstance(World world) throws IOException {
Iris.info("LazyGen: " + C.IRIS + world.getName() + C.BLUE + " Shutting down..");
LazyPregenJob job = jobs.get(world.getName());
File worldDirectory = new File(Bukkit.getWorldContainer(), world.getName());
File lazyFile = new File(worldDirectory, "lazygen.json");
if (job == null) {
Iris.error("No Lazygen job found for world: " + world.getName());
return;
}
try {
if (!job.isPaused()) {
job.setPaused(true);
}
save();
jobs.remove(world.getName());
J.a(() -> {
while (lazyFile.exists()) {
lazyFile.delete();
J.sleep(1000);
}
Iris.info("LazyGen: " + C.IRIS + world.getName() + C.BLUE + " File deleted and instance closed.");
}, 20);
} catch (Exception e) {
Iris.error("Failed to shutdown Lazygen for " + world.getName());
e.printStackTrace();
} finally {
saveNow();
interrupt();
}
}
public void saveNow() throws IOException {
IO.writeAll(this.destination, new Gson().toJson(job));
}
@Data
@lombok.Builder
public static class LazyPregenJob {
private String world;
@lombok.Builder.Default
private int healingPosition = 0;
@lombok.Builder.Default
private boolean healing = false;
@lombok.Builder.Default
private int chunksPerMinute = 32;
@lombok.Builder.Default
private int radiusBlocks = 5000;
@lombok.Builder.Default
private int position = 0;
@lombok.Builder.Default
boolean silent = false;
@lombok.Builder.Default
boolean paused = false;
}
}
@@ -1,357 +0,0 @@
package art.arcane.iris.core.pregenerator;
import com.google.gson.Gson;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.math.RollingSequence;
import art.arcane.volmlib.util.math.Spiraler;
import art.arcane.iris.util.common.parallel.BurstExecutor;
import art.arcane.iris.util.common.parallel.HyperLock;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import io.papermc.lib.PaperLib;
import lombok.Data;
import lombok.Getter;
import org.apache.logging.log4j.core.util.ExecutorServices;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.world.WorldUnloadEvent;
import org.checkerframework.checker.units.qual.N;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Array;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.IntStream;
public class TurboPregenerator extends Thread implements Listener {
@Getter
private static TurboPregenerator instance;
private final TurboPregenJob job;
private final File destination;
private final int maxPosition;
private World world;
private final ChronoLatch latch;
private static AtomicInteger turboGeneratedChunks;
private final AtomicInteger generatedLast;
private final AtomicLong cachedLast;
private final RollingSequence cachePerSecond;
private final AtomicInteger turboTotalChunks;
private final AtomicLong startTime;
private final RollingSequence chunksPerSecond;
private final RollingSequence chunksPerMinute;
private KList<Position2> queue;
private ConcurrentHashMap<Integer, Position2> cache;
private AtomicInteger maxWaiting;
private ReentrantLock cachinglock;
private AtomicBoolean caching;
private final HyperLock hyperLock;
private MultiBurst burst;
private static final Map<String, TurboPregenJob> jobs = new HashMap<>();
public TurboPregenerator(TurboPregenJob job, File destination) {
this.job = job;
queue = new KList<>(512);
this.maxWaiting = new AtomicInteger(128);
this.destination = destination;
this.maxPosition = new Spiraler(job.getRadiusBlocks() * 2, job.getRadiusBlocks() * 2, (x, z) -> {
}).count();
this.world = Bukkit.getWorld(job.getWorld());
this.latch = new ChronoLatch(3000);
this.burst = MultiBurst.burst;
this.hyperLock = new HyperLock();
this.startTime = new AtomicLong(M.ms());
this.cachePerSecond = new RollingSequence(10);
this.chunksPerSecond = new RollingSequence(10);
this.chunksPerMinute = new RollingSequence(10);
turboGeneratedChunks = new AtomicInteger(0);
this.generatedLast = new AtomicInteger(0);
this.cachedLast = new AtomicLong(0);
this.caching = new AtomicBoolean(false);
this.turboTotalChunks = new AtomicInteger((int) Math.ceil(Math.pow((2.0 * job.getRadiusBlocks()) / 16, 2)));
cache = new ConcurrentHashMap<>(turboTotalChunks.get());
this.cachinglock = new ReentrantLock();
jobs.put(job.getWorld(), job);
TurboPregenerator.instance = this;
}
public TurboPregenerator(File file) throws IOException {
this(new Gson().fromJson(IO.readAll(file), TurboPregenerator.TurboPregenJob.class), file);
}
public static void loadTurboGenerator(String i) {
World x = Bukkit.getWorld(i);
File turbogen = new File(x.getWorldFolder(), "turbogen.json");
if (turbogen.exists()) {
try {
TurboPregenerator p = new TurboPregenerator(turbogen);
p.start();
Iris.info("Started Turbo Pregenerator: " + p.job);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@EventHandler
public void on(WorldUnloadEvent e) {
if (e.getWorld().equals(world)) {
interrupt();
}
}
public void run() {
while (!interrupted()) {
tick();
}
try {
saveNow();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void tick() {
TurboPregenJob job = jobs.get(world.getName());
if (!cachinglock.isLocked() && cache.isEmpty() && !caching.get()) {
ExecutorService cache = Executors.newFixedThreadPool(1);
cache.submit(this::cache);
}
if (latch.flip() && caching.get()) {
long secondCached = cache.mappingCount() - cachedLast.get();
cachedLast.set(cache.mappingCount());
secondCached = secondCached / 3;
cachePerSecond.put(secondCached);
Iris.info("TurboGen: " + C.IRIS + world.getName() + C.RESET + C.BLUE + " Caching: " + Form.f(cache.mappingCount()) + " of " + Form.f(turboTotalChunks.get()) + " " + Form.f((int) cachePerSecond.getAverage()) + "/s");
}
if (latch.flip() && !job.paused && !cachinglock.isLocked()) {
long eta = computeETA();
save();
int secondGenerated = turboGeneratedChunks.get() - generatedLast.get();
generatedLast.set(turboGeneratedChunks.get());
secondGenerated = secondGenerated / 3;
chunksPerSecond.put(secondGenerated);
chunksPerMinute.put(secondGenerated * 60);
Iris.info("TurboGen: " + C.IRIS + world.getName() + C.RESET + " RTT: " + Form.f(turboGeneratedChunks.get()) + " of " + Form.f(turboTotalChunks.get()) + " " + Form.f((int) chunksPerSecond.getAverage()) + "/s ETA: " + Form.duration((double) eta, 2));
}
if (turboGeneratedChunks.get() >= turboTotalChunks.get()) {
Iris.info("Completed Turbo Gen!");
interrupt();
} else {
if (!cachinglock.isLocked()) {
int pos = job.getPosition() + 1;
job.setPosition(pos);
if (!job.paused) {
if (queue.size() < maxWaiting.get()) {
Position2 chunk = cache.get(pos);
queue.add(chunk);
}
waitForChunksPartial();
}
}
}
}
private void cache() {
if (!cachinglock.isLocked()) {
cachinglock.lock();
caching.set(true);
PrecisionStopwatch p = PrecisionStopwatch.start();
BurstExecutor b = MultiBurst.burst.burst(turboTotalChunks.get());
b.setMulticore(true);
int[] list = IntStream.rangeClosed(0, turboTotalChunks.get()).toArray();
AtomicInteger order = new AtomicInteger(turboTotalChunks.get());
int threads = Runtime.getRuntime().availableProcessors();
if (threads > 1) threads--;
ExecutorService process = Executors.newFixedThreadPool(threads);
for (int id : list) {
b.queue(() -> {
cache.put(id, getChunk(id));
order.addAndGet(-1);
});
}
b.complete();
if (order.get() < 0) {
cachinglock.unlock();
caching.set(false);
Iris.info("Completed Caching in: " + Form.duration(p.getMilliseconds(), 2));
}
} else {
Iris.error("TurboCache is locked!");
}
}
private void waitForChunksPartial() {
while (!queue.isEmpty() && maxWaiting.get() > queue.size()) {
try {
for (Position2 c : new KList<>(queue)) {
tickGenerate(c);
queue.remove(c);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
private long computeETA() {
return (long) ((turboTotalChunks.get() - turboGeneratedChunks.get()) / chunksPerMinute.getAverage()) * 1000;
// todo broken
}
private final ExecutorService executorService = Executors.newFixedThreadPool(10);
private void tickGenerate(Position2 chunk) {
executorService.submit(() -> {
CountDownLatch latch = new CountDownLatch(1);
PaperLib.getChunkAtAsync(world, chunk.getX(), chunk.getZ(), true)
.thenAccept((i) -> {
latch.countDown();
});
try {
latch.await();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
Iris.verbose("Turbo pregenerator worker interrupted while waiting for chunk " + chunk + ".");
}
turboGeneratedChunks.addAndGet(1);
});
}
public Position2 getChunk(int position) {
int p = -1;
AtomicInteger xx = new AtomicInteger();
AtomicInteger zz = new AtomicInteger();
Spiraler s = new Spiraler(job.getRadiusBlocks() * 2, job.getRadiusBlocks() * 2, (x, z) -> {
xx.set(x);
zz.set(z);
});
while (s.hasNext() && p++ < position) {
s.next();
}
return new Position2(xx.get(), zz.get());
}
public void save() {
J.a(() -> {
try {
saveNow();
} catch (Throwable e) {
e.printStackTrace();
}
});
}
public static void setPausedTurbo(World world) {
TurboPregenJob job = jobs.get(world.getName());
if (isPausedTurbo(world)) {
job.paused = false;
} else {
job.paused = true;
}
if (job.paused) {
Iris.info(C.BLUE + "TurboGen: " + C.IRIS + world.getName() + C.BLUE + " Paused");
} else {
Iris.info(C.BLUE + "TurboGen: " + C.IRIS + world.getName() + C.BLUE + " Resumed");
}
}
public static boolean isPausedTurbo(World world) {
TurboPregenJob job = jobs.get(world.getName());
return job != null && job.isPaused();
}
public static long remainingChunks() {
TurboPregenerator local = instance;
AtomicInteger generated = turboGeneratedChunks;
if (local == null || generated == null) {
return -1L;
}
return Math.max(0L, local.turboTotalChunks.get() - generated.get());
}
public static double chunksPerSecond() {
TurboPregenerator local = instance;
if (local == null) {
return 0D;
}
return Math.max(0D, local.chunksPerSecond.getAverage());
}
public void shutdownInstance(World world) throws IOException {
Iris.info("turboGen: " + C.IRIS + world.getName() + C.BLUE + " Shutting down..");
TurboPregenJob job = jobs.get(world.getName());
File worldDirectory = new File(Bukkit.getWorldContainer(), world.getName());
File turboFile = new File(worldDirectory, "turbogen.json");
if (job == null) {
Iris.error("No turbogen job found for world: " + world.getName());
return;
}
try {
if (!job.isPaused()) {
job.setPaused(true);
}
save();
jobs.remove(world.getName());
J.a(() -> {
while (turboFile.exists()) {
turboFile.delete();
J.sleep(1000);
}
Iris.info("turboGen: " + C.IRIS + world.getName() + C.BLUE + " File deleted and instance closed.");
}, 20);
} catch (Exception e) {
Iris.error("Failed to shutdown turbogen for " + world.getName());
e.printStackTrace();
} finally {
saveNow();
interrupt();
}
}
public void saveNow() throws IOException {
IO.writeAll(this.destination, new Gson().toJson(job));
}
@Data
@lombok.Builder
public static class TurboPregenJob {
private String world;
@lombok.Builder.Default
private int radiusBlocks = 5000;
@lombok.Builder.Default
private int position = 0;
@lombok.Builder.Default
boolean paused = false;
}
}
@@ -52,6 +52,9 @@ import java.util.concurrent.atomic.AtomicLong;
public class AsyncPregenMethod implements PregeneratorMethod { public class AsyncPregenMethod implements PregeneratorMethod {
private static final AtomicInteger THREAD_COUNT = new AtomicInteger(); private static final AtomicInteger THREAD_COUNT = new AtomicInteger();
private static final int ADAPTIVE_TIMEOUT_STEP = 3; private static final int ADAPTIVE_TIMEOUT_STEP = 3;
private static final int ADAPTIVE_RECOVERY_INTERVAL = 8;
private static final long CHUNK_CLEANUP_INTERVAL_MS = 15_000L;
private static final long CHUNK_CLEANUP_MIN_AGE_MS = 5_000L;
private final World world; private final World world;
private final IrisRuntimeSchedulerMode runtimeSchedulerMode; private final IrisRuntimeSchedulerMode runtimeSchedulerMode;
private final IrisPaperLikeBackendMode paperLikeBackendMode; private final IrisPaperLikeBackendMode paperLikeBackendMode;
@@ -84,6 +87,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
private final AtomicLong failed = new AtomicLong(); private final AtomicLong failed = new AtomicLong();
private final AtomicLong lastProgressAt = new AtomicLong(M.ms()); private final AtomicLong lastProgressAt = new AtomicLong(M.ms());
private final AtomicLong lastPermitWaitLog = new AtomicLong(0L); private final AtomicLong lastPermitWaitLog = new AtomicLong(0L);
private final AtomicLong lastChunkCleanup = new AtomicLong(M.ms());
private final Object permitMonitor = new Object(); private final Object permitMonitor = new Object();
private volatile Engine metricsEngine; private volatile Engine metricsEngine;
@@ -120,8 +124,11 @@ public class AsyncPregenMethod implements PregeneratorMethod {
this.backendMode = "paper-ticket"; this.backendMode = "paper-ticket";
} }
} }
int runtimeMaxConcurrency = foliaRuntime
? pregen.getFoliaMaxConcurrency()
: pregen.getPaperLikeMaxConcurrency();
int configuredThreads = applyRuntimeConcurrencyCap( int configuredThreads = applyRuntimeConcurrencyCap(
pregen.getMaxConcurrency(), runtimeMaxConcurrency,
foliaRuntime, foliaRuntime,
workerThreadsForCap workerThreadsForCap
); );
@@ -206,6 +213,48 @@ public class AsyncPregenMethod implements PregeneratorMethod {
} }
} }
private void periodicChunkCleanup() {
long now = M.ms();
long lastCleanup = lastChunkCleanup.get();
if (now - lastCleanup < CHUNK_CLEANUP_INTERVAL_MS) {
return;
}
if (!lastChunkCleanup.compareAndSet(lastCleanup, now)) {
return;
}
if (foliaRuntime) {
int sizeBefore = lastUse.size();
if (sizeBefore > 0) {
lastUse.clear();
Iris.info("Periodic chunk cleanup: cleared " + sizeBefore + " Folia chunk references");
}
return;
}
int sizeBefore = lastUse.size();
if (sizeBefore == 0) {
return;
}
long minTime = now - CHUNK_CLEANUP_MIN_AGE_MS;
AtomicInteger removed = new AtomicInteger();
lastUse.entrySet().removeIf(entry -> {
Long lastUseTime = entry.getValue();
if (lastUseTime == null || lastUseTime < minTime) {
removed.incrementAndGet();
return true;
}
return false;
});
int removedCount = removed.get();
if (removedCount > 0) {
Iris.info("Periodic chunk cleanup: removed " + removedCount + "/" + sizeBefore + " stale chunk references");
}
}
private Chunk onChunkFutureFailure(int x, int z, Throwable throwable) { private Chunk onChunkFutureFailure(int x, int z, Throwable throwable) {
Throwable root = throwable; Throwable root = throwable;
while (root.getCause() != null) { while (root.getCause() != null) {
@@ -246,11 +295,14 @@ public class AsyncPregenMethod implements PregeneratorMethod {
private void onSuccess() { private void onSuccess() {
int streak = timeoutStreak.get(); int streak = timeoutStreak.get();
if (streak > 0) { if (streak > 0) {
timeoutStreak.compareAndSet(streak, streak - 1); int newStreak = Math.max(0, streak - 2);
timeoutStreak.compareAndSet(streak, newStreak);
if (newStreak > 0) {
return; return;
} }
}
if ((completed.get() & 31L) == 0L) { if ((completed.get() & (ADAPTIVE_RECOVERY_INTERVAL - 1)) == 0L) {
raiseAdaptiveInFlightLimit(); raiseAdaptiveInFlightLimit();
} }
} }
@@ -278,7 +330,9 @@ public class AsyncPregenMethod implements PregeneratorMethod {
return; return;
} }
int next = Math.min(threads, current + 1); int deficit = threads - current;
int step = deficit > (threads / 2) ? Math.max(2, threads / 8) : 1;
int next = Math.min(threads, current + step);
if (adaptiveInFlightLimit.compareAndSet(current, next)) { if (adaptiveInFlightLimit.compareAndSet(current, next)) {
logAdaptiveLimit("increase", next); logAdaptiveLimit("increase", next);
notifyPermitWaiters(); notifyPermitWaiters();
@@ -301,13 +355,13 @@ public class AsyncPregenMethod implements PregeneratorMethod {
static int computePaperLikeRecommendedCap(int workerThreads) { static int computePaperLikeRecommendedCap(int workerThreads) {
int normalizedWorkers = Math.max(1, workerThreads); int normalizedWorkers = Math.max(1, workerThreads);
int recommendedCap = normalizedWorkers * 2; int recommendedCap = normalizedWorkers * 4;
if (recommendedCap < 8) { if (recommendedCap < 8) {
return 8; return 8;
} }
if (recommendedCap > 96) { if (recommendedCap > 128) {
return 96; return 128;
} }
return recommendedCap; return recommendedCap;
@@ -400,6 +454,16 @@ public class AsyncPregenMethod implements PregeneratorMethod {
} }
} }
private void cleanupMantleChunk(int x, int z) {
Engine engine = resolveMetricsEngine();
if (engine != null) {
try {
engine.getMantle().forceCleanupChunk(x, z);
} catch (Throwable ignored) {
}
}
}
private Engine resolveMetricsEngine() { private Engine resolveMetricsEngine() {
Engine cachedEngine = metricsEngine; Engine cachedEngine = metricsEngine;
if (cachedEngine != null) { if (cachedEngine != null) {
@@ -488,13 +552,14 @@ public class AsyncPregenMethod implements PregeneratorMethod {
@Override @Override
public void generateChunk(int x, int z, PregenListener listener) { public void generateChunk(int x, int z, PregenListener listener) {
listener.onChunkGenerating(x, z); listener.onChunkGenerating(x, z);
periodicChunkCleanup();
try { try {
long waitStart = M.ms(); long waitStart = M.ms();
synchronized (permitMonitor) { synchronized (permitMonitor) {
while (inFlight.get() >= adaptiveInFlightLimit.get()) { while (inFlight.get() >= adaptiveInFlightLimit.get()) {
long waited = Math.max(0L, M.ms() - waitStart); long waited = Math.max(0L, M.ms() - waitStart);
logPermitWaitIfNeeded(x, z, waited); logPermitWaitIfNeeded(x, z, waited);
permitMonitor.wait(5000L); permitMonitor.wait(500L);
} }
} }
long adaptiveWait = Math.max(0L, M.ms() - waitStart); long adaptiveWait = Math.max(0L, M.ms() - waitStart);
@@ -503,7 +568,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
} }
long permitWaitStart = M.ms(); long permitWaitStart = M.ms();
while (!semaphore.tryAcquire(5, TimeUnit.SECONDS)) { while (!semaphore.tryAcquire(1, TimeUnit.SECONDS)) {
logPermitWaitIfNeeded(x, z, Math.max(0L, M.ms() - waitStart)); logPermitWaitIfNeeded(x, z, Math.max(0L, M.ms() - waitStart));
} }
long permitWait = Math.max(0L, M.ms() - permitWaitStart); long permitWait = Math.max(0L, M.ms() - permitWaitStart);
@@ -698,6 +763,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
} }
listener.onChunkGenerated(x, z); listener.onChunkGenerated(x, z);
cleanupMantleChunk(x, z);
listener.onChunkCleaned(x, z); listener.onChunkCleaned(x, z);
lastUse.put(chunk, M.ms()); lastUse.put(chunk, M.ms());
success = true; success = true;
@@ -730,6 +796,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
} }
listener.onChunkGenerated(x, z); listener.onChunkGenerated(x, z);
cleanupMantleChunk(x, z);
listener.onChunkCleaned(x, z); listener.onChunkCleaned(x, z);
lastUse.put(i, M.ms()); lastUse.put(i, M.ms());
success = true; success = true;
@@ -765,6 +832,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
} }
listener.onChunkGenerated(x, z); listener.onChunkGenerated(x, z);
cleanupMantleChunk(x, z);
listener.onChunkCleaned(x, z); listener.onChunkCleaned(x, z);
lastUse.put(i, M.ms()); lastUse.put(i, M.ms());
success = true; success = true;
@@ -338,7 +338,7 @@ public class IrisProject {
public CompletableFuture<StudioOpenCoordinator.StudioCloseResult> close() { public CompletableFuture<StudioOpenCoordinator.StudioCloseResult> close() {
if (activeProvider == null) { if (activeProvider == null) {
return CompletableFuture.completedFuture(new StudioOpenCoordinator.StudioCloseResult(null, true, true, false, null, null)); return CompletableFuture.completedFuture(new StudioOpenCoordinator.StudioCloseResult(null, true, true, false, null));
} }
return StudioOpenCoordinator.get().closeProject(this); return StudioOpenCoordinator.get().closeProject(this);
@@ -1,368 +0,0 @@
package art.arcane.iris.core.runtime;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import art.arcane.iris.Iris;
import art.arcane.volmlib.util.io.IO;
import lombok.Data;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
public final class SmokeDiagnosticsService {
private static volatile SmokeDiagnosticsService instance;
private final ConcurrentHashMap<String, SmokeRunReport> reports;
private final AtomicReference<String> latestRunId;
private final AtomicLong runCounter;
private final Gson gson;
private SmokeDiagnosticsService() {
this.reports = new ConcurrentHashMap<>();
this.latestRunId = new AtomicReference<>();
this.runCounter = new AtomicLong(1L);
this.gson = new GsonBuilder().setPrettyPrinting().create();
}
public static SmokeDiagnosticsService get() {
SmokeDiagnosticsService current = instance;
if (current != null) {
return current;
}
synchronized (SmokeDiagnosticsService.class) {
if (instance != null) {
return instance;
}
instance = new SmokeDiagnosticsService();
return instance;
}
}
public SmokeRunHandle beginRun(SmokeRunMode mode, String worldName, boolean studio, boolean headless, String playerName, boolean retainOnFailure) {
long ordinal = runCounter.getAndIncrement();
String runId = String.format("%s-%05d", mode.id(), ordinal);
SmokeRunReport report = new SmokeRunReport();
report.setRunId(runId);
report.setMode(mode.id());
report.setWorldName(worldName);
report.setStudio(studio);
report.setHeadless(headless);
report.setPlayerName(playerName);
report.setRetainOnFailure(retainOnFailure);
report.setStartedAt(System.currentTimeMillis());
report.setOutcome("running");
report.setStage("queued");
report.setLifecycleBackend(art.arcane.iris.core.lifecycle.WorldLifecycleService.get().capabilities().serverFamily().id());
report.setRuntimeBackend(WorldRuntimeControlService.get().backendName());
reports.put(runId, report);
latestRunId.set(runId);
persist(report);
return new SmokeRunHandle(report);
}
public SmokeRunReport latest() {
String runId = latestRunId.get();
if (runId == null) {
return null;
}
return get(runId);
}
public SmokeRunReport get(String runId) {
if (runId == null || runId.isBlank()) {
return null;
}
SmokeRunReport report = reports.get(runId);
if (report != null) {
return snapshot(report);
}
return load(runId);
}
public SmokeRunReport latestPersisted() {
File latestFile = latestFile();
if (!latestFile.exists()) {
return null;
}
try {
return gson.fromJson(IO.readAll(latestFile), SmokeRunReport.class);
} catch (Throwable e) {
return null;
}
}
private SmokeRunReport load(String runId) {
File file = reportFile(runId);
if (!file.exists()) {
return null;
}
try {
return gson.fromJson(IO.readAll(file), SmokeRunReport.class);
} catch (Throwable e) {
return null;
}
}
private void persist(SmokeRunReport report) {
if (report == null || !SmokeRunMode.shouldPersist(report.getMode())) {
return;
}
try {
String json = gson.toJson(report);
File file = reportFile(report.getRunId());
IO.writeAll(file, json);
IO.writeAll(latestFile(), json);
} catch (Throwable e) {
Iris.reportError("Failed to persist smoke report \"" + report.getRunId() + "\".", e);
}
}
private SmokeRunReport snapshot(SmokeRunReport report) {
String json = gson.toJson(report);
return gson.fromJson(json, SmokeRunReport.class);
}
private File reportFile(String runId) {
if (Iris.instance == null) {
File root = new File("plugins/Iris/diagnostics/smoke");
root.mkdirs();
return new File(root, runId + ".json");
}
return Iris.instance.getDataFile("diagnostics", "smoke", runId + ".json");
}
private File latestFile() {
if (Iris.instance == null) {
File root = new File("plugins/Iris/diagnostics/smoke");
root.mkdirs();
return new File(root, "latest.json");
}
return Iris.instance.getDataFile("diagnostics", "smoke", "latest.json");
}
public enum SmokeRunMode {
FULL("full", true),
STUDIO("studio", true),
CREATE("create", true),
BENCHMARK("benchmark", true),
STUDIO_OPEN("studio_open", false),
STUDIO_CLOSE("studio_close", false);
private final String id;
private final boolean persisted;
SmokeRunMode(String id, boolean persisted) {
this.id = id;
this.persisted = persisted;
}
public String id() {
return id;
}
static boolean shouldPersist(String id) {
for (SmokeRunMode mode : values()) {
if (mode.id.equals(id)) {
return mode.persisted;
}
}
return false;
}
}
public final class SmokeRunHandle {
private final SmokeRunReport report;
private SmokeRunHandle(SmokeRunReport report) {
this.report = report;
}
public String runId() {
return report.getRunId();
}
public SmokeRunReport snapshot() {
synchronized (report) {
report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt());
return SmokeDiagnosticsService.this.snapshot(report);
}
}
public void setWorldName(String worldName) {
synchronized (report) {
report.setWorldName(worldName);
report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt());
persist(report);
}
}
public void setLifecycleBackend(String backend) {
synchronized (report) {
report.setLifecycleBackend(backend);
persist(report);
}
}
public void setRuntimeBackend(String backend) {
synchronized (report) {
report.setRuntimeBackend(backend);
persist(report);
}
}
public void setEntryChunk(int chunkX, int chunkZ) {
synchronized (report) {
report.setEntryChunkX(chunkX);
report.setEntryChunkZ(chunkZ);
report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt());
persist(report);
}
}
public void setGenerationSession(long sessionId, int activeLeases) {
synchronized (report) {
report.setGenerationSessionId(sessionId);
report.setGenerationActiveLeases(activeLeases);
report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt());
persist(report);
}
}
public void setDatapackReadiness(DatapackReadinessResult readiness) {
synchronized (report) {
report.setDatapackReadiness(readiness);
report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt());
persist(report);
}
}
public void setCloseState(boolean unloadCompletedLive, boolean folderDeletionCompletedLive, boolean startupCleanupQueued) {
synchronized (report) {
report.setCloseUnloadCompletedLive(unloadCompletedLive);
report.setCloseFolderDeletionCompletedLive(folderDeletionCompletedLive);
report.setCloseStartupCleanupQueued(startupCleanupQueued);
report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt());
persist(report);
}
}
public void note(String text) {
synchronized (report) {
ArrayList<String> notes = new ArrayList<>(report.getNotes());
notes.add(text);
report.setNotes(List.copyOf(notes));
report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt());
persist(report);
}
}
public void stage(String stage) {
stage(stage, null);
}
public void stage(String stage, String detail) {
synchronized (report) {
report.setStage(stage);
report.setStageDetail(detail);
report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt());
persist(report);
}
}
public void completeSuccess(String finalStage, boolean cleanupApplied) {
synchronized (report) {
report.setStage(finalStage);
report.setOutcome("success");
report.setCleanupApplied(cleanupApplied);
report.setCompletedAt(System.currentTimeMillis());
report.setElapsedMs(report.getCompletedAt() - report.getStartedAt());
persist(report);
}
}
public void completeFailure(String finalStage, Throwable throwable, boolean cleanupApplied) {
synchronized (report) {
report.setStage(finalStage);
report.setOutcome("failed");
report.setCleanupApplied(cleanupApplied);
report.setCompletedAt(System.currentTimeMillis());
report.setElapsedMs(report.getCompletedAt() - report.getStartedAt());
if (throwable != null) {
report.setFailureType(throwable.getClass().getName());
report.setFailureMessage(String.valueOf(throwable.getMessage()));
report.setFailureChain(failureChain(throwable));
report.setFailureStacktrace(stacktrace(throwable));
}
persist(report);
}
}
private List<String> failureChain(Throwable throwable) {
ArrayList<String> chain = new ArrayList<>();
Throwable cursor = throwable;
while (cursor != null) {
chain.add(cursor.getClass().getName() + ": " + String.valueOf(cursor.getMessage()));
cursor = cursor.getCause();
}
return List.copyOf(chain);
}
private String stacktrace(Throwable throwable) {
StringWriter writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
throwable.printStackTrace(printWriter);
printWriter.flush();
return writer.toString();
}
}
@Data
public static final class SmokeRunReport {
private String runId;
private String mode;
private String worldName;
private String stage;
private String stageDetail;
private long startedAt;
private long completedAt;
private long elapsedMs;
private String outcome;
private String lifecycleBackend;
private String runtimeBackend;
private long generationSessionId;
private int generationActiveLeases;
private Integer entryChunkX;
private Integer entryChunkZ;
private boolean studio;
private boolean headless;
private String playerName;
private boolean retainOnFailure;
private boolean cleanupApplied;
private boolean closeUnloadCompletedLive;
private boolean closeFolderDeletionCompletedLive;
private boolean closeStartupCleanupQueued;
private DatapackReadinessResult datapackReadiness;
private String failureType;
private String failureMessage;
private List<String> failureChain = List.of();
private String failureStacktrace;
private List<String> notes = List.of();
}
}
@@ -1,418 +0,0 @@
package art.arcane.iris.core.runtime;
import art.arcane.iris.Iris;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.tools.IrisCreator;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.exceptions.IrisException;
import art.arcane.volmlib.util.io.IO;
import org.bukkit.Bukkit;
import org.bukkit.World;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public final class SmokeTestService {
private static volatile SmokeTestService instance;
private final SmokeDiagnosticsService diagnostics;
private SmokeTestService() {
this.diagnostics = SmokeDiagnosticsService.get();
}
public static SmokeTestService get() {
SmokeTestService current = instance;
if (current != null) {
return current;
}
synchronized (SmokeTestService.class) {
if (instance != null) {
return instance;
}
instance = new SmokeTestService();
return instance;
}
}
public String startCreateSmoke(VolmitSender sender, String dimensionKey, long seed, boolean retainOnFailure) {
SmokeDiagnosticsService.SmokeRunHandle handle = diagnostics.beginRun(
SmokeDiagnosticsService.SmokeRunMode.CREATE,
nextWorldName("create"),
false,
true,
null,
retainOnFailure
);
J.a(() -> executeCreateSmoke(handle, sender, dimensionKey, seed, false, true));
return handle.runId();
}
public String startBenchmarkSmoke(VolmitSender sender, String dimensionKey, long seed, boolean retainOnFailure) {
SmokeDiagnosticsService.SmokeRunHandle handle = diagnostics.beginRun(
SmokeDiagnosticsService.SmokeRunMode.BENCHMARK,
nextWorldName("benchmark"),
false,
true,
null,
retainOnFailure
);
J.a(() -> executeCreateSmoke(handle, sender, dimensionKey, seed, true, true));
return handle.runId();
}
public String startStudioSmoke(VolmitSender sender, String dimensionKey, long seed, String playerName, boolean retainOnFailure) {
String normalizedPlayer = normalizePlayerName(playerName);
SmokeDiagnosticsService.SmokeRunHandle handle = diagnostics.beginRun(
SmokeDiagnosticsService.SmokeRunMode.STUDIO,
nextWorldName("studio"),
true,
normalizedPlayer == null,
normalizedPlayer,
retainOnFailure
);
J.a(() -> executeStudioSmoke(handle, sender, dimensionKey, seed, normalizedPlayer, retainOnFailure, true));
return handle.runId();
}
public String startFullSmoke(VolmitSender sender, String dimensionKey, long seed, String playerName, boolean retainOnFailure) {
String normalizedPlayer = normalizePlayerName(playerName);
SmokeDiagnosticsService.SmokeRunHandle handle = diagnostics.beginRun(
SmokeDiagnosticsService.SmokeRunMode.FULL,
nextWorldName("full"),
false,
normalizedPlayer == null,
normalizedPlayer,
retainOnFailure
);
J.a(() -> executeFullSmoke(handle, sender, dimensionKey, seed, normalizedPlayer, retainOnFailure));
return handle.runId();
}
public SmokeDiagnosticsService.SmokeRunReport latest() {
SmokeDiagnosticsService.SmokeRunReport latest = diagnostics.latest();
if (latest != null) {
return latest;
}
return diagnostics.latestPersisted();
}
public SmokeDiagnosticsService.SmokeRunReport get(String runId) {
return diagnostics.get(runId);
}
public WorldInspection inspectWorld(String worldName) {
World world = Bukkit.getWorld(worldName);
if (world == null) {
return null;
}
PlatformChunkGenerator provider = IrisToolbelt.access(world);
boolean studio = provider != null && provider.isStudio();
boolean engineClosed = false;
boolean engineFailing = false;
long generationSessionId = 0L;
int activeLeases = 0;
if (provider != null && provider.getEngine() instanceof IrisEngine irisEngine) {
engineClosed = irisEngine.isClosed();
engineFailing = irisEngine.isFailing();
generationSessionId = irisEngine.getGenerationSessionId();
activeLeases = irisEngine.getGenerationSessions().activeLeases();
}
ArrayList<String> datapackFolders = new ArrayList<>();
File datapacksFolder = ServerConfigurator.resolveDatapacksFolder(world.getWorldFolder());
datapackFolders.add(datapacksFolder.getAbsolutePath());
return new WorldInspection(
world.getName(),
WorldLifecycleService.get().backendNameForWorld(world.getName()),
WorldRuntimeControlService.get().backendName(),
studio,
engineClosed,
engineFailing,
generationSessionId,
activeLeases,
List.copyOf(datapackFolders),
IrisToolbelt.isWorldMaintenanceActive(world)
);
}
private void executeFullSmoke(
SmokeDiagnosticsService.SmokeRunHandle handle,
VolmitSender sender,
String dimensionKey,
long seed,
String playerName,
boolean retainOnFailure
) {
try {
handle.stage("create");
executeCreateSmoke(handle, sender, dimensionKey, seed, false, false);
handle.note("create smoke complete");
handle.stage("benchmark");
executeCreateSmoke(handle, sender, dimensionKey, seed, true, false);
handle.note("benchmark smoke complete");
handle.stage("studio");
executeStudioSmoke(handle, sender, dimensionKey, seed, playerName, retainOnFailure, false);
handle.note("studio smoke complete");
handle.completeSuccess("cleanup", true);
} catch (Throwable e) {
handle.completeFailure("cleanup", e, !retainOnFailure);
}
}
private void executeCreateSmoke(
SmokeDiagnosticsService.SmokeRunHandle handle,
VolmitSender sender,
String dimensionKey,
long seed,
boolean benchmark,
boolean completeHandle
) {
String worldName = nextWorldName(benchmark ? "benchmark" : "create");
handle.setWorldName(worldName);
cleanupTransientPrefix("iris-smoke-");
World world = null;
PlatformChunkGenerator provider = null;
boolean cleanupApplied = false;
try {
IrisCreator creator = IrisToolbelt.createWorld()
.dimension(dimensionKey)
.name(worldName)
.seed(seed)
.sender(sender)
.studio(false)
.benchmark(benchmark)
.studioProgressConsumer((progress, stage) -> handle.stage(mapCreateStage(stage)));
world = creator.create();
provider = IrisToolbelt.access(world);
handle.setLifecycleBackend(WorldLifecycleService.get().backendNameForWorld(world.getName()));
handle.setRuntimeBackend(WorldRuntimeControlService.get().backendName());
handle.setDatapackReadiness(creator.getLastDatapackReadinessResult());
captureGenerationSession(provider, handle);
if (benchmark) {
handle.stage("apply_world_rules");
WorldRuntimeControlService.get().applyStudioWorldRules(world);
}
handle.stage("cleanup");
cleanupWorld(world, worldName);
cleanupApplied = true;
if (completeHandle) {
handle.completeSuccess("cleanup", true);
}
} catch (Throwable e) {
Iris.reportError("Smoke create failed for world \"" + worldName + "\".", e);
if (!handle.snapshot().isRetainOnFailure()) {
try {
cleanupWorld(world, worldName);
cleanupApplied = true;
} catch (Throwable cleanupError) {
Iris.reportError("Smoke cleanup failed for world \"" + worldName + "\".", cleanupError);
}
}
if (completeHandle) {
handle.completeFailure("cleanup", e, cleanupApplied);
} else {
if (e instanceof RuntimeException runtimeException) {
throw runtimeException;
}
throw new RuntimeException(e);
}
}
}
private void executeStudioSmoke(
SmokeDiagnosticsService.SmokeRunHandle handle,
VolmitSender sender,
String dimensionKey,
long seed,
String playerName,
boolean retainOnFailure,
boolean completeHandle
) {
String worldName = nextWorldName("studio");
handle.setWorldName(worldName);
cleanupTransientPrefix("iris-smoke-");
World world = null;
boolean cleanupApplied = false;
CompletableFuture<StudioOpenCoordinator.StudioOpenResult> future = StudioOpenCoordinator.get().open(
new StudioOpenCoordinator.StudioOpenRequest(
dimensionKey,
null,
sender,
seed,
worldName,
playerName,
false,
retainOnFailure,
SmokeDiagnosticsService.SmokeRunMode.STUDIO,
handle,
completeHandle,
update -> handle.stage(update.stage()),
openedWorld -> {
}
)
);
try {
StudioOpenCoordinator.StudioOpenResult result = future.join();
world = result == null ? null : result.world();
handle.stage("cleanup");
cleanupWorld(world, worldName);
cleanupApplied = true;
if (completeHandle) {
handle.completeSuccess("cleanup", true);
}
} catch (Throwable e) {
if (world != null && !cleanupApplied) {
try {
cleanupWorld(world, worldName);
cleanupApplied = true;
} catch (Throwable cleanupError) {
Iris.reportError("Smoke cleanup failed for world \"" + worldName + "\".", cleanupError);
}
}
if (completeHandle && !"failed".equalsIgnoreCase(handle.snapshot().getOutcome())) {
handle.completeFailure("cleanup", e, cleanupApplied);
}
if (!completeHandle) {
if (e instanceof RuntimeException runtimeException) {
throw runtimeException;
}
throw new RuntimeException(e);
}
}
}
private void captureGenerationSession(PlatformChunkGenerator provider, SmokeDiagnosticsService.SmokeRunHandle handle) {
if (provider == null || provider.getEngine() == null) {
return;
}
if (provider.getEngine() instanceof IrisEngine irisEngine) {
handle.setGenerationSession(irisEngine.getGenerationSessionId(), irisEngine.getGenerationSessions().activeLeases());
}
}
private void cleanupWorld(World world, String worldName) {
if (world != null) {
PlatformChunkGenerator provider = IrisToolbelt.access(world);
if (provider != null) {
provider.close();
}
WorldLifecycleService.get().unload(world, false);
}
File container = Bukkit.getWorldContainer();
deleteFolder(new File(container, worldName), worldName);
deleteFolder(new File(container, worldName + "_nether"), null);
deleteFolder(new File(container, worldName + "_the_end"), null);
}
private void deleteFolder(File folder, String worldName) {
if (folder == null) {
return;
}
IO.delete(folder);
if (!folder.exists()) {
return;
}
if (worldName == null) {
return;
}
try {
Iris.queueWorldDeletionOnStartup(Collections.singleton(worldName));
} catch (IOException e) {
Iris.reportError("Failed to queue smoke world deletion for \"" + worldName + "\".", e);
}
}
private void cleanupTransientPrefix(String prefix) {
File container = Bukkit.getWorldContainer();
File[] children = container.listFiles();
if (children == null) {
return;
}
for (File child : children) {
if (!child.isDirectory()) {
continue;
}
if (!child.getName().startsWith(prefix)) {
continue;
}
if (Bukkit.getWorld(child.getName()) != null) {
continue;
}
IO.delete(child);
}
}
private String nextWorldName(String mode) {
return "iris-smoke-" + mode + "-" + UUID.randomUUID().toString().substring(0, 8);
}
private String normalizePlayerName(String playerName) {
if (playerName == null) {
return null;
}
String trimmed = playerName.trim();
if (trimmed.isEmpty() || trimmed.equalsIgnoreCase("none")) {
return null;
}
return trimmed;
}
private String mapCreateStage(String stage) {
if (stage == null || stage.isBlank()) {
return "create_world";
}
String normalized = stage.trim().toLowerCase();
return switch (normalized) {
case "resolve_dimension", "resolving dimension" -> "resolve_dimension";
case "prepare_world_pack", "preparing world pack" -> "prepare_world_pack";
case "install_datapacks", "installing datapacks" -> "install_datapacks";
case "create_world", "creating world", "world created" -> "create_world";
default -> normalized.replace(' ', '_');
};
}
public record WorldInspection(
String worldName,
String lifecycleBackend,
String runtimeBackend,
boolean studio,
boolean engineClosed,
boolean engineFailing,
long generationSessionId,
int activeLeaseCount,
List<String> datapackFolders,
boolean maintenanceActive
) {
}
}
@@ -5,7 +5,6 @@ import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.project.IrisProject; import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.tools.IrisCreator; import art.arcane.iris.core.tools.IrisCreator;
import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J; import art.arcane.iris.util.common.scheduling.J;
@@ -31,10 +30,7 @@ import java.util.function.Consumer;
public final class StudioOpenCoordinator { public final class StudioOpenCoordinator {
private static volatile StudioOpenCoordinator instance; private static volatile StudioOpenCoordinator instance;
private final SmokeDiagnosticsService diagnostics;
private StudioOpenCoordinator() { private StudioOpenCoordinator() {
this.diagnostics = SmokeDiagnosticsService.get();
} }
public static StudioOpenCoordinator get() { public static StudioOpenCoordinator get() {
@@ -67,94 +63,54 @@ public final class StudioOpenCoordinator {
private StudioCloseResult executeClose(IrisProject project) { private StudioCloseResult executeClose(IrisProject project) {
if (project == null) { if (project == null) {
return new StudioCloseResult(null, true, true, false, null, null); return new StudioCloseResult(null, true, true, false, null);
} }
PlatformChunkGenerator provider = project.getActiveProvider(); PlatformChunkGenerator provider = project.getActiveProvider();
if (provider == null) { if (provider == null) {
return new StudioCloseResult(null, true, true, false, null, null); return new StudioCloseResult(null, true, true, false, null);
} }
World world = provider.getTarget().getWorld().realWorld(); World world = provider.getTarget().getWorld().realWorld();
String worldName = world == null ? provider.getTarget().getWorld().name() : world.getName(); String worldName = world == null ? provider.getTarget().getWorld().name() : world.getName();
SmokeDiagnosticsService.SmokeRunHandle handle = diagnostics.beginRun(
SmokeDiagnosticsService.SmokeRunMode.STUDIO_CLOSE,
worldName,
true,
true,
null,
false
);
StudioCloseResult result;
try { try {
handle.setRuntimeBackend(WorldRuntimeControlService.get().backendName()); return closeWorld(provider, worldName, world, true, project);
if (world != null) {
handle.setLifecycleBackend(WorldLifecycleService.get().backendNameForWorld(world.getName()));
captureGenerationSession(provider, handle);
}
result = closeWorld(provider, worldName, world, true, handle, project);
handle.setCloseState(result.unloadCompletedLive(), result.folderDeletionCompletedLive(), result.startupCleanupQueued());
if (result.failureCause() != null) {
handle.completeFailure("finalize_close", result.failureCause(), result.folderDeletionCompletedLive() || result.startupCleanupQueued());
} else {
handle.completeSuccess("finalize_close", result.folderDeletionCompletedLive() || result.startupCleanupQueued());
}
} catch (Throwable e) { } catch (Throwable e) {
project.setActiveProvider(null); project.setActiveProvider(null);
handle.completeFailure("finalize_close", e, false); return new StudioCloseResult(worldName, false, false, false, e);
result = new StudioCloseResult(worldName, false, false, false, e, handle.runId());
} }
return result;
} }
private void executeOpen(StudioOpenRequest request, CompletableFuture<StudioOpenResult> future) { private void executeOpen(StudioOpenRequest request, CompletableFuture<StudioOpenResult> future) {
boolean ownsHandle = request.runHandle() == null;
SmokeDiagnosticsService.SmokeRunHandle handle = ownsHandle
? diagnostics.beginRun(
request.mode(),
request.worldName(),
true,
request.playerName() == null || request.playerName().isBlank(),
request.playerName(),
request.retainOnFailure()
)
: request.runHandle();
World world = null; World world = null;
PlatformChunkGenerator provider = null; PlatformChunkGenerator provider = null;
boolean cleanupApplied = false;
try { try {
updateStage(handle, request, "resolve_dimension", 0.04D); updateStage(request, "resolve_dimension", 0.04D);
if (IrisToolbelt.getDimension(request.dimensionKey()) == null) { if (IrisToolbelt.getDimension(request.dimensionKey()) == null) {
throw new IrisException("Dimension cannot be found for id " + request.dimensionKey() + "."); throw new IrisException("Dimension cannot be found for id " + request.dimensionKey() + ".");
} }
updateStage(handle, request, "prepare_world_pack", 0.10D); updateStage(request, "prepare_world_pack", 0.10D);
cleanupStaleTransientWorlds(request.worldName()); cleanupStaleTransientWorlds(request.worldName());
updateStage(handle, request, "install_datapacks", 0.18D); updateStage(request, "install_datapacks", 0.18D);
IrisCreator creator = IrisToolbelt.createWorld() IrisCreator creator = IrisToolbelt.createWorld()
.seed(request.seed()) .seed(request.seed())
.sender(request.sender()) .sender(request.sender())
.studio(true) .studio(true)
.name(request.worldName()) .name(request.worldName())
.dimension(request.dimensionKey()) .dimension(request.dimensionKey())
.studioProgressConsumer((progress, stage) -> updateStage(handle, request, mapCreatorStage(stage), progress)); .studioProgressConsumer((progress, stage) -> updateStage(request, mapCreatorStage(stage), progress));
world = creator.create(); world = creator.create();
provider = IrisToolbelt.access(world); provider = IrisToolbelt.access(world);
if (provider == null) { if (provider == null) {
throw new IllegalStateException("Studio runtime provider is unavailable for world \"" + request.worldName() + "\"."); throw new IllegalStateException("Studio runtime provider is unavailable for world \"" + request.worldName() + "\".");
} }
handle.setLifecycleBackend(WorldLifecycleService.get().backendNameForWorld(world.getName())); updateStage(request, "apply_world_rules", 0.72D);
handle.setRuntimeBackend(WorldRuntimeControlService.get().backendName());
handle.setDatapackReadiness(creator.getLastDatapackReadinessResult());
captureGenerationSession(provider, handle);
updateStage(handle, request, "apply_world_rules", 0.72D);
WorldRuntimeControlService.get().applyStudioWorldRules(world); WorldRuntimeControlService.get().applyStudioWorldRules(world);
updateStage(handle, request, "prepare_generator", 0.78D); updateStage(request, "prepare_generator", 0.78D);
WorldRuntimeControlService.get().prepareGenerator(world); WorldRuntimeControlService.get().prepareGenerator(world);
Location entryAnchor = WorldRuntimeControlService.get().resolveEntryAnchor(world); Location entryAnchor = WorldRuntimeControlService.get().resolveEntryAnchor(world);
@@ -163,17 +119,17 @@ public final class StudioOpenCoordinator {
} }
long deadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(120L); long deadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(120L);
updateStage(handle, request, "request_entry_chunk", 0.84D); updateStage(request, "request_entry_chunk", 0.84D);
requestEntryChunk(world, entryAnchor, deadline, handle); requestEntryChunk(world, entryAnchor, deadline);
updateStage(handle, request, "resolve_safe_entry", 0.90D); updateStage(request, "resolve_safe_entry", 0.90D);
Location safeEntry = resolveSafeEntry(world, entryAnchor, deadline); Location safeEntry = resolveSafeEntry(world, entryAnchor, deadline);
if (safeEntry == null) { if (safeEntry == null) {
throw new IllegalStateException("Studio safe entry resolution timed out."); throw new IllegalStateException("Studio safe entry resolution timed out.");
} }
if (request.playerName() != null && !request.playerName().isBlank()) { if (request.playerName() != null && !request.playerName().isBlank()) {
updateStage(handle, request, "teleport_player", 0.96D); updateStage(request, "teleport_player", 0.96D);
Player player = resolvePlayer(request.playerName()); Player player = resolvePlayer(request.playerName());
if (player == null) { if (player == null) {
throw new IllegalStateException("Player \"" + request.playerName() + "\" is not online."); throw new IllegalStateException("Player \"" + request.playerName() + "\" is not online.");
@@ -186,7 +142,7 @@ public final class StudioOpenCoordinator {
} }
} }
updateStage(handle, request, "finalize_open", 1.00D); updateStage(request, "finalize_open", 1.00D);
if (request.project() != null) { if (request.project() != null) {
request.project().setActiveProvider(provider); request.project().setActiveProvider(provider);
} }
@@ -197,36 +153,24 @@ public final class StudioOpenCoordinator {
request.onDone().accept(world); request.onDone().accept(world);
} }
if (request.completeHandle()) { future.complete(new StudioOpenResult(world, safeEntry, creator.getLastDatapackReadinessResult()));
handle.completeSuccess("finalize_open", false);
} else {
handle.stage("finalize_open");
}
future.complete(new StudioOpenResult(world, handle.runId(), safeEntry, creator.getLastDatapackReadinessResult()));
} catch (Throwable e) { } catch (Throwable e) {
Iris.reportError("Studio open failed for world \"" + request.worldName() + "\".", e); Iris.reportError("Studio open failed for world \"" + request.worldName() + "\".", e);
if (!request.retainOnFailure()) { if (!request.retainOnFailure()) {
try { try {
updateStage(handle, request, "cleanup", 1.00D); updateStage(request, "cleanup", 1.00D);
StudioCloseResult cleanupResult = closeWorld(provider, request.worldName(), world, true, handle, request.project()); closeWorld(provider, request.worldName(), world, true, request.project());
cleanupApplied = cleanupResult.folderDeletionCompletedLive() || cleanupResult.startupCleanupQueued();
} catch (Throwable cleanupError) { } catch (Throwable cleanupError) {
Iris.reportError("Studio cleanup failed for world \"" + request.worldName() + "\".", cleanupError); Iris.reportError("Studio cleanup failed for world \"" + request.worldName() + "\".", cleanupError);
} }
} }
if (request.completeHandle()) {
handle.completeFailure("cleanup", e, cleanupApplied);
} else {
handle.stage("cleanup", String.valueOf(e.getMessage()));
}
future.completeExceptionally(e); future.completeExceptionally(e);
} }
} }
private void requestEntryChunk(World world, Location entryAnchor, long deadline, SmokeDiagnosticsService.SmokeRunHandle handle) throws Exception { private void requestEntryChunk(World world, Location entryAnchor, long deadline) throws Exception {
int chunkX = entryAnchor.getBlockX() >> 4; int chunkX = entryAnchor.getBlockX() >> 4;
int chunkZ = entryAnchor.getBlockZ() >> 4; int chunkZ = entryAnchor.getBlockZ() >> 4;
handle.setEntryChunk(chunkX, chunkZ);
long remaining = Math.max(1000L, deadline - System.currentTimeMillis()); long remaining = Math.max(1000L, deadline - System.currentTimeMillis());
waitForEntryChunk(world, chunkX, chunkZ, deadline, null).get(remaining, TimeUnit.MILLISECONDS); waitForEntryChunk(world, chunkX, chunkZ, deadline, null).get(remaining, TimeUnit.MILLISECONDS);
} }
@@ -241,25 +185,15 @@ public final class StudioOpenCoordinator {
String worldName, String worldName,
World world, World world,
boolean deleteFolder, boolean deleteFolder,
SmokeDiagnosticsService.SmokeRunHandle handle,
IrisProject project IrisProject project
) { ) {
Throwable failure = null; Throwable failure = null;
boolean unloadCompletedLive = world == null || !isWorldFamilyLoaded(worldName); boolean unloadCompletedLive = world == null || !isWorldFamilyLoaded(worldName);
boolean folderDeletionCompletedLive = !deleteFolder; boolean folderDeletionCompletedLive = !deleteFolder;
boolean startupCleanupQueued = false; boolean startupCleanupQueued = false;
CompletableFuture<Void> closeFuture = provider == null ? CompletableFuture.completedFuture(null) : CompletableFuture.completedFuture(null); CompletableFuture<Void> closeFuture = CompletableFuture.completedFuture(null);
updateCloseStage(handle, "prepare_close");
if (world != null) {
handle.setWorldName(world.getName());
handle.setLifecycleBackend(WorldLifecycleService.get().backendNameForWorld(world.getName()));
handle.setRuntimeBackend(WorldRuntimeControlService.get().backendName());
captureGenerationSession(provider, handle);
}
if (world != null) { if (world != null) {
updateCloseStage(handle, "evacuate_players");
try { try {
evacuatePlayers(world); evacuatePlayers(world);
} catch (Throwable e) { } catch (Throwable e) {
@@ -272,21 +206,17 @@ public final class StudioOpenCoordinator {
} }
try { try {
updateCloseStage(handle, "seal_runtime");
if (project != null) { if (project != null) {
project.setActiveProvider(null); project.setActiveProvider(null);
} }
if (provider != null) { if (provider != null) {
captureGenerationSession(provider, handle);
closeFuture = provider.closeAsync(); closeFuture = provider.closeAsync();
} }
updateCloseStage(handle, "request_unload");
if (worldName != null && !worldName.isBlank()) { if (worldName != null && !worldName.isBlank()) {
requestWorldFamilyUnload(worldName); requestWorldFamilyUnload(worldName);
} }
updateCloseStage(handle, "await_unload");
if (worldName != null && !worldName.isBlank()) { if (worldName != null && !worldName.isBlank()) {
long unloadDeadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(20L); long unloadDeadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(20L);
CompletableFuture<Void> unloadFuture = waitForWorldFamilyUnload(worldName, unloadDeadline); CompletableFuture<Void> unloadFuture = waitForWorldFamilyUnload(worldName, unloadDeadline);
@@ -310,21 +240,17 @@ public final class StudioOpenCoordinator {
} }
if (deleteFolder && worldName != null && !worldName.isBlank()) { if (deleteFolder && worldName != null && !worldName.isBlank()) {
updateCloseStage(handle, "delete_world_family");
WorldFamilyDeleteResult deleteResult = deleteWorldFamily(worldName, unloadCompletedLive); WorldFamilyDeleteResult deleteResult = deleteWorldFamily(worldName, unloadCompletedLive);
folderDeletionCompletedLive = deleteResult.liveDeleted(); folderDeletionCompletedLive = deleteResult.liveDeleted();
startupCleanupQueued = deleteResult.startupCleanupQueued(); startupCleanupQueued = deleteResult.startupCleanupQueued();
} }
updateCloseStage(handle, "finalize_close");
} finally { } finally {
if (world != null) { if (world != null) {
IrisToolbelt.endWorldMaintenance(world, "studio-close"); IrisToolbelt.endWorldMaintenance(world, "studio-close");
} }
} }
handle.setCloseState(unloadCompletedLive, folderDeletionCompletedLive, startupCleanupQueued); return new StudioCloseResult(worldName, unloadCompletedLive, folderDeletionCompletedLive, startupCleanupQueued, failure);
return new StudioCloseResult(worldName, unloadCompletedLive, folderDeletionCompletedLive, startupCleanupQueued, failure, handle.runId());
} }
private void evacuatePlayers(World world) throws Exception { private void evacuatePlayers(World world) throws Exception {
@@ -414,18 +340,7 @@ public final class StudioOpenCoordinator {
} }
} }
private void captureGenerationSession(PlatformChunkGenerator provider, SmokeDiagnosticsService.SmokeRunHandle handle) { private void updateStage(StudioOpenRequest request, String stage, double progress) {
if (provider == null || provider.getEngine() == null) {
return;
}
if (provider.getEngine() instanceof IrisEngine irisEngine) {
handle.setGenerationSession(irisEngine.getGenerationSessionId(), irisEngine.getGenerationSessions().activeLeases());
}
}
private void updateStage(SmokeDiagnosticsService.SmokeRunHandle handle, StudioOpenRequest request, String stage, double progress) {
handle.stage(stage);
if (request.progressConsumer() != null) { if (request.progressConsumer() != null) {
request.progressConsumer().accept(new StudioOpenProgress(progress, stage)); request.progressConsumer().accept(new StudioOpenProgress(progress, stage));
} }
@@ -583,10 +498,6 @@ public final class StudioOpenCoordinator {
return null; return null;
} }
private void updateCloseStage(SmokeDiagnosticsService.SmokeRunHandle handle, String stage) {
handle.stage(stage);
}
private boolean isWorldFamilyLoaded(String worldName) { private boolean isWorldFamilyLoaded(String worldName) {
if (worldName == null || worldName.isBlank()) { if (worldName == null || worldName.isBlank()) {
return false; return false;
@@ -610,9 +521,6 @@ public final class StudioOpenCoordinator {
String playerName, String playerName,
boolean openWorkspace, boolean openWorkspace,
boolean retainOnFailure, boolean retainOnFailure,
SmokeDiagnosticsService.SmokeRunMode mode,
SmokeDiagnosticsService.SmokeRunHandle runHandle,
boolean completeHandle,
Consumer<StudioOpenProgress> progressConsumer, Consumer<StudioOpenProgress> progressConsumer,
Consumer<World> onDone Consumer<World> onDone
) { ) {
@@ -627,9 +535,6 @@ public final class StudioOpenCoordinator {
playerName, playerName,
true, true,
false, false,
SmokeDiagnosticsService.SmokeRunMode.STUDIO_OPEN,
null,
true,
progressConsumer, progressConsumer,
onDone onDone
); );
@@ -639,7 +544,7 @@ public final class StudioOpenCoordinator {
public record StudioOpenProgress(double progress, String stage) { public record StudioOpenProgress(double progress, String stage) {
} }
public record StudioOpenResult(World world, String runId, Location entryLocation, DatapackReadinessResult datapackReadiness) { public record StudioOpenResult(World world, Location entryLocation, DatapackReadinessResult datapackReadiness) {
} }
public record StudioCloseResult( public record StudioCloseResult(
@@ -647,8 +552,7 @@ public final class StudioOpenCoordinator {
boolean unloadCompletedLive, boolean unloadCompletedLive,
boolean folderDeletionCompletedLive, boolean folderDeletionCompletedLive,
boolean startupCleanupQueued, boolean startupCleanupQueued,
Throwable failureCause, Throwable failureCause
String runId
) { ) {
public boolean successful() { public boolean successful() {
return failureCause == null; return failureCause == null;
@@ -74,7 +74,7 @@ public enum Mode {
String[] info = new String[]{ String[] info = new String[]{
"", "",
padd2 + color + " Iris, " + C.AQUA + "Dimension Engine " + C.RED + "[" + releaseTrain + " RC.1]", padd2 + color + " Iris, " + C.AQUA + "Dimension Engine " + C.RED + "[" + releaseTrain + " RC.1.1.6]",
padd2 + C.GRAY + " Version: " + color + version, padd2 + C.GRAY + " Version: " + color + version,
padd2 + C.GRAY + " By: " + color + "Volmit Software (Arcane Arts)", padd2 + C.GRAY + " By: " + color + "Volmit Software (Arcane Arts)",
padd2 + C.GRAY + " Server: " + color + serverVersion, padd2 + C.GRAY + " Server: " + color + serverVersion,
@@ -33,6 +33,7 @@ import java.util.concurrent.atomic.AtomicLong;
public class IrisEngineSVC implements IrisService { public class IrisEngineSVC implements IrisService {
private static final int TRIM_PERIOD = 2_000; private static final int TRIM_PERIOD = 2_000;
private static final long ACTIVE_PREGEN_IDLE_MILLIS = 500L;
private final AtomicInteger tectonicLimit = new AtomicInteger(30); private final AtomicInteger tectonicLimit = new AtomicInteger(30);
private final AtomicInteger tectonicPlates = new AtomicInteger(); private final AtomicInteger tectonicPlates = new AtomicInteger();
private final AtomicInteger queuedTectonicPlates = new AtomicInteger(); private final AtomicInteger queuedTectonicPlates = new AtomicInteger();
@@ -298,7 +299,7 @@ public class IrisEngineSVC implements IrisService {
} }
try { try {
engine.getMantle().trim(tectonicLimit()); engine.getMantle().trim(activeIdleDuration(engineWorld), activeTectonicLimit(engineWorld));
} catch (Throwable e) { } catch (Throwable e) {
if (isMantleClosed(e)) { if (isMantleClosed(e)) {
close(); close();
@@ -326,7 +327,7 @@ public class IrisEngineSVC implements IrisService {
try { try {
long unloadStart = System.currentTimeMillis(); long unloadStart = System.currentTimeMillis();
int count = engine.getMantle().unloadTectonicPlate(IrisSettings.get().getPerformance().getEngineSVC().forceMulticoreWrite ? 0 : tectonicLimit()); int count = engine.getMantle().unloadTectonicPlate(IrisSettings.get().getPerformance().getEngineSVC().forceMulticoreWrite ? 0 : activeTectonicLimit(engineWorld));
if (count > 0) { if (count > 0) {
Iris.debug(C.GOLD + "Unloaded " + C.YELLOW + count + " TectonicPlates in " + C.RED + Form.duration(System.currentTimeMillis() - unloadStart, 2)); Iris.debug(C.GOLD + "Unloaded " + C.YELLOW + count + " TectonicPlates in " + C.RED + Form.duration(System.currentTimeMillis() - unloadStart, 2));
} }
@@ -347,6 +348,33 @@ public class IrisEngineSVC implements IrisService {
return tectonicLimit.get() / Math.max(worlds.size(), 1); return tectonicLimit.get() / Math.max(worlds.size(), 1);
} }
private int activeTectonicLimit(@Nullable World world) {
int limit = tectonicLimit();
if (world == null) {
return limit;
}
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
if (pregeneratorJob == null || !pregeneratorJob.targetsWorld(world)) {
return limit;
}
return Math.max(1, Math.min(limit, Math.max(2, limit / 8)));
}
private long activeIdleDuration(@Nullable World world) {
if (world == null) {
return TimeUnit.SECONDS.toMillis(IrisSettings.get().getPerformance().getMantleKeepAlive());
}
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
if (pregeneratorJob == null || !pregeneratorJob.targetsWorld(world)) {
return TimeUnit.SECONDS.toMillis(IrisSettings.get().getPerformance().getMantleKeepAlive());
}
return ACTIVE_PREGEN_IDLE_MILLIS;
}
@Synchronized @Synchronized
private void close() { private void close() {
if (closed) return; if (closed) return;
@@ -2,8 +2,6 @@ package art.arcane.iris.core.service;
import art.arcane.iris.Iris; import art.arcane.iris.Iris;
import art.arcane.iris.core.gui.PregeneratorJob; import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.pregenerator.LazyPregenerator;
import art.arcane.iris.core.pregenerator.TurboPregenerator;
import art.arcane.iris.util.common.plugin.IrisService; import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.volmlib.integration.IntegrationHandshakeRequest; import art.arcane.volmlib.integration.IntegrationHandshakeRequest;
import art.arcane.volmlib.integration.IntegrationHandshakeResponse; import art.arcane.volmlib.integration.IntegrationHandshakeResponse;
@@ -155,12 +153,6 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
IntegrationMetricDescriptor descriptor = IntegrationMetricSchema.descriptor(IntegrationMetricSchema.IRIS_CHUNK_STREAM_MS); IntegrationMetricDescriptor descriptor = IntegrationMetricSchema.descriptor(IntegrationMetricSchema.IRIS_CHUNK_STREAM_MS);
double chunksPerSecond = PregeneratorJob.chunksPerSecond(); double chunksPerSecond = PregeneratorJob.chunksPerSecond();
if (chunksPerSecond <= 0D) {
chunksPerSecond = TurboPregenerator.chunksPerSecond();
}
if (chunksPerSecond <= 0D) {
chunksPerSecond = LazyPregenerator.chunksPerSecond();
}
if (chunksPerSecond > 0D) { if (chunksPerSecond > 0D) {
return IntegrationMetricSample.available(descriptor, 1000D / chunksPerSecond, now); return IntegrationMetricSample.available(descriptor, 1000D / chunksPerSecond, now);
@@ -188,18 +180,6 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
hasAnySource = true; hasAnySource = true;
} }
long turboRemaining = TurboPregenerator.remainingChunks();
if (turboRemaining >= 0L) {
totalQueue += turboRemaining;
hasAnySource = true;
}
long lazyRemaining = LazyPregenerator.remainingChunks();
if (lazyRemaining >= 0L) {
totalQueue += lazyRemaining;
hasAnySource = true;
}
IrisEngineSVC engineService = Iris.service(IrisEngineSVC.class); IrisEngineSVC engineService = Iris.service(IrisEngineSVC.class);
if (engineService != null) { if (engineService != null) {
totalQueue += Math.max(0, engineService.getQueuedTectonicPlateCount()); totalQueue += Math.max(0, engineService.getQueuedTectonicPlateCount());
@@ -434,7 +434,7 @@ public class StudioSVC implements IrisService {
} }
if (activeProject == null) { if (activeProject == null) {
return CompletableFuture.completedFuture(new art.arcane.iris.core.runtime.StudioOpenCoordinator.StudioCloseResult(null, true, true, false, null, null)); return CompletableFuture.completedFuture(new art.arcane.iris.core.runtime.StudioOpenCoordinator.StudioCloseResult(null, true, true, false, null));
} }
Iris.debug("Closing Active Project"); Iris.debug("Closing Active Project");
@@ -87,7 +87,7 @@ public class IrisDecorantActuator extends EngineAssignedActuator<BlockData> {
continue; continue;
} }
if (height < getDimension().getFluidHeight()) { if (height < getDimension().getFluidHeight() && PREDICATE_SOLID.test(output.get(i, height, j))) {
getSeaSurfaceDecorator().decorate(i, j, getSeaSurfaceDecorator().decorate(i, j,
realX, Math.round(i + 1), Math.round(x + i - 1), realX, Math.round(i + 1), Math.round(x + i - 1),
realZ, Math.round(z + j + 1), Math.round(z + j - 1), realZ, Math.round(z + j + 1), Math.round(z + j - 1),
@@ -86,6 +86,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<BlockData>
int zf, realX, realZ, hf, he; int zf, realX, realZ, hf, he;
IrisBiome biome; IrisBiome biome;
IrisRegion region; IrisRegion region;
int clampedFluidHeight = Math.min(h.getHeight(), getDimension().getFluidHeight());
for (zf = 0; zf < h.getDepth(); zf++) { for (zf = 0; zf < h.getDepth(); zf++) {
realX = xf + x; realX = xf + x;
@@ -93,7 +94,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<BlockData>
biome = context.getBiome().get(xf, zf); biome = context.getBiome().get(xf, zf);
region = context.getRegion().get(xf, zf); region = context.getRegion().get(xf, zf);
he = Math.min(h.getHeight(), context.getRoundedHeight(xf, zf)); he = Math.min(h.getHeight(), context.getRoundedHeight(xf, zf));
hf = Math.round(Math.max(Math.min(h.getHeight(), getDimension().getFluidHeight()), he)); hf = Math.max(clampedFluidHeight, he);
if (hf < 0) { if (hf < 0) {
continue; continue;
@@ -109,7 +110,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<BlockData>
if (i == 0) { if (i == 0) {
if (getDimension().isBedrock()) { if (getDimension().isBedrock()) {
h.set(xf, i, zf, BEDROCK); h.setRaw(xf, i, zf, BEDROCK);
lastBedrock = i; lastBedrock = i;
continue; continue;
} }
@@ -119,7 +120,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<BlockData>
ore = ore == null ? region.generateOres(realX, i, realZ, rng, getData(), true) : ore; ore = ore == null ? region.generateOres(realX, i, realZ, rng, getData(), true) : ore;
ore = ore == null ? getDimension().generateOres(realX, i, realZ, rng, getData(), true) : ore; ore = ore == null ? getDimension().generateOres(realX, i, realZ, rng, getData(), true) : ore;
if (ore != null) { if (ore != null) {
h.set(xf, i, zf, ore); h.setRaw(xf, i, zf, ore);
continue; continue;
} }
@@ -131,11 +132,11 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<BlockData>
} }
if (fblocks.hasIndex(fdepth)) { if (fblocks.hasIndex(fdepth)) {
h.set(xf, i, zf, fblocks.get(fdepth)); h.setRaw(xf, i, zf, fblocks.get(fdepth));
continue; continue;
} }
h.set(xf, i, zf, context.getFluid().get(xf, zf)); h.setRaw(xf, i, zf, context.getFluid().get(xf, zf));
continue; continue;
} }
@@ -151,7 +152,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<BlockData>
if (blocks.hasIndex(depth)) { if (blocks.hasIndex(depth)) {
h.set(xf, i, zf, blocks.get(depth)); h.setRaw(xf, i, zf, blocks.get(depth));
continue; continue;
} }
@@ -160,9 +161,9 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<BlockData>
ore = ore == null ? getDimension().generateOres(realX, i, realZ, rng, getData(), false) : ore; ore = ore == null ? getDimension().generateOres(realX, i, realZ, rng, getData(), false) : ore;
if (ore != null) { if (ore != null) {
h.set(xf, i, zf, ore); h.setRaw(xf, i, zf, ore);
} else { } else {
h.set(xf, i, zf, context.getRock().get(xf, zf)); h.setRaw(xf, i, zf, context.getRock().get(xf, zf));
} }
} }
} }
@@ -178,6 +179,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<BlockData>
IrisComplex complex = getComplex(); IrisComplex complex = getComplex();
RNG localRng = rng; RNG localRng = rng;
int fluidHeight = dimension.getFluidHeight(); int fluidHeight = dimension.getFluidHeight();
int clampedFluidHeight = Math.min(chunkHeight, fluidHeight);
boolean bedrockEnabled = dimension.isBedrock(); boolean bedrockEnabled = dimension.isBedrock();
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome(); ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
ChunkedDataCache<IrisRegion> regionCache = context.getRegion(); ChunkedDataCache<IrisRegion> regionCache = context.getRegion();
@@ -190,7 +192,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<BlockData>
IrisBiome biome = biomeCache.get(xf, zf); IrisBiome biome = biomeCache.get(xf, zf);
IrisRegion region = regionCache.get(xf, zf); IrisRegion region = regionCache.get(xf, zf);
int he = Math.min(chunkHeight, context.getRoundedHeight(xf, zf)); int he = Math.min(chunkHeight, context.getRoundedHeight(xf, zf));
int hf = Math.round(Math.max(Math.min(chunkHeight, fluidHeight), he)); int hf = Math.max(clampedFluidHeight, he);
if (hf < 0) { if (hf < 0) {
continue; continue;
} }
@@ -205,7 +207,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<BlockData>
for (int i = topY; i >= 0; i--) { for (int i = topY; i >= 0; i--) {
if (i == 0 && bedrockEnabled) { if (i == 0 && bedrockEnabled) {
h.set(xf, i, zf, BEDROCK); h.setRaw(xf, i, zf, BEDROCK);
lastBedrock = i; lastBedrock = i;
continue; continue;
} }
@@ -217,7 +219,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<BlockData>
ore = ore == null ? dimension.generateSurfaceOres(realX, i, realZ, localRng, data) : ore; ore = ore == null ? dimension.generateSurfaceOres(realX, i, realZ, localRng, data) : ore;
} }
if (ore != null) { if (ore != null) {
h.set(xf, i, zf, ore); h.setRaw(xf, i, zf, ore);
continue; continue;
} }
@@ -228,9 +230,9 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<BlockData>
} }
if (fblocks.hasIndex(fdepth)) { if (fblocks.hasIndex(fdepth)) {
h.set(xf, i, zf, fblocks.get(fdepth)); h.setRaw(xf, i, zf, fblocks.get(fdepth));
} else { } else {
h.set(xf, i, zf, fluid); h.setRaw(xf, i, zf, fluid);
} }
continue; continue;
} }
@@ -242,7 +244,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<BlockData>
} }
if (blocks.hasIndex(depth)) { if (blocks.hasIndex(depth)) {
h.set(xf, i, zf, blocks.get(depth)); h.setRaw(xf, i, zf, blocks.get(depth));
continue; continue;
} }
@@ -253,9 +255,9 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<BlockData>
} }
if (ore != null) { if (ore != null) {
h.set(xf, i, zf, ore); h.setRaw(xf, i, zf, ore);
} else { } else {
h.set(xf, i, zf, rock); h.setRaw(xf, i, zf, rock);
} }
} }
} }
@@ -50,7 +50,7 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
RNG rng = getRNG(realX, realZ); RNG rng = getRNG(realX, realZ);
IrisDecorator decorator = getDecorator(rng, biome, realX, realZ); IrisDecorator decorator = getDecorator(rng, biome, realX, realZ);
bdx = data.get(x, height, z); bdx = data.get(x, height, z);
boolean underwater = height < getDimension().getFluidHeight(); boolean underwater = height < getDimension().getFluidHeight() && biome.getInferredType() != InferredType.CAVE;
if (decorator != null) { if (decorator != null) {
if (!decorator.isForcePlace() && !decorator.getSlopeCondition().isDefault() if (!decorator.isForcePlace() && !decorator.getSlopeCondition().isDefault()
@@ -23,12 +23,12 @@ import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.events.IrisLootEvent; import art.arcane.iris.core.events.IrisLootEvent;
import art.arcane.iris.core.gui.components.RenderType; import art.arcane.iris.core.gui.components.RenderType;
import art.arcane.iris.core.gui.components.Renderer; import art.arcane.iris.core.gui.components.Renderer;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.link.Identifier; import art.arcane.iris.core.link.Identifier;
import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant; import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.core.nms.container.BlockPos; import art.arcane.iris.core.nms.container.BlockPos;
import art.arcane.iris.core.nms.container.Pair; import art.arcane.iris.core.nms.container.Pair;
import art.arcane.iris.core.pregenerator.ChunkUpdater;
import art.arcane.iris.core.service.ExternalDataSVC; import art.arcane.iris.core.service.ExternalDataSVC;
import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.IrisComplex; import art.arcane.iris.engine.IrisComplex;
@@ -333,16 +333,14 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
if (c.getWorld().isChunkLoaded(c.getX() + x, c.getZ() + z)) if (c.getWorld().isChunkLoaded(c.getX() + x, c.getZ() + z))
continue; continue;
var msg = "Chunk %s, %s [%s, %s] is not loaded".formatted(c.getX() + x, c.getZ() + z, x, z); var msg = "Chunk %s, %s [%s, %s] is not loaded".formatted(c.getX() + x, c.getZ() + z, x, z);
if (W.getStack().getCallerClass().equals(ChunkUpdater.class)) Iris.warn(msg); Iris.debug(msg);
else Iris.debug(msg);
return; return;
} }
} }
var mantle = getMantle().getMantle(); var mantle = getMantle().getMantle();
if (!mantle.isLoaded(c)) { if (!mantle.isLoaded(c)) {
var msg = "Mantle Chunk " + c.getX() + "," + c.getZ() + " is not loaded"; var msg = "Mantle Chunk " + c.getX() + "," + c.getZ() + " is not loaded";
if (W.getStack().getCallerClass().equals(ChunkUpdater.class)) Iris.warn(msg); Iris.debug(msg);
else Iris.debug(msg);
return; return;
} }
@@ -1081,8 +1079,11 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
default void cleanupMantleChunk(int x, int z) { default void cleanupMantleChunk(int x, int z) {
World world = getWorld().realWorld(); World world = getWorld().realWorld();
if (world != null && IrisToolbelt.isWorldMaintenanceActive(world)) { if (world != null && IrisToolbelt.isWorldMaintenanceActive(world)) {
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
if (pregeneratorJob == null || !pregeneratorJob.targetsWorld(world)) {
return; return;
} }
}
if (IrisSettings.get().getPerformance().isTrimMantleInStudio() || !isStudio()) { if (IrisSettings.get().getPerformance().isTrimMantleInStudio() || !isStudio()) {
getMantle().cleanupChunk(x, z); getMantle().cleanupChunk(x, z);
} }
@@ -21,6 +21,7 @@ package art.arcane.iris.engine.mantle;
import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.container.Pair; import art.arcane.iris.core.nms.container.Pair;
import art.arcane.iris.core.link.Identifier;
import art.arcane.iris.engine.IrisComplex; import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineTarget; import art.arcane.iris.engine.framework.EngineTarget;
@@ -32,6 +33,7 @@ import art.arcane.iris.util.common.data.B;
import art.arcane.volmlib.util.documentation.BlockCoordinates; import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.documentation.ChunkCoordinates; import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.iris.util.project.hunk.Hunk; import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.iris.util.project.matter.TileWrapper;
import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.mantle.flag.MantleFlag; import art.arcane.volmlib.util.mantle.flag.MantleFlag;
@@ -243,13 +245,39 @@ public interface EngineMantle extends MatterGenerator {
default void cleanupChunk(int x, int z) { default void cleanupChunk(int x, int z) {
if (!isCovered(x, z)) return; if (!isCovered(x, z)) return;
doCleanupChunk(x, z);
}
default void forceCleanupChunk(int x, int z) {
MantleChunk<Matter> chunk = getMantle().getChunk(x, z).use(); MantleChunk<Matter> chunk = getMantle().getChunk(x, z).use();
try { try {
chunk.raiseFlagUnchecked(MantleFlag.CLEANED, () -> { chunk.raiseFlagUnchecked(MantleFlag.CLEANED, () -> {
chunk.deleteSlices(BlockData.class); chunk.deleteSlices(BlockData.class);
chunk.deleteSlices(String.class); chunk.deleteSlices(String.class);
chunk.deleteSlices(TileWrapper.class);
chunk.deleteSlices(Identifier.class);
chunk.deleteSlices(UpdateMatter.class);
chunk.deleteSlices(MatterCavern.class); chunk.deleteSlices(MatterCavern.class);
chunk.deleteSlices(MatterFluidBody.class); chunk.deleteSlices(MatterFluidBody.class);
chunk.deleteSlices(MatterMarker.class);
chunk.trimSlices();
});
} finally {
chunk.release();
}
}
private void doCleanupChunk(int x, int z) {
MantleChunk<Matter> chunk = getMantle().getChunk(x, z).use();
try {
chunk.raiseFlagUnchecked(MantleFlag.CLEANED, () -> {
chunk.deleteSlices(BlockData.class);
chunk.deleteSlices(TileWrapper.class);
chunk.deleteSlices(Identifier.class);
chunk.deleteSlices(UpdateMatter.class);
chunk.deleteSlices(MatterCavern.class);
chunk.deleteSlices(MatterFluidBody.class);
chunk.trimSlices();
}); });
} finally { } finally {
chunk.release(); chunk.release();
@@ -62,6 +62,12 @@ public interface MantleComponent extends Comparable<MantleComponent> {
MantleFlag getFlag(); MantleFlag getFlag();
default MantleFlag[] getPrerequisiteFlags() {
return EMPTY_PREREQUISITES;
}
MantleFlag[] EMPTY_PREREQUISITES = new MantleFlag[0];
boolean isEnabled(); boolean isEnabled();
void setEnabled(boolean b); void setEnabled(boolean b);
@@ -142,11 +142,29 @@ public interface MatterGenerator {
continue; continue;
} }
int componentPassRadius = Math.ceilDiv(component.getRadius(), 16); int componentRadius = component.getRadius();
if (componentRadius > 0) {
int componentPassRadius = Math.ceilDiv(componentRadius, 16);
if (Math.abs(i) > componentPassRadius || Math.abs(j) > componentPassRadius) { if (Math.abs(i) > componentPassRadius || Math.abs(j) > componentPassRadius) {
partialChunks.add(passKey); partialChunks.add(passKey);
continue; continue;
} }
}
MantleFlag[] prerequisites = component.getPrerequisiteFlags();
if (prerequisites.length > 0) {
boolean prerequisitesMet = true;
for (MantleFlag prereq : prerequisites) {
if (!chunk.isFlagged(prereq)) {
prerequisitesMet = false;
break;
}
}
if (!prerequisitesMet) {
partialChunks.add(passKey);
continue;
}
}
if (forceRegen && chunk.isFlagged(component.getFlag())) { if (forceRegen && chunk.isFlagged(component.getFlag())) {
chunk.flag(component.getFlag(), false); chunk.flag(component.getFlag(), false);
@@ -41,8 +41,13 @@ public class IrisCaveCarver3D {
private static final byte LIQUID_AIR = 0; private static final byte LIQUID_AIR = 0;
private static final byte LIQUID_LAVA = 2; private static final byte LIQUID_LAVA = 2;
private static final byte LIQUID_FORCED_AIR = 3; private static final byte LIQUID_FORCED_AIR = 3;
private static final int ADAPTIVE_MIN_PLANE_COLUMNS = 48; private static final int ADAPTIVE_MIN_PLANE_COLUMNS = 32;
private static final int ADAPTIVE_DEEP_MIN_PLANE_COLUMNS = 64;
private static final int ADAPTIVE_DEEP_SAMPLE_STEP = 8;
private static final int ADAPTIVE_DEEP_SURFACE_MARGIN = 12;
private static final int ADAPTIVE_DEEP_NEAR_SURFACE_DIVISOR = 4;
private static final double ADAPTIVE_LOCAL_RANGE_SCALE = 0.25D; private static final double ADAPTIVE_LOCAL_RANGE_SCALE = 0.25D;
private static final double ADAPTIVE_DEEP_MARGIN_BOOST = 0.015D;
private static final ThreadLocal<Scratch> SCRATCH = ThreadLocal.withInitial(Scratch::new); private static final ThreadLocal<Scratch> SCRATCH = ThreadLocal.withInitial(Scratch::new);
private final Engine engine; private final Engine engine;
@@ -609,6 +614,19 @@ public class IrisCaveCarver3D {
continue; continue;
} }
int effectiveAdaptiveSampleStep = resolveAdaptivePlaneSampleStep(
y,
planeColumnIndices,
planeCount,
adaptiveSampleStep,
surfaceBreakColumn,
surfaceBreakFloorY
);
double effectiveAdaptiveThresholdMargin = resolveAdaptivePlaneThresholdMargin(
adaptiveThresholdMargin,
adaptiveSampleStep,
effectiveAdaptiveSampleStep
);
classifyDensityPlaneAdaptive( classifyDensityPlaneAdaptive(
x0, x0,
z0, z0,
@@ -617,8 +635,8 @@ public class IrisCaveCarver3D {
planeThresholdLimit, planeThresholdLimit,
planeCount, planeCount,
planeCarve, planeCarve,
adaptiveSampleStep, effectiveAdaptiveSampleStep,
adaptiveThresholdMargin effectiveAdaptiveThresholdMargin
); );
int fadeIndex = y - minY; int fadeIndex = y - minY;
int localY = y & 15; int localY = y & 15;
@@ -660,6 +678,45 @@ public class IrisCaveCarver3D {
return carved; return carved;
} }
private int resolveAdaptivePlaneSampleStep(
int y,
int[] planeColumnIndices,
int planeCount,
int adaptiveSampleStep,
boolean[] surfaceBreakColumn,
int[] surfaceBreakFloorY
) {
if (adaptiveSampleStep >= ADAPTIVE_DEEP_SAMPLE_STEP || planeCount < ADAPTIVE_DEEP_MIN_PLANE_COLUMNS) {
return adaptiveSampleStep;
}
int nearSurfaceColumns = 0;
int allowedNearSurfaceColumns = Math.max(8, planeCount / ADAPTIVE_DEEP_NEAR_SURFACE_DIVISOR);
for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) {
int columnIndex = planeColumnIndices[planeIndex];
if (surfaceBreakColumn[columnIndex] || y > (surfaceBreakFloorY[columnIndex] - ADAPTIVE_DEEP_SURFACE_MARGIN)) {
nearSurfaceColumns++;
if (nearSurfaceColumns > allowedNearSurfaceColumns) {
return adaptiveSampleStep;
}
}
}
return ADAPTIVE_DEEP_SAMPLE_STEP;
}
private double resolveAdaptivePlaneThresholdMargin(
double adaptiveThresholdMargin,
int adaptiveSampleStep,
int effectiveAdaptiveSampleStep
) {
if (effectiveAdaptiveSampleStep <= adaptiveSampleStep) {
return adaptiveThresholdMargin;
}
return adaptiveThresholdMargin + ((effectiveAdaptiveSampleStep - adaptiveSampleStep) * ADAPTIVE_DEEP_MARGIN_BOOST);
}
private int carvePassLattice( private int carvePassLattice(
MantleChunk<Matter> chunk, MantleChunk<Matter> chunk,
int x0, int x0,
@@ -1285,6 +1342,10 @@ public class IrisCaveCarver3D {
planeCarve[planeIndex] = false; planeCarve[planeIndex] = false;
continue; continue;
} }
if (adaptiveSampleStep >= ADAPTIVE_DEEP_SAMPLE_STEP) {
planeCarve[planeIndex] = predictedDensity <= threshold;
continue;
}
planeCarve[planeIndex] = classifyDensityPointNoWarpNoModules(x0 + localX, y, z0 + localZ, planeThresholdLimit[planeIndex]); planeCarve[planeIndex] = classifyDensityPointNoWarpNoModules(x0 + localX, y, z0 + localZ, planeThresholdLimit[planeIndex]);
} }
@@ -1353,6 +1414,10 @@ public class IrisCaveCarver3D {
planeCarve[planeIndex] = false; planeCarve[planeIndex] = false;
continue; continue;
} }
if (adaptiveSampleStep >= ADAPTIVE_DEEP_SAMPLE_STEP) {
planeCarve[planeIndex] = predictedDensity <= threshold;
continue;
}
planeCarve[planeIndex] = classifyDensityPointNoWarpModules( planeCarve[planeIndex] = classifyDensityPointNoWarpModules(
x0 + localX, x0 + localX,
@@ -1414,6 +1479,10 @@ public class IrisCaveCarver3D {
planeCarve[planeIndex] = false; planeCarve[planeIndex] = false;
continue; continue;
} }
if (adaptiveSampleStep >= ADAPTIVE_DEEP_SAMPLE_STEP) {
planeCarve[planeIndex] = predictedDensity <= threshold;
continue;
}
planeCarve[planeIndex] = classifyDensityPointWarpOnly(x0 + localX, y, z0 + localZ, planeThresholdLimit[planeIndex]); planeCarve[planeIndex] = classifyDensityPointWarpOnly(x0 + localX, y, z0 + localZ, planeThresholdLimit[planeIndex]);
} }
@@ -1547,6 +1616,10 @@ public class IrisCaveCarver3D {
planeCarve[planeIndex] = false; planeCarve[planeIndex] = false;
continue; continue;
} }
if (adaptiveSampleStep >= ADAPTIVE_DEEP_SAMPLE_STEP) {
planeCarve[planeIndex] = predictedDensity <= threshold;
continue;
}
planeCarve[planeIndex] = classifyDensityPointWarpModules( planeCarve[planeIndex] = classifyDensityPointWarpModules(
x0 + localX, x0 + localX,
@@ -30,17 +30,22 @@ import art.arcane.iris.engine.object.*;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.collection.KSet; import art.arcane.volmlib.util.collection.KSet;
import art.arcane.iris.util.project.context.ChunkedDoubleDataCache;
import art.arcane.iris.util.project.context.ChunkContext; import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.common.data.B; import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.project.stream.ProceduralStream; import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.volmlib.util.documentation.BlockCoordinates; import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.documentation.ChunkCoordinates; import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.format.Form; import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag; import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterStructurePOI; import art.arcane.volmlib.util.matter.MatterStructurePOI;
import art.arcane.iris.util.project.noise.CNG; import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.project.noise.NoiseType; import art.arcane.iris.util.project.noise.NoiseType;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import org.bukkit.util.BlockVector; import org.bukkit.util.BlockVector;
import java.io.IOException; import java.io.IOException;
@@ -55,8 +60,8 @@ import java.util.concurrent.atomic.AtomicLong;
@ComponentFlag(ReservedFlag.OBJECT) @ComponentFlag(ReservedFlag.OBJECT)
public class MantleObjectComponent extends IrisMantleComponent { public class MantleObjectComponent extends IrisMantleComponent {
private static final long CAVE_REJECT_LOG_THROTTLE_MS = 5000L; private static final long CAVE_REJECT_LOG_THROTTLE_MS = 5000L;
private static final int SURFACE_HEIGHT_CHUNK_FILL_THRESHOLD = 128;
private static final Map<String, CaveRejectLogState> CAVE_REJECT_LOG_STATE = new ConcurrentHashMap<>(); private static final Map<String, CaveRejectLogState> CAVE_REJECT_LOG_STATE = new ConcurrentHashMap<>();
public MantleObjectComponent(EngineMantle engineMantle) { public MantleObjectComponent(EngineMantle engineMantle) {
super(engineMantle, ReservedFlag.OBJECT, 1); super(engineMantle, ReservedFlag.OBJECT, 1);
} }
@@ -73,6 +78,25 @@ public class MantleObjectComponent extends IrisMantleComponent {
int surfaceY = getEngineMantle().getEngine().getHeight(xxx, zzz, true); int surfaceY = getEngineMantle().getEngine().getHeight(xxx, zzz, true);
IrisBiome caveBiome = resolveCaveObjectBiome(xxx, zzz, surfaceY, surfaceBiome); IrisBiome caveBiome = resolveCaveObjectBiome(xxx, zzz, surfaceY, surfaceBiome);
SurfaceHeightLookup surfaceHeightLookup = new SurfaceHeightLookup(context); SurfaceHeightLookup surfaceHeightLookup = new SurfaceHeightLookup(context);
if (IrisSettings.get().getGeneral().isDebug() && (x & 31) == 0 && (z & 31) == 0) {
int carvedBlocks = 0;
int minY = 1;
int maxY = Math.min(getEngineMantle().getEngine().getHeight() - 1, surfaceY - 14);
for (int sy = minY; sy < maxY; sy++) {
if (writer.isCarved(8 + (x << 4), sy, 8 + (z << 4))) {
carvedBlocks++;
}
}
Iris.info("Cave object diag: chunk=" + x + "," + z
+ " surfaceBiome=" + surfaceBiome.getLoadKey()
+ " caveBiome=" + caveBiome.getLoadKey()
+ " surfaceY=" + surfaceY
+ " maxAnchorY=" + maxY
+ " carvedAtCenter=" + carvedBlocks
+ " biomeCarvingObjects=" + caveBiome.getCarvingObjects().size()
+ " regionCarvingObjects=" + region.getCarvingObjects().size()
+ " sameBiome=" + (caveBiome == surfaceBiome || caveBiome.getLoadKey().equals(surfaceBiome.getLoadKey())));
}
if (traceRegen) { if (traceRegen) {
Iris.info("Regen object layer start: chunk=" + x + "," + z Iris.info("Regen object layer start: chunk=" + x + "," + z
+ " surfaceBiome=" + surfaceBiome.getLoadKey() + " surfaceBiome=" + surfaceBiome.getLoadKey()
@@ -194,7 +218,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
} }
for (IrisObjectPlacement i : caveBiome.getCarvingObjects()) { for (IrisObjectPlacement i : caveBiome.getCarvingObjects()) {
if (!i.getCarvingSupport().equals(CarvingMode.CARVING_ONLY)) { if (!i.getCarvingSupport().supportsCarving()) {
continue; continue;
} }
biomeCaveChecked++; biomeCaveChecked++;
@@ -259,7 +283,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
} }
for (IrisObjectPlacement i : region.getCarvingObjects()) { for (IrisObjectPlacement i : region.getCarvingObjects()) {
if (!i.getCarvingSupport().equals(CarvingMode.CARVING_ONLY)) { if (!i.getCarvingSupport().supportsCarving()) {
continue; continue;
} }
regionCaveChecked++; regionCaveChecked++;
@@ -316,7 +340,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
int x, int x,
int z, int z,
IrisObjectPlacement objectPlacement, IrisObjectPlacement objectPlacement,
int surfaceObjectExclusionDepth, int surfaceObjectExclusionBaseDepth,
IrisComplex complex, IrisComplex complex,
boolean traceRegen, boolean traceRegen,
int chunkX, int chunkX,
@@ -347,6 +371,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
} }
int xx = rng.i(x, x + 15); int xx = rng.i(x, x + 15);
int zz = rng.i(z, z + 15); int zz = rng.i(z, z + 15);
int surfaceObjectExclusionDepth = resolveSurfaceObjectExclusionDepth(surfaceObjectExclusionBaseDepth, v);
int surfaceObjectExclusionRadius = resolveSurfaceObjectExclusionRadius(v); int surfaceObjectExclusionRadius = resolveSurfaceObjectExclusionRadius(v);
if (surfaceObjectExclusionDepth > 0 && hasSurfaceCarveExposure(writer, surfaceHeightLookup, xx, zz, surfaceObjectExclusionDepth, surfaceObjectExclusionRadius)) { if (surfaceObjectExclusionDepth > 0 && hasSurfaceCarveExposure(writer, surfaceHeightLookup, xx, zz, surfaceObjectExclusionDepth, surfaceObjectExclusionRadius)) {
rejected++; rejected++;
@@ -491,7 +516,15 @@ public class MantleObjectComponent extends IrisMantleComponent {
} }
int id = rng.i(0, Integer.MAX_VALUE); int id = rng.i(0, Integer.MAX_VALUE);
IrisObjectPlacement effectivePlacement = resolveEffectivePlacement(objectPlacement, object); IrisObjectPlacement resolvedPlacement = resolveEffectivePlacement(objectPlacement, object);
if (resolvedPlacement.getMode() == ObjectPlaceMode.CENTER_HEIGHT && caveProfile != null) {
ObjectPlaceMode profileMode = caveProfile.getDefaultObjectPlaceMode();
if (profileMode != null) {
resolvedPlacement = resolvedPlacement.toPlacement(object.getLoadKey());
resolvedPlacement.setMode(profileMode);
}
}
IrisObjectPlacement effectivePlacement = resolvedPlacement;
AtomicBoolean wrotePlacementData = new AtomicBoolean(false); AtomicBoolean wrotePlacementData = new AtomicBoolean(false);
try { try {
@@ -717,14 +750,17 @@ public class MantleObjectComponent extends IrisMantleComponent {
int surfaceY = getEngineMantle().getEngine().getHeight(x, z); int surfaceY = getEngineMantle().getEngine().getHeight(x, z);
int maxAnchorY = Math.min(height - 1, surfaceY - Math.max(0, objectMinDepthBelowSurface)); int maxAnchorY = Math.min(height - 1, surfaceY - Math.max(0, objectMinDepthBelowSurface));
if (maxAnchorY <= 1) { if (maxAnchorY <= 1) {
logCaveAnchorDiag(writer, x, z, surfaceY, maxAnchorY, height, objectMinDepthBelowSurface, 0, 0);
return anchors; return anchors;
} }
int carvedCount = 0;
for (int y = 1; y < maxAnchorY; y += step) { for (int y = 1; y < maxAnchorY; y += step) {
if (!writer.isCarved(x, y, z)) { if (!writer.isCarved(x, y, z)) {
continue; continue;
} }
carvedCount++;
boolean solidBelow = y <= 0 || !writer.isCarved(x, y - 1, z); boolean solidBelow = y <= 0 || !writer.isCarved(x, y - 1, z);
boolean solidAbove = y >= (height - 1) || !writer.isCarved(x, y + 1, z); boolean solidAbove = y >= (height - 1) || !writer.isCarved(x, y + 1, z);
if (matchesCaveAnchor(anchorMode, solidBelow, solidAbove)) { if (matchesCaveAnchor(anchorMode, solidBelow, solidAbove)) {
@@ -732,9 +768,33 @@ public class MantleObjectComponent extends IrisMantleComponent {
} }
} }
if (anchors.isEmpty()) {
logCaveAnchorDiag(writer, x, z, surfaceY, maxAnchorY, height, objectMinDepthBelowSurface, carvedCount, 0);
}
return anchors; return anchors;
} }
private void logCaveAnchorDiag(MantleWriter writer, int x, int z, int surfaceY, int maxAnchorY, int height, int minDepth, int carvedCount, int anchorCount) {
long now = System.currentTimeMillis();
CaveRejectLogState state = CAVE_REJECT_LOG_STATE.computeIfAbsent("anchor-diag-" + (x >> 4) + "," + (z >> 4), k -> new CaveRejectLogState());
if (now - state.lastLogMs.get() < CAVE_REJECT_LOG_THROTTLE_MS) {
return;
}
state.lastLogMs.set(now);
MantleChunk<Matter> chunk = writer.acquireChunk(x >> 4, z >> 4);
Iris.info("Cave anchor diag: block=" + x + "," + z
+ " chunk=" + (x >> 4) + "," + (z >> 4)
+ " surfaceY=" + surfaceY
+ " maxAnchorY=" + maxAnchorY
+ " worldHeight=" + height
+ " minDepth=" + minDepth
+ " carvedInColumn=" + carvedCount
+ " anchorsFound=" + anchorCount
+ " chunkRef=" + (chunk == null ? "null" : System.identityHashCode(chunk))
+ " writerRef=" + System.identityHashCode(writer));
}
private boolean matchesCaveAnchor(IrisCaveAnchorMode anchorMode, boolean solidBelow, boolean solidAbove) { private boolean matchesCaveAnchor(IrisCaveAnchorMode anchorMode, boolean solidBelow, boolean solidAbove) {
return switch (anchorMode) { return switch (anchorMode) {
case PROFILE_DEFAULT, FLOOR -> solidBelow; case PROFILE_DEFAULT, FLOOR -> solidBelow;
@@ -803,6 +863,16 @@ public class MantleObjectComponent extends IrisMantleComponent {
return Math.max(0, caveProfile.getSurfaceObjectExclusionDepth()); return Math.max(0, caveProfile.getSurfaceObjectExclusionDepth());
} }
private int resolveSurfaceObjectExclusionDepth(int baseDepth, IrisObject object) {
if (object == null) {
return baseDepth;
}
int horizontalReach = resolveSurfaceObjectExclusionRadius(object) + 2;
int verticalReach = Math.max(4, Math.min(16, Math.floorDiv(Math.max(1, object.getH()), 2)));
return Math.max(baseDepth, Math.max(horizontalReach, verticalReach));
}
private int resolveSurfaceObjectExclusionRadius(IrisObject object) { private int resolveSurfaceObjectExclusionRadius(IrisObject object) {
if (object == null) { if (object == null) {
return 1; return 1;
@@ -940,7 +1010,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
} }
for (Map.Entry<IrisObjectScale, KList<String>> entry : scalars.entrySet()) { for (Map.Entry<IrisObjectScale, KList<String>> entry : scalars.entrySet()) {
double ms = entry.getKey().getMaximumScale(); double ms = entry.getKey().getMaxScale();
for (String j : entry.getValue()) { for (String j : entry.getValue()) {
updateRadiusBounds(sizeCache, xg, zg, j, ms); updateRadiusBounds(sizeCache, xg, zg, j, ms);
} }
@@ -993,12 +1063,12 @@ public class MantleObjectComponent extends IrisMantleComponent {
private static final class SurfaceHeightLookup { private static final class SurfaceHeightLookup {
private final ChunkContext context; private final ChunkContext context;
private final ProceduralStream<Double> heightStream; private final ProceduralStream<Double> heightStream;
private final KMap<Long, Integer> columnHeights; private final Long2ObjectOpenHashMap<ForeignChunkHeights> foreignChunkHeights;
private SurfaceHeightLookup(ChunkContext context) { private SurfaceHeightLookup(ChunkContext context) {
this.context = context; this.context = context;
this.heightStream = context.getComplex().getHeightStream(); this.heightStream = context.getComplex().getHeightStream();
this.columnHeights = new KMap<>(); this.foreignChunkHeights = new Long2ObjectOpenHashMap<>();
} }
private int getRoundedHeight(int worldX, int worldZ) { private int getRoundedHeight(int worldX, int worldZ) {
@@ -1008,14 +1078,66 @@ public class MantleObjectComponent extends IrisMantleComponent {
return context.getRoundedHeight(worldX & 15, worldZ & 15); return context.getRoundedHeight(worldX & 15, worldZ & 15);
} }
long columnKey = Cache.key(worldX, worldZ); long chunkKey = Cache.key(chunkBlockX, chunkBlockZ);
Integer columnHeight = columnHeights.get(columnKey); ForeignChunkHeights chunkHeights = foreignChunkHeights.get(chunkKey);
if (columnHeight == null) { if (chunkHeights == null) {
columnHeight = (int) Math.round(heightStream.getDouble(worldX, worldZ)); chunkHeights = new ForeignChunkHeights(heightStream, chunkBlockX, chunkBlockZ);
columnHeights.put(columnKey, columnHeight); foreignChunkHeights.put(chunkKey, chunkHeights);
}
return chunkHeights.getRoundedHeight(worldX, worldZ);
}
} }
return columnHeight; private static final class ForeignChunkHeights {
private final ProceduralStream<Double> heightStream;
private final int chunkBlockX;
private final int chunkBlockZ;
private final Long2IntOpenHashMap sparseColumnHeights;
private int uniqueColumnCount;
private int[] roundedHeights;
private ForeignChunkHeights(ProceduralStream<Double> heightStream, int chunkBlockX, int chunkBlockZ) {
this.heightStream = heightStream;
this.chunkBlockX = chunkBlockX;
this.chunkBlockZ = chunkBlockZ;
this.sparseColumnHeights = new Long2IntOpenHashMap();
this.sparseColumnHeights.defaultReturnValue(Integer.MIN_VALUE);
this.uniqueColumnCount = 0;
}
private int getRoundedHeight(int worldX, int worldZ) {
int[] localRoundedHeights = roundedHeights;
if (localRoundedHeights != null) {
int localX = worldX - chunkBlockX;
int localZ = worldZ - chunkBlockZ;
return localRoundedHeights[(localZ << 4) + localX];
}
long columnKey = Cache.key(worldX, worldZ);
int cachedHeight = sparseColumnHeights.get(columnKey);
if (cachedHeight != Integer.MIN_VALUE) {
return cachedHeight;
}
int roundedHeight = (int) Math.round(heightStream.getDouble(worldX, worldZ));
sparseColumnHeights.put(columnKey, roundedHeight);
uniqueColumnCount++;
if (uniqueColumnCount >= SURFACE_HEIGHT_CHUNK_FILL_THRESHOLD) {
promoteToChunkCache();
}
return roundedHeight;
}
private void promoteToChunkCache() {
if (roundedHeights != null) {
return;
}
int[] filledHeights = new int[256];
new ChunkedDoubleDataCache(heightStream, chunkBlockX, chunkBlockZ, true).fillRounded(filledHeights);
roundedHeights = filledHeights;
sparseColumnHeights.clear();
} }
} }
} }
@@ -97,7 +97,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
int rx = xx & 15; int rx = xx & 15;
int rz = zz & 15; int rz = zz & 15;
int columnIndex = PowerOfTwoCoordinates.packLocal16(rx, rz); int columnIndex = PowerOfTwoCoordinates.packLocal16(rx, rz);
BlockData current = output.get(rx, yy, rz); BlockData current = output.getRaw(rx, yy, rz);
if (B.isFluid(current)) { if (B.isFluid(current)) {
return; return;
@@ -126,15 +126,15 @@ public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
} }
if (c.isWater()) { if (c.isWater()) {
output.set(rx, yy, rz, context.getFluid().get(rx, rz)); output.setRaw(rx, yy, rz, context.getFluid().get(rx, rz));
} else if (c.isLava()) { } else if (c.isLava()) {
output.set(rx, yy, rz, LAVA); output.setRaw(rx, yy, rz, LAVA);
} else if (c.getLiquid() == 3) { } else if (c.getLiquid() == 3) {
output.set(rx, yy, rz, AIR); output.setRaw(rx, yy, rz, AIR);
} else if (getEngine().getDimension().getCaveLavaHeight() > yy) { } else if (getEngine().getDimension().getCaveLavaHeight() > yy) {
output.set(rx, yy, rz, LAVA); output.setRaw(rx, yy, rz, LAVA);
} else { } else {
output.set(rx, yy, rz, AIR); output.setRaw(rx, yy, rz, AIR);
} }
}); });
getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds()); getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds());
@@ -154,8 +154,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
BlockData data = biome.getWall().get(rng, worldX, yy, worldZ, getData()); BlockData data = biome.getWall().get(rng, worldX, yy, worldZ, getData());
int columnIndex = PowerOfTwoCoordinates.packLocal16(rx, rz); int columnIndex = PowerOfTwoCoordinates.packLocal16(rx, rz);
if (data != null && B.isSolid(output.get(rx, yy, rz)) && yy <= surfaceHeights[columnIndex]) { if (data != null && B.isSolid(output.getRaw(rx, yy, rz)) && yy <= surfaceHeights[columnIndex]) {
output.set(rx, yy, rz, data); output.setRaw(rx, yy, rz, data);
} }
} }
}); });
@@ -231,11 +231,11 @@ public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
String customBiome = ""; String customBiome = "";
if (B.isDecorant(output.getClosest(rx, zone.ceiling + 1, rz))) { if (B.isDecorant(output.getClosest(rx, zone.ceiling + 1, rz))) {
output.set(rx, zone.ceiling + 1, rz, AIR); output.setRaw(rx, zone.ceiling + 1, rz, AIR);
} }
if (B.isDecorant(output.get(rx, zone.ceiling, rz))) { if (B.isDecorant(output.getRaw(rx, zone.ceiling, rz))) {
output.set(rx, zone.ceiling, rz, AIR); output.setRaw(rx, zone.ceiling, rz, AIR);
} }
if (M.r(1D / 16D)) { if (M.r(1D / 16D)) {
@@ -275,18 +275,18 @@ public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
int y = zone.floor - i - 1; int y = zone.floor - i - 1;
BlockData b = blocks.get(i); BlockData b = blocks.get(i);
BlockData down = output.get(rx, y, rz); BlockData down = output.getRaw(rx, y, rz);
if (!B.isSolid(down)) { if (!B.isSolid(down)) {
continue; continue;
} }
if (B.isOre(down)) { if (B.isOre(down)) {
output.set(rx, y, rz, B.toDeepSlateOre(down, b)); output.setRaw(rx, y, rz, B.toDeepSlateOre(down, b));
continue; continue;
} }
output.set(rx, y, rz, blocks.get(i)); output.setRaw(rx, y, rz, blocks.get(i));
} }
blocks = biome.generateCeilingLayers(getDimension(), xx, zz, rng, 3, zone.ceiling, getData(), getComplex()); blocks = biome.generateCeilingLayers(getDimension(), xx, zz, rng, 3, zone.ceiling, getData(), getComplex());
@@ -298,25 +298,25 @@ public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
} }
BlockData b = blocks.get(i); BlockData b = blocks.get(i);
BlockData up = output.get(rx, zone.ceiling + i + 1, rz); BlockData up = output.getRaw(rx, zone.ceiling + i + 1, rz);
if (!B.isSolid(up)) { if (!B.isSolid(up)) {
continue; continue;
} }
if (B.isOre(up)) { if (B.isOre(up)) {
output.set(rx, zone.ceiling + i + 1, rz, B.toDeepSlateOre(up, b)); output.setRaw(rx, zone.ceiling + i + 1, rz, B.toDeepSlateOre(up, b));
continue; continue;
} }
output.set(rx, zone.ceiling + i + 1, rz, b); output.setRaw(rx, zone.ceiling + i + 1, rz, b);
} }
} }
for (IrisDecorator i : biome.getDecorators()) { for (IrisDecorator decorator : biome.getDecorators()) {
if (i.getPartOf().equals(IrisDecorationPart.NONE) && B.isSolid(output.get(rx, zone.getFloor() - 1, rz))) { if (decorator.getPartOf().equals(IrisDecorationPart.NONE) && B.isSolid(output.getRaw(rx, zone.getFloor() - 1, rz))) {
decorant.getSurfaceDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, biome, zone.getFloor() - 1, zone.airThickness()); decorant.getSurfaceDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, biome, zone.getFloor() - 1, zone.airThickness());
} else if (i.getPartOf().equals(IrisDecorationPart.CEILING) && B.isSolid(output.get(rx, zone.getCeiling() + 1, rz))) { } else if (decorator.getPartOf().equals(IrisDecorationPart.CEILING) && B.isSolid(output.getRaw(rx, zone.getCeiling() + 1, rz))) {
decorant.getCeilingDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, biome, zone.getCeiling(), zone.airThickness()); decorant.getCeilingDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, biome, zone.getCeiling(), zone.airThickness());
} }
} }
@@ -134,6 +134,9 @@ public class IrisCaveProfile {
@Desc("Default cave anchor mode for cave-only object placement.") @Desc("Default cave anchor mode for cave-only object placement.")
private IrisCaveAnchorMode defaultObjectAnchor = IrisCaveAnchorMode.FLOOR; private IrisCaveAnchorMode defaultObjectAnchor = IrisCaveAnchorMode.FLOOR;
@Desc("Default placement mode for cave objects. Stilt modes tile the object base block down to the cave floor surface. FAST_MIN_STILT is recommended for cave objects to prevent floating.")
private ObjectPlaceMode defaultObjectPlaceMode = null;
@MinNumber(1) @MinNumber(1)
@MaxNumber(8) @MaxNumber(8)
@Desc("Vertical scan step used while searching cave anchors.") @Desc("Vertical scan step used while searching cave anchors.")
@@ -14,17 +14,18 @@ import java.util.Map;
public final class IrisDimensionCarvingResolver { public final class IrisDimensionCarvingResolver {
private static final int MAX_CHILD_DEPTH = 32; private static final int MAX_CHILD_DEPTH = 32;
private static final long CHILD_SEED_SALT = 0x9E3779B97F4A7C15L; private static final long CHILD_SEED_SALT = 0x9E3779B97F4A7C15L;
private static final ThreadLocal<State> THREAD_STATE = ThreadLocal.withInitial(State::new);
private IrisDimensionCarvingResolver() { private IrisDimensionCarvingResolver() {
} }
public static IrisDimensionCarvingEntry resolveRootEntry(Engine engine, int worldY) { public static IrisDimensionCarvingEntry resolveRootEntry(Engine engine, int worldY) {
return resolveRootEntry(engine, worldY, new State()); return resolveRootEntry(engine, worldY, THREAD_STATE.get());
} }
public static IrisDimensionCarvingEntry resolveRootEntry(Engine engine, int worldY, State state) { public static IrisDimensionCarvingEntry resolveRootEntry(Engine engine, int worldY, State state) {
State resolvedState = state == null ? new State() : state; State resolvedState = state == null ? THREAD_STATE.get() : state;
if (resolvedState.rootEntriesByWorldY.containsKey(worldY)) { if (resolvedState.rootEntriesByWorldY.containsKey(worldY)) {
return resolvedState.rootEntriesByWorldY.get(worldY); return resolvedState.rootEntriesByWorldY.get(worldY);
} }
@@ -50,11 +51,11 @@ public final class IrisDimensionCarvingResolver {
} }
public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ) { public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ) {
return resolveFromRoot(engine, rootEntry, worldX, worldZ, new State()); return resolveFromRoot(engine, rootEntry, worldX, worldZ, THREAD_STATE.get());
} }
public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ, State state) { public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ, State state) {
State resolvedState = state == null ? new State() : state; State resolvedState = state == null ? THREAD_STATE.get() : state;
if (rootEntry == null) { if (rootEntry == null) {
return null; return null;
} }
@@ -37,61 +37,89 @@ import lombok.experimental.Accessors;
@Desc("Scale objects") @Desc("Scale objects")
@Data @Data
public class IrisObjectScale { public class IrisObjectScale {
private static ConcurrentLinkedHashMap<IrisObject, KList<IrisObject>> cache private static final ConcurrentLinkedHashMap<CacheKey, KList<IrisObject>> cache
= new ConcurrentLinkedHashMap.Builder<IrisObject, KList<IrisObject>>() = new ConcurrentLinkedHashMap.Builder<CacheKey, KList<IrisObject>>()
.initialCapacity(64) .initialCapacity(64)
.maximumWeightedCapacity(1024) .maximumWeightedCapacity(1024)
.concurrencyLevel(32) .concurrencyLevel(32)
.build(); .build();
@MinNumber(0.01)
@MaxNumber(50)
@Desc("Fixed scale multiplier for this object. 0.5 shrinks to half size, 2.0 doubles the size. When set to anything other than 1, this overrides minimumScale and maximumScale. Leave at 1 to use the minimumScale/maximumScale range.")
private double size = 1;
@MinNumber(1) @MinNumber(1)
@MaxNumber(32) @MaxNumber(32)
@Desc("Iris Objects are scaled and cached to speed up placements. Because of this extra memory is used, so we evenly distribute variations across the defined scale range, then pick one randomly. If the differences is small, use a lower number. For more possibilities on the scale spectrum, increase this at the cost of memory.") @Desc("Iris Objects are scaled and cached to speed up placements. Because of this extra memory is used, so we evenly distribute variations across the defined scale range, then pick one randomly. If the differences is small, use a lower number. For more possibilities on the scale spectrum, increase this at the cost of memory.")
private int variations = 7; private int variations = 7;
@MinNumber(0.01) @MinNumber(0.01)
@MaxNumber(50) @MaxNumber(50)
@Desc("The minimum scale") @Desc("The minimum scale. Used when size is 1 to pick a random scale per placement.")
private double minimumScale = 1; private double minimumScale = 1;
@MinNumber(0.01) @MinNumber(0.01)
@MaxNumber(50) @MaxNumber(50)
@Desc("The maximum height for placement (top of object)") @Desc("The maximum scale. Used when size is 1 to pick a random scale per placement.")
private double maximumScale = 1; private double maximumScale = 1;
@Desc("If this object is scaled up beyond its origin size, specify a 3D interpolator")
@Desc("If this object is scaled up beyond its origin size, specify a 3D interpolator. NONE keeps blocky scaled output, TRILINEAR (LERP) smooths with linear interpolation, TRICUBIC and TRIHERMITE produce smoother but slower output.")
private IrisObjectPlacementScaleInterpolator interpolation = IrisObjectPlacementScaleInterpolator.NONE; private IrisObjectPlacementScaleInterpolator interpolation = IrisObjectPlacementScaleInterpolator.NONE;
public boolean shouldScale() { public boolean shouldScale() {
return ((minimumScale == maximumScale) && maximumScale == 1) || variations <= 0; if (size != 1) {
return true;
}
if (variations <= 0) {
return false;
}
return minimumScale != 1 || maximumScale != 1;
} }
public int getMaxSizeFor(int indim) { public int getMaxSizeFor(int indim) {
return (int) (getMaxScale() * indim); return (int) Math.ceil(getMaxScale() * indim);
} }
public double getMaxScale() { public double getMaxScale() {
double mx = 0; if (size != 1) {
return size;
for (double i = minimumScale; i < maximumScale; i += (maximumScale - minimumScale) / (double) (Math.min(variations, 32))) {
mx = i;
} }
return Math.max(minimumScale, maximumScale);
return mx;
} }
public IrisObject get(RNG rng, IrisObject origin) { public IrisObject get(RNG rng, IrisObject origin) {
if (shouldScale()) { if (!shouldScale()) {
return origin; return origin;
} }
return cache.computeIfAbsent(origin, (k) -> { CacheKey key = new CacheKey(origin, size, minimumScale, maximumScale, variations, interpolation);
return cache.computeIfAbsent(key, (k) -> {
KList<IrisObject> c = new KList<>(); KList<IrisObject> c = new KList<>();
for (double i = minimumScale; i < maximumScale; i += (maximumScale - minimumScale) / (double) (Math.min(variations, 32))) {
c.add(origin.scaled(i, getInterpolation())); if (size != 1) {
c.add(origin.scaled(size, interpolation));
return c;
} }
if (minimumScale == maximumScale) {
c.add(origin.scaled(minimumScale, interpolation));
return c;
}
int vs = Math.max(1, Math.min(variations, 32));
double step = (maximumScale - minimumScale) / (double) vs;
for (int v = 0; v < vs; v++) {
c.add(origin.scaled(minimumScale + step * v, interpolation));
}
return c; return c;
}).getRandom(rng); }).getRandom(rng);
} }
public boolean canScaleBeyond() { public boolean canScaleBeyond() {
return shouldScale() && maximumScale > 1; return shouldScale() && getMaxScale() > 1;
}
private record CacheKey(IrisObject origin, double size, double minimumScale, double maximumScale, int variations, IrisObjectPlacementScaleInterpolator interpolation) {
} }
} }
@@ -60,7 +60,7 @@ public class ChunkContext {
long totalStartNanos = capturePrefillMetric ? System.nanoTime() : 0L; long totalStartNanos = capturePrefillMetric ? System.nanoTime() : 0L;
List<Runnable> fillTasks = new ArrayList<>(6); List<Runnable> fillTasks = new ArrayList<>(6);
if (resolvedPlan.height) { if (resolvedPlan.height) {
fillTasks.add(height::fill); fillTasks.add(() -> height.fillRounded(roundedHeight));
} }
if (resolvedPlan.biome) { if (resolvedPlan.biome) {
fillTasks.add(new PrefillFillTask(biome)); fillTasks.add(new PrefillFillTask(biome));
@@ -91,14 +91,9 @@ public class ChunkContext {
future.join(); future.join();
} }
} }
if (capturePrefillMetric) { if (capturePrefillMetric) {
metrics.getContextPrefill().put((System.nanoTime() - totalStartNanos) / 1_000_000D); metrics.getContextPrefill().put((System.nanoTime() - totalStartNanos) / 1_000_000D);
} }
if (resolvedPlan.height) {
fillRoundedHeight();
}
} }
} }
@@ -189,13 +184,4 @@ public class ChunkContext {
dataCache.fill(); dataCache.fill();
} }
} }
private void fillRoundedHeight() {
for (int z = 0; z < 16; z++) {
int rowOffset = z << 4;
for (int x = 0; x < 16; x++) {
roundedHeight[rowOffset + x] = (int) Math.round(height.getDouble(x, z));
}
}
}
} }
@@ -36,12 +36,30 @@ public class ChunkedDoubleDataCache {
} }
public void fill(Executor executor) { public void fill(Executor executor) {
fillRounded(null);
}
public void fillRounded(int[] roundedTarget) {
if (!cache) { if (!cache) {
if (roundedTarget != null) {
for (int row = 0; row < 16; row++) {
int rowOffset = row << 4;
int worldZ = z + row;
for (int column = 0; column < 16; column++) {
roundedTarget[rowOffset + column] = (int) Math.round(stream.getDouble(x + column, worldZ));
}
}
}
return; return;
} }
if (stream instanceof ChunkFillableDoubleStream2D cachedStream) { if (stream instanceof ChunkFillableDoubleStream2D cachedStream) {
cachedStream.fillChunkDoubles(x, z, data); cachedStream.fillChunkDoubles(x, z, data);
if (roundedTarget != null) {
for (int index = 0; index < 256; index++) {
roundedTarget[index] = (int) Math.round(data[index]);
}
}
return; return;
} }
@@ -49,7 +67,11 @@ public class ChunkedDoubleDataCache {
int rowOffset = row << 4; int rowOffset = row << 4;
int worldZ = z + row; int worldZ = z + row;
for (int column = 0; column < 16; column++) { for (int column = 0; column < 16; column++) {
data[rowOffset + column] = stream.getDouble(x + column, worldZ); double sampled = stream.getDouble(x + column, worldZ);
data[rowOffset + column] = sampled;
if (roundedTarget != null) {
roundedTarget[rowOffset + column] = (int) Math.round(sampled);
}
} }
} }
} }
@@ -1,70 +0,0 @@
package art.arcane.iris.util.project.profile;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.volmlib.util.math.M;
import lombok.Getter;
import lombok.Setter;
import java.util.concurrent.Semaphore;
@Getter
public class LoadBalancer extends MsptTimings {
private final Semaphore semaphore;
private final int maxPermits;
private final double range;
@Setter
private int minMspt, maxMspt;
private int permits, lastMspt;
private long lastTime = M.ms();
public LoadBalancer(Semaphore semaphore, int maxPermits, IrisSettings.MsRange range) {
this(semaphore, maxPermits, range.getMin(), range.getMax());
}
public LoadBalancer(Semaphore semaphore, int maxPermits, int minMspt, int maxMspt) {
this.semaphore = semaphore;
this.maxPermits = maxPermits;
this.minMspt = minMspt;
this.maxMspt = maxMspt;
this.range = maxMspt - minMspt;
setName("LoadBalancer");
start();
}
@Override
protected void update(int raw) {
lastTime = M.ms();
int mspt = raw;
if (mspt < lastMspt) {
int min = (int) Math.max(lastMspt * IrisSettings.get().getUpdater().getChunkLoadSensitivity(), 1);
mspt = Math.max(mspt, min);
}
lastMspt = mspt;
mspt = Math.max(mspt - minMspt, 0);
double percent = mspt / range;
int target = (int) (maxPermits * percent);
target = Math.min(target, maxPermits - 20);
int diff = target - permits;
permits = target;
if (diff == 0) return;
Iris.debug("Adjusting load to %s (%s) permits (%s mspt, %.2f)".formatted(target, diff, raw, percent));
if (diff > 0) semaphore.acquireUninterruptibly(diff);
else semaphore.release(Math.abs(diff));
}
public void close() {
interrupt();
semaphore.release(permits);
}
public void setRange(IrisSettings.MsRange range) {
minMspt = range.getMin();
maxMspt = range.getMax();
}
}
@@ -1,28 +0,0 @@
package art.arcane.iris.core.runtime;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class SmokeDiagnosticsServiceCloseStateTest {
@Test
public void closeStateIsPersistedIntoRunSnapshot() {
SmokeDiagnosticsService service = SmokeDiagnosticsService.get();
SmokeDiagnosticsService.SmokeRunHandle handle = service.beginRun(
SmokeDiagnosticsService.SmokeRunMode.STUDIO_CLOSE,
"iris-test-world",
true,
true,
null,
false
);
handle.setCloseState(true, false, true);
SmokeDiagnosticsService.SmokeRunReport report = handle.snapshot();
assertTrue(report.isCloseUnloadCompletedLive());
assertFalse(report.isCloseFolderDeletionCompletedLive());
assertTrue(report.isCloseStartupCleanupQueued());
}
}
@@ -204,6 +204,10 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
List<StructureStart> starts = new ArrayList<>(structureManager.startsForStructure(chunkAccess.getPos(), structure -> true)); List<StructureStart> starts = new ArrayList<>(structureManager.startsForStructure(chunkAccess.getPos(), structure -> true));
starts.sort(Comparator.comparingInt(start -> structureOrder.getOrDefault(start.getStructure(), Integer.MAX_VALUE))); starts.sort(Comparator.comparingInt(start -> structureOrder.getOrDefault(start.getStructure(), Integer.MAX_VALUE)));
Set<String> externalSmartBoreStructures = ExternalDataPackPipeline.snapshotSmartBoreStructureKeys(); Set<String> externalSmartBoreStructures = ExternalDataPackPipeline.snapshotSmartBoreStructureKeys();
IrisSettings.IrisSettingsGeneral general = IrisSettings.get().getGeneral();
boolean intrinsicFoundationsEnabled = general.isIntrinsicStructureFoundations();
int intrinsicFoundationDepth = Math.max(0, general.getIntrinsicFoundationMaxDepth());
List<String> intrinsicAllowlist = general.getIntrinsicStructureAllowlist();
int seededStructureIndex = Integer.MIN_VALUE; int seededStructureIndex = Integer.MIN_VALUE;
for (int j = 0; j < starts.size(); j++) { for (int j = 0; j < starts.size(); j++) {
@@ -217,16 +221,23 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
Supplier<String> supplier = () -> structureRegistry.getResourceKey(structure).map(Object::toString).orElseGet(structure::toString); Supplier<String> supplier = () -> structureRegistry.getResourceKey(structure).map(Object::toString).orElseGet(structure::toString);
String structureKey = resolveStructureKey(structureRegistry, structure); String structureKey = resolveStructureKey(structureRegistry, structure);
boolean isExternalSmartBoreStructure = externalSmartBoreStructures.contains(structureKey); boolean isExternalSmartBoreStructure = externalSmartBoreStructures.contains(structureKey);
boolean isIntrinsicFoundationStructure = !isExternalSmartBoreStructure
&& intrinsicFoundationsEnabled
&& intrinsicFoundationDepth > 0
&& matchesIntrinsicAllowlist(structureKey, intrinsicAllowlist);
int foundationDepth = isExternalSmartBoreStructure
? EXTERNAL_FOUNDATION_MAX_DEPTH
: (isIntrinsicFoundationStructure ? intrinsicFoundationDepth : 0);
BitSet[] beforeSolidColumns = null; BitSet[] beforeSolidColumns = null;
if (isExternalSmartBoreStructure) { if (foundationDepth > 0) {
beforeSolidColumns = snapshotChunkSolidColumns(level, chunkAccess); beforeSolidColumns = snapshotChunkSolidColumns(level, chunkAccess);
} }
try { try {
level.setCurrentlyGenerating(supplier); level.setCurrentlyGenerating(supplier);
start.placeInChunk(level, structureManager, this, random, getWritableArea(chunkAccess), chunkAccess.getPos()); start.placeInChunk(level, structureManager, this, random, getWritableArea(chunkAccess), chunkAccess.getPos());
if (isExternalSmartBoreStructure && beforeSolidColumns != null) { if (beforeSolidColumns != null) {
applyExternalStructureFoundations(level, chunkAccess, beforeSolidColumns, EXTERNAL_FOUNDATION_MAX_DEPTH); applyStructureFoundations(level, chunkAccess, beforeSolidColumns, foundationDepth);
} }
if (shouldLogExternalStructureFingerprint(structureKey)) { if (shouldLogExternalStructureFingerprint(structureKey)) {
logExternalStructureFingerprint(structureKey, start); logExternalStructureFingerprint(structureKey, start);
@@ -300,7 +311,31 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
return columns; return columns;
} }
private static void applyExternalStructureFoundations( private static boolean matchesIntrinsicAllowlist(String structureKey, List<String> allowlist) {
if (structureKey == null || structureKey.isBlank() || allowlist == null || allowlist.isEmpty()) {
return false;
}
String key = structureKey.toLowerCase(Locale.ROOT);
for (String raw : allowlist) {
if (raw == null) {
continue;
}
String pattern = raw.trim().toLowerCase(Locale.ROOT);
if (pattern.isEmpty()) {
continue;
}
if (pattern.endsWith("*")) {
if (key.startsWith(pattern.substring(0, pattern.length() - 1))) {
return true;
}
} else if (key.equals(pattern)) {
return true;
}
}
return false;
}
private static void applyStructureFoundations(
WorldGenLevel level, WorldGenLevel level,
ChunkAccess chunkAccess, ChunkAccess chunkAccess,
BitSet[] beforeSolidColumns, BitSet[] beforeSolidColumns,