This commit is contained in:
Brian Neumann-Fopiano committed 2026-07-30 12:31:39 -04:00
1 parent 0afbfcb7a2
commit 324cf9e095
374 files changed
+22703 -25648

No files matched your search

+7 -7
View File
@@ -40,6 +40,7 @@ art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java
art/arcane/iris/core/pregenerator/methods/HybridPregenMethod.java
art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java
art/arcane/iris/core/project/IrisProject.java
art/arcane/iris/core/project/StudioOpenProgressReporter.java
art/arcane/iris/core/runtime/BukkitPublicRuntimeControlBackend.java
art/arcane/iris/core/runtime/ChunkClearer.java
art/arcane/iris/core/runtime/ChunkJobReporter.java
@@ -72,6 +73,11 @@ art/arcane/iris/core/tools/IrisToolbelt.java
art/arcane/iris/core/tools/IrisWorldCreator.java
art/arcane/iris/engine/IrisEngineEffects.java
art/arcane/iris/engine/IrisWorldManager.java
art/arcane/iris/engine/MarkerSpawnScanner.java
art/arcane/iris/engine/WorldBlockDropRouter.java
art/arcane/iris/engine/WorldChunkMaintenance.java
art/arcane/iris/engine/WorldEntitySpawner.java
art/arcane/iris/engine/WorldTeleportWarmup.java
art/arcane/iris/engine/data/chunk/LinkedTerrainChunk.java
art/arcane/iris/engine/data/chunk/TerrainChunk.java
art/arcane/iris/engine/decorator/DecoratorCore.java
@@ -85,6 +91,7 @@ art/arcane/iris/engine/object/IPostBlockAccess.java
art/arcane/iris/engine/object/IrisAttributeModifier.java
art/arcane/iris/engine/object/IrisBiome.java
art/arcane/iris/engine/object/IrisBiomeCustomParticle.java
art/arcane/iris/engine/object/IrisBiomeDerivatives.java
art/arcane/iris/engine/object/IrisBiomeCustomSpawn.java
art/arcane/iris/engine/object/IrisBlockDrops.java
art/arcane/iris/engine/object/IrisCommandRegistry.java
@@ -153,19 +160,12 @@ art/arcane/iris/util/common/misc/Bindings.java
art/arcane/iris/util/common/nbt/mca/Chunk.java
art/arcane/iris/util/common/nbt/mca/NBTWorld.java
art/arcane/iris/util/common/plugin/Chunks.java
art/arcane/iris/util/common/plugin/CommandDummy.java
art/arcane/iris/util/common/plugin/IController.java
art/arcane/iris/util/common/plugin/IrisService.java
art/arcane/iris/util/common/plugin/MortarCommand.java
art/arcane/iris/util/common/plugin/MortarPermission.java
art/arcane/iris/util/common/plugin/RouterCommand.java
art/arcane/iris/util/common/plugin/VirtualCommand.java
art/arcane/iris/util/common/plugin/VolmitPlugin.java
art/arcane/iris/util/common/plugin/VolmitSender.java
art/arcane/iris/util/common/plugin/chunk/ChunkTickets.java
art/arcane/iris/util/common/plugin/chunk/TicketHolder.java
art/arcane/iris/util/common/reflect/KeyedType.java
art/arcane/iris/util/common/reflect/OldEnum.java
art/arcane/iris/util/common/scheduling/J.java
art/arcane/iris/util/project/hunk/Hunk.java
art/arcane/iris/util/project/hunk/view/ChunkBiomeHunkView.java
@@ -31,10 +31,11 @@ import lombok.Data;
import java.io.File;
import java.io.IOException;
@SuppressWarnings("SynchronizeOnNonFinalField")
@Data
public class IrisSettings {
public static IrisSettings settings;
private static final Object SETTINGS_LOCK = new Object();
private static final IrisSettings BOOTSTRAP_DEFAULTS = new IrisSettings();
public static volatile IrisSettings settings;
private IrisSettingsGeneral general = new IrisSettingsGeneral();
private IrisSettingsWorld world = new IrisSettingsWorld();
private IrisSettingsGUI gui = new IrisSettingsGUI();
@@ -55,53 +56,82 @@ public class IrisSettings {
}
public static IrisSettings get() {
if (settings != null) {
return settings;
IrisSettings current = settings;
if (current != null) {
return current;
}
settings = new IrisSettings();
if (Thread.holdsLock(SETTINGS_LOCK)) {
// read() logs and does IO, and the logging path calls back into get().
// Serve defaults instead of recursing into another disk read.
return BOOTSTRAP_DEFAULTS;
}
synchronized (SETTINGS_LOCK) {
current = settings;
if (current != null) {
return current;
}
current = read();
settings = current;
return current;
}
}
private static IrisSettings read() {
IrisSettings loaded = new IrisSettings();
File s = IrisPlatforms.get().dataFile("settings.json");
if (!s.exists()) {
try {
IO.writeAll(s, new JSONObject(new Gson().toJson(settings)).toString(4));
IO.writeAll(s, new JSONObject(new Gson().toJson(loaded)).toString(4));
} catch (JSONException | IOException e) {
e.printStackTrace();
IrisLogging.reportError(e);
}
} else {
try {
String ss = IO.readAll(s);
settings = new Gson().fromJson(ss, IrisSettings.class);
migrateLegacyKeys(ss);
try {
IO.writeAll(s, new JSONObject(new Gson().toJson(settings)).toString(4));
} catch (IOException e) {
e.printStackTrace();
}
} catch (Throwable ee) {
// IrisLogging.reportError(ee); causes a self-reference & stackoverflow
IrisLogging.error("Configuration Error in settings.json! " + ee.getClass().getSimpleName() + ": " + ee.getMessage());
}
return loaded;
}
return settings;
try {
String ss = IO.readAll(s);
IrisSettings parsed = new Gson().fromJson(ss, IrisSettings.class);
if (parsed != null) {
loaded = parsed;
}
migrateLegacyKeys(loaded, ss);
try {
IO.writeAll(s, new JSONObject(new Gson().toJson(loaded)).toString(4));
} catch (IOException e) {
e.printStackTrace();
}
} catch (Throwable ee) {
// IrisLogging.reportError(ee); causes a self-reference & stackoverflow
IrisLogging.error("Configuration Error in settings.json! " + ee.getClass().getSimpleName() + ": " + ee.getMessage());
}
return loaded;
}
private static void migrateLegacyKeys(String rawJson) {
private static void migrateLegacyKeys(IrisSettings target, String rawJson) {
JSONObject root = new JSONObject(rawJson);
JSONObject worldObject = root.optJSONObject("world");
if (worldObject == null || !worldObject.has("anbientEntitySpawningSystem")) {
return;
}
settings.getWorld().setAmbientEntitySpawningSystem(worldObject.optBoolean("anbientEntitySpawningSystem", settings.getWorld().isAmbientEntitySpawningSystem()));
target.getWorld().setAmbientEntitySpawningSystem(worldObject.optBoolean("anbientEntitySpawningSystem", target.getWorld().isAmbientEntitySpawningSystem()));
IrisLogging.info("Migrated legacy settings key world.anbientEntitySpawningSystem -> world.ambientEntitySpawningSystem");
}
public static void invalidate() {
synchronized (settings) {
synchronized (SETTINGS_LOCK) {
settings = null;
}
}
@@ -110,7 +140,7 @@ public class IrisSettings {
File s = IrisPlatforms.get().dataFile("settings.json");
try {
IO.writeAll(s, new JSONObject(new Gson().toJson(settings)).toString(4));
IO.writeAll(s, new JSONObject(new Gson().toJson(this)).toString(4));
} catch (JSONException | IOException e) {
e.printStackTrace();
IrisLogging.reportError(e);
@@ -158,7 +188,6 @@ public class IrisSettings {
private static final int MIN_RESIDENT_TECTONIC_PLATES = 16;
private static final double MANTLE_HEAP_FRACTION = 0.6D;
private static final int REFERENCE_PLATE_MEGABYTES = 48;
public boolean useTicketQueue = true;
public IrisRuntimeSchedulerMode runtimeSchedulerMode = IrisRuntimeSchedulerMode.AUTO;
public IrisPaperLikeBackendMode paperLikeBackendMode = IrisPaperLikeBackendMode.AUTO;
public int chunkLoadTimeoutSeconds = 15;
@@ -276,7 +305,6 @@ public class IrisSettings {
@Data
public static class IrisSettingsGenerator {
public String defaultWorldType = "overworld";
public int maxBiomeChildDepth = 4;
public boolean preventLeafDecay = true;
}
@@ -292,7 +320,6 @@ public class IrisSettings {
@Data
public static class IrisSettingsStudio {
public boolean studio = true;
public boolean openVSCode = true;
public boolean disableTimeAndWeather = true;
public boolean entitySpawning = true;
@@ -26,6 +26,7 @@ import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.datapack.ModrinthResolver.ResolvedDatapack;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.project.IrisCodeWorkspace;
import art.arcane.iris.core.structure.BulkStructureImporter;
import art.arcane.iris.core.structure.StructureImporter;
import art.arcane.iris.engine.object.IrisDimension;
@@ -45,11 +46,14 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@@ -100,7 +104,7 @@ public final class DatapackIngestService {
return;
}
try {
new IrisProject(data.getDataFolder()).updateWorkspace();
new IrisCodeWorkspace(new IrisProject(data.getDataFolder())).updateWorkspace();
} catch (Throwable e) {
IrisLogging.reportError(e);
}
@@ -319,14 +323,41 @@ public final class DatapackIngestService {
if (!force && installed && stripStateMatches) {
continue;
}
if (!worldFolder.exists()) {
worldFolder.mkdirs();
if (!worldFolder.isDirectory() && !worldFolder.mkdirs() && !worldFolder.isDirectory()) {
throw new IOException("Couldn't create datapacks folder " + worldFolder.getPath());
}
IO.delete(target);
IO.copyDirectory(stagedDir.toPath(), target.toPath());
if (stripOverrides) {
stripVanillaStructureOverrides(target);
writeMarker(marker);
// Stage outside the datapacks folder so a crash mid-copy can't leave a half-written pack for Minecraft to load.
File pendingRoot = worldFolder.getParentFile() == null
? new File(worldFolder, ".iris-datapack-install")
: new File(worldFolder.getParentFile(), ".iris-datapack-install");
File pending = new File(pendingRoot, id);
IO.delete(pending);
try {
IO.copyDirectory(stagedDir.toPath(), pending.toPath());
if (stripOverrides) {
stripVanillaStructureOverrides(pending);
writeMarker(new File(pending, OVERRIDES_STRIPPED_MARKER));
}
if (!new File(pending, "pack.mcmeta").isFile()) {
throw new IOException("Staged datapack " + id + " is missing pack.mcmeta");
}
IO.delete(target);
try {
move(pending.toPath(), target.toPath());
} catch (IOException swapFailure) {
IrisLogging.warn("Couldn't swap staged datapack " + id + " into " + target.getPath() + " (" + swapFailure.getMessage() + "); copying instead");
try {
IO.copyDirectory(pending.toPath(), target.toPath());
} catch (UncheckedIOException copyFailure) {
IO.delete(target);
throw copyFailure.getCause();
}
}
} catch (UncheckedIOException e) {
throw e.getCause();
} finally {
IO.delete(pending);
pendingRoot.delete();
}
}
}
@@ -372,12 +403,8 @@ public final class DatapackIngestService {
}
}
private static void writeMarker(File marker) {
try {
Files.writeString(marker.toPath(), "stripped", StandardCharsets.UTF_8);
} catch (IOException e) {
IrisLogging.reportError(e);
}
private static void writeMarker(File marker) throws IOException {
Files.writeString(marker.toPath(), "stripped", StandardCharsets.UTF_8);
}
private static void autoImportDatapackStructures() {
@@ -622,21 +649,43 @@ public final class DatapackIngestService {
}
return manifest;
} catch (Exception e) {
IrisLogging.reportError(e);
IrisLogging.reportError("Unreadable datapack manifest " + file.getPath()
+ "; moving it to manifest.json.corrupt instead of overwriting it", e);
quarantine(file.toPath());
return new Manifest();
}
}
private static void writeManifest(File root, Manifest manifest) {
File file = new File(root, "manifest.json");
private static void quarantine(Path file) {
try {
File parent = file.getParentFile();
if (parent != null) {
parent.mkdirs();
}
Files.writeString(file.toPath(), GSON.toJson(manifest), StandardCharsets.UTF_8);
move(file, file.resolveSibling(file.getFileName().toString() + ".corrupt"));
} catch (IOException e) {
IrisLogging.reportError(e);
IrisLogging.reportError("Failed to move aside corrupt datapack manifest " + file, e);
}
}
private static void writeManifest(File root, Manifest manifest) {
Path file = new File(root, "manifest.json").toPath();
try {
Path parent = file.getParent();
Files.createDirectories(parent);
Path temp = Files.createTempFile(parent, "manifest", ".json.tmp");
try {
Files.writeString(temp, GSON.toJson(manifest), StandardCharsets.UTF_8);
move(temp, file);
} finally {
Files.deleteIfExists(temp);
}
} catch (IOException e) {
IrisLogging.reportError("Failed to write datapack manifest " + file, e);
}
}
private static void move(Path source, Path target) throws IOException {
try {
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
} catch (AtomicMoveNotSupportedException e) {
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
}
}
@@ -33,7 +33,6 @@ import art.arcane.iris.core.pregenerator.PregenPhaseTracker;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.format.MemoryMonitor;
import art.arcane.volmlib.util.function.Consumer2;
@@ -45,6 +44,7 @@ import art.arcane.iris.util.common.scheduling.J;
import java.awt.Color;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@@ -65,8 +65,8 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
private final MemoryMonitor monitor;
private final PregenTask task;
private final boolean saving;
private final KList<Consumer<Double>> onProgress = new KList<>();
private final KList<Runnable> whenDone = new KList<>();
private final List<Consumer<Double>> onProgress = new CopyOnWriteArrayList<>();
private final List<Runnable> whenDone = new CopyOnWriteArrayList<>();
private final IrisPregenerator pregenerator;
private final Position2 min;
private final Position2 max;
@@ -91,6 +91,7 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
instance.updateAndGet(old -> {
if (old != null) {
old.pregenerator.close();
old.worker.interrupt();
old.close();
}
return this;
@@ -135,7 +136,10 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
return false;
}
J.a(inst.pregenerator::close);
J.a(() -> {
inst.pregenerator.close();
inst.worker.interrupt();
});
return true;
}
@@ -305,6 +309,7 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
public void stop() {
J.a(() -> {
pregenerator.close();
worker.interrupt();
close();
instance.compareAndSet(this, null);
});
@@ -1,31 +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.gui.components;
import lombok.Builder;
import lombok.Data;
import java.awt.image.BufferedImage;
@Builder
@Data
public class TileRender {
private BufferedImage image;
private int quality;
}
@@ -55,6 +55,12 @@ public class ImageResourceLoader extends ResourceLoader<IrisImage> {
try {
PrecisionStopwatch p = PrecisionStopwatch.start();
BufferedImage bu = ImageIO.read(j);
if (bu == null) {
IrisLogging.warn("Couldn't read " + resourceTypeName + " file: " + j.getPath() + " (unsupported or corrupt image)");
return null;
}
IrisImage img = new IrisImage(bu);
img.setLoadFile(j);
img.setLoader(manager);
@@ -69,7 +75,7 @@ public class ImageResourceLoader extends ResourceLoader<IrisImage> {
}
}
void getPNGFiles(File directory, Set<String> m, HashSet<String> visitedDirectories) {
void getPNGFiles(File directory, String prefix, Set<String> m, HashSet<String> visitedDirectories) {
if (directory == null || !directory.exists()) {
return;
}
@@ -88,9 +94,9 @@ public class ImageResourceLoader extends ResourceLoader<IrisImage> {
for (File file : listedFiles) {
if (file.isFile() && file.getName().endsWith(".png")) {
m.add(file.getName().replaceAll("\\Q.png\\E", ""));
m.add(prefix + file.getName().replaceAll("\\Q.png\\E", ""));
} else if (file.isDirectory()) {
getPNGFiles(file, m, visitedDirectories);
getPNGFiles(file, prefix + file.getName() + "/", m, visitedDirectories);
}
}
}
@@ -105,31 +111,10 @@ public class ImageResourceLoader extends ResourceLoader<IrisImage> {
KSet<String> m = new KSet<>();
HashSet<String> visitedDirectories = new HashSet<>();
for (File i : getFolders()) {
getPNGFiles(i, m, visitedDirectories);
getPNGFiles(i, "", m, visitedDirectories);
}
// for (File i : getFolders()) {
// for (File j : i.listFiles()) {
// if (j.isFile() && j.getName().endsWith(".png")) {
// m.add(j.getName().replaceAll("\\Q.png\\E", ""));
// } else if (j.isDirectory()) {
// for (File k : j.listFiles()) {
// if (k.isFile() && k.getName().endsWith(".png")) {
// m.add(j.getName() + "/" + k.getName().replaceAll("\\Q.png\\E", ""));
// } else if (k.isDirectory()) {
// for (File l : k.listFiles()) {
// if (l.isFile() && l.getName().endsWith(".png")) {
// m.add(j.getName() + "/" + k.getName() + "/" + l.getName().replaceAll("\\Q.png\\E", ""));
// }
// }
// }
// }
// }
// }
// }
KList<String> v = new KList<>(m);
possibleKeys = v.toArray(new String[0]);
return possibleKeys;
@@ -152,18 +137,10 @@ public class ImageResourceLoader extends ResourceLoader<IrisImage> {
return null;
}
for (File i : getFolders(name)) {
for (File j : i.listFiles()) {
if (j.isFile() && j.getName().endsWith(".png") && j.getName().split("\\Q.\\E")[0].equals(name)) {
return j;
}
}
File file = resolveFile(name, ".png");
File file = new File(i, name + ".png");
if (file.exists()) {
return file;
}
if (file != null) {
return file;
}
IrisLogging.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")");
@@ -176,18 +153,10 @@ public class ImageResourceLoader extends ResourceLoader<IrisImage> {
}
private IrisImage loadRaw(String name) {
for (File i : getFolders(name)) {
for (File j : i.listFiles()) {
if (j.isFile() && j.getName().endsWith(".png") && j.getName().split("\\Q.\\E")[0].equals(name)) {
return loadFile(j, name);
}
}
File file = resolveFile(name, ".png");
File file = new File(i, name + ".png");
if (file.exists()) {
return loadFile(file, name);
}
if (file != null) {
return loadFile(file, name);
}
IrisLogging.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")");
@@ -94,7 +94,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
private final File dataFolder;
private final int id;
private final boolean datapackCompiler;
private boolean closed = false;
private volatile boolean closed = false;
private ResourceLoader<IrisBiome> biomeLoader;
private ResourceLoader<IrisLootTable> lootLoader;
private ResourceLoader<IrisRegion> regionLoader;
@@ -116,7 +116,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
private Gson gson;
private Gson snippetLoader;
private GsonBuilder builder;
private KMap<Class<? extends IrisRegistrant>, ResourceLoader<? extends IrisRegistrant>> loaders = new KMap<>();
private volatile KMap<Class<? extends IrisRegistrant>, ResourceLoader<? extends IrisRegistrant>> loaders = new KMap<>();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient List<Engine> engines = new ArrayList<>();
@@ -367,16 +367,14 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
synchronized (engines) {
engines.clear();
}
if (dataLoaders.get(dataFolder) == this) {
dataLoaders.remove(dataFolder);
}
dataLoaders.remove(dataFolder, this);
}
public IrisData copy() {
return IrisData.get(dataFolder);
}
private <T extends IrisRegistrant> ResourceLoader<T> registerLoader(Class<T> registrant) {
private <T extends IrisRegistrant> ResourceLoader<T> registerLoader(Class<T> registrant, KMap<Class<? extends IrisRegistrant>, ResourceLoader<? extends IrisRegistrant>> target) {
try {
IrisRegistrant rr = registrant.getConstructor().newInstance();
ResourceLoader<T> r = null;
@@ -397,7 +395,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
r = new ResourceLoader<>(dataFolder, this, rr.getFolderName(), rr.getTypeName(), registrant, options);
}
loaders.put(registrant, r);
target.put(registrant, r);
return r;
} catch (Throwable e) {
@@ -420,30 +418,31 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
.registerTypeAdapterFactory(this)
.registerTypeAdapter(MantleFlag.class, new MantleFlagAdapter())
.setPrettyPrinting();
loaders.clear();
KMap<Class<? extends IrisRegistrant>, ResourceLoader<? extends IrisRegistrant>> replacement = new KMap<>();
File packs = dataFolder;
packs.mkdirs();
recoverStructureTransactions();
this.lootLoader = registerLoader(IrisLootTable.class);
this.spawnerLoader = registerLoader(IrisSpawner.class);
this.entityLoader = registerLoader(IrisEntity.class);
this.regionLoader = registerLoader(IrisRegion.class);
this.biomeLoader = registerLoader(IrisBiome.class);
this.modLoader = registerLoader(IrisMod.class);
this.dimensionLoader = registerLoader(IrisDimension.class);
this.generatorLoader = registerLoader(IrisGenerator.class);
this.markerLoader = registerLoader(IrisMarker.class);
this.blockLoader = registerLoader(IrisBlockData.class);
this.expressionLoader = registerLoader(IrisExpression.class);
this.objectLoader = registerLoader(IrisObject.class);
this.imageLoader = registerLoader(IrisImage.class);
this.matterLoader = registerLoader(IrisMatterObject.class);
this.structureLoader = registerLoader(IrisStructure.class);
this.jigsawPoolLoader = registerLoader(IrisJigsawPool.class);
this.jigsawPieceLoader = registerLoader(IrisJigsawPiece.class);
this.lootLoader = registerLoader(IrisLootTable.class, replacement);
this.spawnerLoader = registerLoader(IrisSpawner.class, replacement);
this.entityLoader = registerLoader(IrisEntity.class, replacement);
this.regionLoader = registerLoader(IrisRegion.class, replacement);
this.biomeLoader = registerLoader(IrisBiome.class, replacement);
this.modLoader = registerLoader(IrisMod.class, replacement);
this.dimensionLoader = registerLoader(IrisDimension.class, replacement);
this.generatorLoader = registerLoader(IrisGenerator.class, replacement);
this.markerLoader = registerLoader(IrisMarker.class, replacement);
this.blockLoader = registerLoader(IrisBlockData.class, replacement);
this.expressionLoader = registerLoader(IrisExpression.class, replacement);
this.objectLoader = registerLoader(IrisObject.class, replacement);
this.imageLoader = registerLoader(IrisImage.class, replacement);
this.matterLoader = registerLoader(IrisMatterObject.class, replacement);
this.structureLoader = registerLoader(IrisStructure.class, replacement);
this.jigsawPoolLoader = registerLoader(IrisJigsawPool.class, replacement);
this.jigsawPieceLoader = registerLoader(IrisJigsawPiece.class, replacement);
builder.registerTypeAdapterFactory(KeyedType::createTypeAdapter);
gson = builder.create();
loaders = replacement;
for (Engine engine : getEngines()) {
engine.hotload();
@@ -460,13 +459,14 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
.registerTypeAdapterFactory(this)
.registerTypeAdapter(MantleFlag.class, new MantleFlagAdapter())
.setPrettyPrinting();
loaders.clear();
KMap<Class<? extends IrisRegistrant>, ResourceLoader<? extends IrisRegistrant>> replacement = new KMap<>();
dataFolder.mkdirs();
recoverStructureTransactions();
biomeLoader = registerLoader(IrisBiome.class);
dimensionLoader = registerLoader(IrisDimension.class);
biomeLoader = registerLoader(IrisBiome.class, replacement);
dimensionLoader = registerLoader(IrisDimension.class, replacement);
builder.registerTypeAdapterFactory(KeyedType::createTypeAdapter);
gson = builder.create();
loaders = replacement;
if (biomeLoader == null || dimensionLoader == null) {
throw new IllegalStateException("Unable to initialize Iris datapack compiler loaders for " + dataFolder);
}
@@ -31,8 +31,6 @@ import java.io.IOException;
import java.util.HashSet;
public class MatterObjectResourceLoader extends ResourceLoader<IrisMatterObject> {
private String[] possibleKeys;
public MatterObjectResourceLoader(File root, IrisData idm, String folderName, String resourceTypeName, Options options) {
super(root, idm, folderName, resourceTypeName, IrisMatterObject.class, options);
loadCache = new KCache<>(this::loadRaw, IrisSettings.get().getPerformance().getObjectLoaderCacheSize());
@@ -67,7 +65,7 @@ public class MatterObjectResourceLoader extends ResourceLoader<IrisMatterObject>
}
}
private void findMatFiles(File dir, KSet<String> m, HashSet<String> visitedDirectories) {
private void findMatFiles(File dir, String prefix, KSet<String> m, HashSet<String> visitedDirectories) {
if (dir == null || !dir.exists()) {
return;
}
@@ -86,9 +84,9 @@ public class MatterObjectResourceLoader extends ResourceLoader<IrisMatterObject>
for (File file : listedFiles) {
if (file.isFile() && file.getName().endsWith(".mat")) {
m.add(file.getName().replaceAll("\\Q.mat\\E", ""));
m.add(prefix + file.getName().replaceAll("\\Q.mat\\E", ""));
} else if (file.isDirectory()) {
findMatFiles(file, m, visitedDirectories);
findMatFiles(file, prefix + file.getName() + "/", m, visitedDirectories);
}
}
}
@@ -103,7 +101,7 @@ public class MatterObjectResourceLoader extends ResourceLoader<IrisMatterObject>
HashSet<String> visitedDirectories = new HashSet<>();
for (File folder : getFolders()) {
findMatFiles(folder, m, visitedDirectories);
findMatFiles(folder, "", m, visitedDirectories);
}
KList<String> v = new KList<>(m);
@@ -119,40 +117,6 @@ public class MatterObjectResourceLoader extends ResourceLoader<IrisMatterObject>
}
}
// public String[] getPossibleKeys() {
// if (possibleKeys != null) {
// return possibleKeys;
// }
//
// IrisLogging.debug("Building " + resourceTypeName + " Possibility Lists");
// KSet<String> m = new KSet<>();
//
// for (File i : getFolders()) {
// for (File j : i.listFiles()) {
// if (j.isFile() && j.getName().endsWith(".mat")) {
// m.add(j.getName().replaceAll("\\Q.mat\\E", ""));
// } else if (j.isDirectory()) {
// for (File k : j.listFiles()) {
// if (k.isFile() && k.getName().endsWith(".mat")) {
// m.add(j.getName() + "/" + k.getName().replaceAll("\\Q.mat\\E", ""));
// } else if (k.isDirectory()) {
// for (File l : k.listFiles()) {
// if (l.isFile() && l.getName().endsWith(".mat")) {
// m.add(j.getName() + "/" + k.getName() + "/" + l.getName().replaceAll("\\Q.mat\\E", ""));
// }
// }
// }
// }
// }
// }
// }
//
// KList<String> v = new KList<>(m);
// possibleKeys = v.toArray(new String[0]);
// return possibleKeys;
// }
public File findFile(String name) {
if (name == null || name.trim().isEmpty()) {
return null;
@@ -162,18 +126,10 @@ public class MatterObjectResourceLoader extends ResourceLoader<IrisMatterObject>
return null;
}
for (File i : getFolders(name)) {
for (File j : i.listFiles()) {
if (j.isFile() && j.getName().endsWith(".mat") && j.getName().split("\\Q.\\E")[0].equals(name)) {
return j;
}
}
File file = resolveFile(name, ".mat");
File file = new File(i, name + ".mat");
if (file.exists()) {
return file;
}
if (file != null) {
return file;
}
IrisLogging.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")");
@@ -186,18 +142,10 @@ public class MatterObjectResourceLoader extends ResourceLoader<IrisMatterObject>
}
private IrisMatterObject loadRaw(String name) {
for (File i : getFolders(name)) {
for (File j : i.listFiles()) {
if (j.isFile() && j.getName().endsWith(".mat") && j.getName().split("\\Q.\\E")[0].equals(name)) {
return loadFile(j, name);
}
}
File file = resolveFile(name, ".mat");
File file = new File(i, name + ".mat");
if (file.exists()) {
return loadFile(file, name);
}
if (file != null) {
return loadFile(file, name);
}
IrisLogging.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")");
@@ -131,18 +131,10 @@ public class ObjectResourceLoader extends ResourceLoader<IrisObject> {
return null;
}
for (File i : getFolders(name)) {
for (File j : i.listFiles()) {
if (j.isFile() && j.getName().endsWith(".iob") && j.getName().split("\\Q.\\E")[0].equals(name)) {
return j;
}
}
File file = resolveFile(name, ".iob");
File file = new File(i, name + ".iob");
if (file.exists()) {
return file;
}
if (file != null) {
return file;
}
IrisLogging.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")");
@@ -155,21 +147,8 @@ public class ObjectResourceLoader extends ResourceLoader<IrisObject> {
}
private IrisObject loadRaw(String name) {
for (File i : getFolders(name)) {
for (File j : i.listFiles()) {
if (j.isFile() && j.getName().endsWith(".iob") && j.getName().split("\\Q.\\E")[0].equals(name)) {
return loadFile(j, name);
}
}
File file = new File(i, name + ".iob");
if (file.exists()) {
return loadFile(file, name);
}
}
return null;
File file = resolveFile(name, ".iob");
return file == null ? null : loadFile(file, name);
}
public IrisObject load(String name, boolean warn) {
@@ -52,7 +52,11 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Locale;
import java.util.Objects;
@@ -64,6 +68,7 @@ import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
@@ -83,7 +88,7 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
private static final Set<String> schemaBuildQueue = ConcurrentHashMap.newKeySet();
private static final AtomicBoolean schemaBuildExecutorRegistered = new AtomicBoolean();
protected final AtomicCache<KList<File>> folderCache;
protected KSet<String> firstAccess;
protected volatile KSet<String> firstAccess;
protected File root;
protected String folderName;
protected String resourceTypeName;
@@ -163,18 +168,10 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
return null;
}
for (File i : getFolders(name)) {
for (File j : i.listFiles()) {
if (j.isFile() && j.getName().endsWith(".json") && j.getName().split("\\Q.\\E")[0].equals(name)) {
return j;
}
}
File file = resolveFile(name, ".json");
File file = new File(i, name + ".json");
if (file.exists()) {
return file;
}
if (file != null) {
return file;
}
IrisLogging.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")");
@@ -182,6 +179,57 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
return null;
}
/**
* Resolves a resource file by key. An exact <code>name + extension</code> hit always wins;
* only then is the dotted-prefix scan used (so plains.json beats plains.disabled.json).
*/
protected File resolveFile(String name, String extension) {
return resolveFile(name, extension, getFolders(name));
}
protected File resolveFile(String name, String extension, KList<File> folders) {
if (folders == null) {
return null;
}
for (File folder : folders) {
File exact = new File(folder, name + extension);
if (exact.isFile()) {
return exact;
}
File[] listed = folder.listFiles();
if (listed == null) {
continue;
}
KList<File> matches = new KList<>();
for (File candidate : listed) {
if (candidate.isFile() && candidate.getName().endsWith(extension) && candidate.getName().split("\\Q.\\E")[0].equals(name)) {
matches.add(candidate);
}
}
if (matches.isEmpty()) {
continue;
}
if (matches.size() > 1) {
matches.sort(Comparator.comparing(File::getName));
IrisLogging.warn("Ambiguous " + resourceTypeName + " " + name + " in " + folder.getPath() + ": "
+ matches.stream().map(File::getName).collect(Collectors.joining(", "))
+ " (using " + matches.get(0).getName() + ")");
}
return matches.get(0);
}
return null;
}
protected static String describeName(String name) {
if (name == null) return "<java null>";
if (name.isEmpty()) return "<empty string>";
@@ -413,22 +461,8 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
}
private T loadRaw(String name) {
for (File i : getFolders(name)) {
//noinspection ConstantConditions
for (File j : i.listFiles()) {
if (j.isFile() && j.getName().endsWith(".json") && j.getName().split("\\Q.\\E")[0].equals(name)) {
return loadFile(j, name);
}
}
File file = new File(i, name + ".json");
if (file.exists()) {
return loadFile(file, name);
}
}
return null;
File file = resolveFile(name, ".json");
return file == null ? null : loadFile(file, name);
}
public T load(String name, boolean warn) {
@@ -440,53 +474,90 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
return null;
}
var set = firstAccess;
if (set != null) firstAccess.add(name);
KSet<String> set = firstAccess;
if (set != null) set.add(name);
return loadCache.get(name);
}
public void loadFirstAccess(Engine engine) throws IOException {
private File prefetchFile(Engine engine) {
String id = "DIM" + Math.abs(engine.getSeedManager().getSeed() + engine.getDimension().getVersion() + engine.getDimension().getLoadKey().hashCode());
File file = IrisPlatforms.get().dataFile("prefetch/" + id + "/" + Math.abs(getFolderName().hashCode()) + ".ipfch");
return IrisPlatforms.get().dataFile("prefetch/" + id + "/" + Math.abs(getFolderName().hashCode()) + ".ipfch");
}
public void loadFirstAccess(Engine engine) throws IOException {
File file = prefetchFile(engine);
if (!file.exists()) {
return;
}
FileInputStream fin = new FileInputStream(file);
GZIPInputStream gzi = new GZIPInputStream(fin);
DataInputStream din = new DataInputStream(gzi);
int m = din.readInt();
KList<String> s = new KList<>();
for (int i = 0; i < m; i++) {
s.add(din.readUTF());
try (FileInputStream fin = new FileInputStream(file);
GZIPInputStream gzi = new GZIPInputStream(fin);
DataInputStream din = new DataInputStream(gzi)) {
int m = din.readInt();
if (m < 0) {
throw new IOException("Bad prefetch count " + m);
}
for (int i = 0; i < m; i++) {
s.add(din.readUTF());
}
} catch (IOException e) {
IrisLogging.warn("Discarding corrupt prefetch " + file.getPath() + ": " + e.getMessage());
if (!file.delete()) {
IrisLogging.warn("Couldn't delete corrupt prefetch " + file.getPath());
}
return;
}
din.close();
IrisLogging.info("Loading " + s.size() + " prefetch " + getFolderName());
firstAccess = null;
loadAllParallel(s);
}
public void saveFirstAccess(Engine engine) throws IOException {
if (firstAccess == null) return;
String id = "DIM" + Math.abs(engine.getSeedManager().getSeed() + engine.getDimension().getVersion() + engine.getDimension().getLoadKey().hashCode());
File file = IrisPlatforms.get().dataFile("prefetch/" + id + "/" + Math.abs(getFolderName().hashCode()) + ".ipfch");
file.getParentFile().mkdirs();
FileOutputStream fos = new FileOutputStream(file);
GZIPOutputStream gzo = new CustomOutputStream(fos, 9);
DataOutputStream dos = new DataOutputStream(gzo);
var set = firstAccess;
firstAccess = null;
dos.writeInt(set.size());
KSet<String> set = firstAccess;
if (set == null) return;
KList<String> snapshot = new KList<>(set);
File file = prefetchFile(engine);
File parent = file.getParentFile();
for (String i : set) {
dos.writeUTF(i);
if (parent == null) {
throw new IOException("Prefetch path has no parent: " + file.getPath());
}
dos.flush();
dos.close();
if (!parent.isDirectory() && !parent.mkdirs() && !parent.isDirectory()) {
throw new IOException("Couldn't create prefetch folder " + parent.getPath());
}
File temp = File.createTempFile(file.getName(), ".tmp", parent);
try {
try (FileOutputStream fos = new FileOutputStream(temp);
GZIPOutputStream gzo = new CustomOutputStream(fos, 9);
DataOutputStream dos = new DataOutputStream(gzo)) {
dos.writeInt(snapshot.size());
for (String i : snapshot) {
dos.writeUTF(i);
}
}
try {
Files.move(temp.toPath(), file.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
} catch (AtomicMoveNotSupportedException e) {
Files.move(temp.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
} finally {
Files.deleteIfExists(temp.toPath());
}
firstAccess = null;
}
public KList<File> getFolders() {
@@ -531,21 +602,7 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
}
public File fileFor(T b) {
for (File i : getFolders()) {
for (File j : i.listFiles()) {
if (j.isFile() && j.getName().endsWith(".json") && j.getName().split("\\Q.\\E")[0].equals(b.getLoadKey())) {
return j;
}
}
File file = new File(i, b.getLoadKey() + ".json");
if (file.exists()) {
return file;
}
}
return null;
return resolveFile(b.getLoadKey(), ".json", getFolders());
}
public boolean isLoaded(String next) {
@@ -1,39 +0,0 @@
package art.arcane.iris.core.nms.container;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.function.NastyRunnable;
import lombok.AllArgsConstructor;
import java.util.concurrent.atomic.AtomicBoolean;
@AllArgsConstructor
public class AutoClosing implements AutoCloseable {
private static final KMap<Thread, AutoClosing> CONTEXTS = new KMap<>();
private final AtomicBoolean closed = new AtomicBoolean();
private final NastyRunnable action;
@Override
public void close() {
if (closed.getAndSet(true)) return;
try {
removeContext();
action.run();
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
public void storeContext() {
CONTEXTS.put(Thread.currentThread(), this);
}
public void removeContext() {
CONTEXTS.values().removeIf(c -> c == this);
}
public static void closeContext() {
AutoClosing closing = CONTEXTS.remove(Thread.currentThread());
if (closing == null) return;
closing.close();
}
}
@@ -18,8 +18,17 @@
package art.arcane.iris.core.pack;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
@@ -28,6 +37,7 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
public final class ContentKeyValidator {
private static final int MAX_SUGGESTION_SCANS = 4096;
@@ -221,4 +231,151 @@ public final class ContentKeyValidator {
}
return prev[lb];
}
static void runContentKeyValidation(File packFolder, List<String> warnings) {
try {
if (!IrisPlatforms.isBound()) {
return;
}
PlatformRegistries registries = IrisPlatforms.get().registries();
if (registries == null) {
return;
}
List<String> blockKeys = registries.blockKeys();
List<String> itemKeys = registries.itemKeys();
List<String> entityKeys = registries.entityKeys();
if (blockKeys == null || blockKeys.isEmpty() || itemKeys == null || itemKeys.isEmpty() || entityKeys == null || entityKeys.isEmpty()) {
return;
}
ReferencedContentKeys referenced = collectReferencedContentKeys(packFolder);
List<ContentKeyValidator.ContentKeyError> errors = ContentKeyValidator.validate(
registries, referenced.blocks(), referenced.items(), referenced.entities());
for (ContentKeyValidator.ContentKeyError error : errors) {
warnings.add(error.message());
}
} catch (Throwable e) {
IrisLogging.reportError("PackValidator content-key validation failed for pack '" + packFolder.getName() + "'", e);
}
}
private static ReferencedContentKeys collectReferencedContentKeys(File packFolder) {
Set<String> blocks = new HashSet<>();
Set<String> items = new HashSet<>();
Set<String> entities = new HashSet<>();
Set<String> customBlocks = deriveRegistrantKeys(new File(packFolder, "blocks"));
try (Stream<Path> stream = Files.walk(packFolder.toPath())) {
List<Path> files = stream.filter(Files::isRegularFile)
.filter(PackValidationIo::isScannableJsonPath)
.toList();
for (Path path : files) {
String relative = packFolder.toPath().relativize(path).toString().replace(File.separatorChar, '/');
boolean inLoot = relative.startsWith("loot/");
boolean inEntities = relative.startsWith("entities/");
JSONObject json;
try {
json = new JSONObject(Files.readString(path, StandardCharsets.UTF_8));
} catch (Throwable ignored) {
continue;
}
collectFromNode(json, blocks, inLoot ? items : null, inEntities ? entities : null, customBlocks);
}
} catch (Throwable e) {
IrisLogging.reportError("PackValidator failed to walk pack for content-key extraction", e);
}
return new ReferencedContentKeys(blocks, items, entities);
}
private static void collectFromNode(Object node, Set<String> blocks, Set<String> items, Set<String> entities, Set<String> customBlocks) {
if (node instanceof JSONObject obj) {
for (String key : obj.keySet()) {
Object value = obj.get(key);
if (value instanceof String str) {
if ("block".equals(key)) {
addBlockRef(str, blocks, customBlocks);
} else if (items != null && "type".equals(key)) {
addSimpleRef(str, items);
} else if (entities != null && "type".equals(key)) {
addSimpleRef(str, entities);
}
} else {
collectFromNode(value, blocks, items, entities, customBlocks);
}
}
} else if (node instanceof JSONArray arr) {
for (int i = 0; i < arr.length(); i++) {
collectFromNode(arr.get(i), blocks, items, entities, customBlocks);
}
}
}
private static void addBlockRef(String raw, Set<String> blocks, Set<String> customBlocks) {
String value = raw.trim().toLowerCase(Locale.ROOT);
int bracket = value.indexOf('[');
if (bracket >= 0) {
value = value.substring(0, bracket).trim();
}
if (value.isEmpty() || customBlocks.contains(value)) {
return;
}
blocks.add(value);
}
private static void addSimpleRef(String raw, Set<String> target) {
String value = raw.trim().toLowerCase(Locale.ROOT);
if (!value.isEmpty()) {
target.add(value);
}
}
private static Set<String> deriveRegistrantKeys(File folder) {
Set<String> keys = new HashSet<>();
if (!folder.isDirectory()) {
return keys;
}
for (File file : PackValidationIo.listJsonRecursive(folder)) {
String key = PackValidationIo.deriveKey(folder, file);
if (key != null && !key.isBlank()) {
keys.add(key.toLowerCase(Locale.ROOT));
}
}
return keys;
}
static Set<String> deriveRegistrantKeysExact(File folder) {
Set<String> keys = new HashSet<>();
if (!folder.isDirectory()) {
return keys;
}
for (File file : PackValidationIo.listJsonRecursive(folder)) {
String key = PackValidationIo.deriveKey(folder, file);
if (key != null && !key.isBlank()) {
keys.add(key);
}
}
return keys;
}
static Set<String> deriveObjectKeysExact(File folder) {
Set<String> keys = new HashSet<>();
if (!folder.isDirectory()) {
return keys;
}
try (Stream<Path> stream = Files.walk(folder.toPath())) {
stream.filter(Files::isRegularFile)
.filter(path -> path.getFileName().toString().endsWith(".iob"))
.forEach(path -> {
Path relative = folder.toPath().relativize(path);
String key = relative.toString().replace(File.separatorChar, '/');
keys.add(key.substring(0, key.length() - ".iob".length()));
});
} catch (IOException ignored) {
}
return keys;
}
private record ReferencedContentKeys(Set<String> blocks, Set<String> items, Set<String> entities) {
}
}
@@ -0,0 +1,193 @@
/*
* 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.pack;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.List;
final class PackDimensionValidator {
private PackDimensionValidator() {
}
static void validateDimensions(File packFolder, File[] dimensionFiles, List<String> blockingErrors, List<String> warnings) {
File regionsFolder = new File(packFolder, "regions");
File biomesFolder = new File(packFolder, "biomes");
for (File dimFile : dimensionFiles) {
String dimensionKey = PackValidationIo.stripExtension(dimFile.getName());
JSONObject dimJson;
try {
dimJson = new JSONObject(Files.readString(dimFile.toPath(), StandardCharsets.UTF_8));
} catch (Throwable e) {
blockingErrors.add("Dimension '" + dimensionKey + "' has invalid JSON: " + e.getMessage());
continue;
}
validateImportedStructurePolicy(dimensionKey, dimJson, blockingErrors);
JSONArray regionsArray = dimJson.optJSONArray("regions");
if (regionsArray == null || regionsArray.length() == 0) {
blockingErrors.add("Dimension '" + dimensionKey + "' declares no regions.");
continue;
}
int resolvedRegions = 0;
for (int i = 0; i < regionsArray.length(); i++) {
String regionKey = regionsArray.optString(i, null);
if (regionKey == null || regionKey.isBlank()) {
warnings.add("Dimension '" + dimensionKey + "' has a blank region entry at index " + i + ".");
continue;
}
File regionFile = new File(regionsFolder, regionKey + ".json");
if (!regionFile.isFile()) {
blockingErrors.add("Dimension '" + dimensionKey + "' references missing region '" + regionKey + "'.");
continue;
}
JSONObject regionJson;
try {
regionJson = new JSONObject(Files.readString(regionFile.toPath(), StandardCharsets.UTF_8));
} catch (Throwable e) {
blockingErrors.add("Region '" + regionKey + "' has invalid JSON: " + e.getMessage());
continue;
}
int anyBiome = countBiomeRefs(regionJson, "landBiomes", biomesFolder, regionKey, warnings)
+ countBiomeRefs(regionJson, "seaBiomes", biomesFolder, regionKey, warnings)
+ countBiomeRefs(regionJson, "shoreBiomes", biomesFolder, regionKey, warnings)
+ countBiomeRefs(regionJson, "caveBiomes", biomesFolder, regionKey, warnings);
if (anyBiome == 0) {
blockingErrors.add("Region '" + regionKey + "' has no resolvable biomes.");
}
resolvedRegions++;
}
if (resolvedRegions == 0) {
blockingErrors.add("Dimension '" + dimensionKey + "' has no resolvable regions.");
}
}
}
static void validateImportedStructurePolicy(String dimensionKey, JSONObject dimension,
List<String> blockingErrors) {
if (!dimension.has("importedStructures")) {
return;
}
if (dimension.isNull("importedStructures")) {
blockingErrors.add("Dimension '" + dimensionKey + "' importedStructures must be an object.");
return;
}
JSONObject policy = dimension.optJSONObject("importedStructures");
if (policy == null) {
blockingErrors.add("Dimension '" + dimensionKey + "' importedStructures must be an object.");
return;
}
if (policy.has("mode")) {
blockingErrors.add("Dimension '" + dimensionKey
+ "' importedStructures.mode is not supported. Native structures are enabled by default; list explicit denials in importedStructures.disabled.");
}
if (policy.has("enabled")) {
blockingErrors.add("Dimension '" + dimensionKey
+ "' importedStructures.enabled is not supported. Native structures are enabled by default; list explicit denials in importedStructures.disabled.");
}
validateStructureKeyList(dimensionKey, policy, "disabled", blockingErrors);
JSONArray adjustments = policy.optJSONArray("adjustments");
if (adjustments == null) {
if (policy.has("adjustments")) {
blockingErrors.add("Dimension '" + dimensionKey
+ "' importedStructures.adjustments must be an array.");
}
return;
}
for (int index = 0; index < adjustments.length(); index++) {
JSONObject adjustment = adjustments.optJSONObject(index);
if (adjustment == null) {
blockingErrors.add("Dimension '" + dimensionKey
+ "' importedStructures.adjustments has a non-object entry at index " + index + ".");
continue;
}
validateStructureKeyList(dimensionKey, adjustment, "match", blockingErrors);
validateAdjustmentYBand(dimensionKey, adjustment, index, blockingErrors);
PackStructurePlacementValidator.validateNativeTerrain("Dimension '" + dimensionKey
+ "' importedStructures.adjustments[" + index + "]", adjustment, blockingErrors);
}
}
private static void validateAdjustmentYBand(String dimensionKey, JSONObject adjustment, int index,
List<String> blockingErrors) {
if (!adjustment.has("yBand") || adjustment.opt("yBand") == JSONObject.NULL) {
return;
}
String path = "Dimension '" + dimensionKey
+ "' importedStructures.adjustments[" + index + "].yBand";
JSONObject band = adjustment.optJSONObject("yBand");
if (band == null) {
blockingErrors.add(path + " must be an object.");
return;
}
PackJsonFieldChecks.validateOptionalIntegerRange(path, band, "min", -4064, 4064, blockingErrors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, band, "max", -4064, 4064, blockingErrors);
}
private static void validateStructureKeyList(String dimensionKey, JSONObject owner, String field,
List<String> blockingErrors) {
if (!owner.has(field)) {
return;
}
JSONArray keys = owner.optJSONArray(field);
if (keys == null) {
blockingErrors.add("Dimension '" + dimensionKey + "' structure policy field '"
+ field + "' must be an array.");
return;
}
for (int index = 0; index < keys.length(); index++) {
Object value = keys.opt(index);
if (!(value instanceof String key) || key.isBlank()) {
blockingErrors.add("Dimension '" + dimensionKey + "' structure policy field '"
+ field + "' has a blank or non-string entry at index " + index + ".");
}
}
}
private static int countBiomeRefs(JSONObject regionJson, String field, File biomesFolder, String regionKey, List<String> warnings) {
JSONArray arr = regionJson.optJSONArray(field);
if (arr == null) {
return 0;
}
int resolved = 0;
for (int i = 0; i < arr.length(); i++) {
String biomeKey = arr.optString(i, null);
if (biomeKey == null || biomeKey.isBlank()) {
continue;
}
File biomeFile = new File(biomesFolder, biomeKey + ".json");
if (!biomeFile.isFile()) {
warnings.add("Region '" + regionKey + "' references missing biome '" + biomeKey + "' in " + field + ".");
continue;
}
resolved++;
}
return resolved;
}
}
@@ -72,8 +72,9 @@ public final class PackDownloader {
ZipUtil.unpack(zip, work);
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
feedback.accept(IrisLanguage.plain(PackDownloadMessages.UNPACK_FAILED));
IO.delete(work);
return null;
}
File dir = null;
File[] zipFiles = work.listFiles();
@@ -0,0 +1,101 @@
/*
* 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.pack;
import art.arcane.volmlib.util.json.JSONObject;
import java.util.List;
import java.util.Set;
final class PackJsonFieldChecks {
private PackJsonFieldChecks() {
}
static void validateOptionalBoolean(String path, JSONObject object, String field,
List<String> blockingErrors) {
if (!object.has(field) || object.opt(field) == JSONObject.NULL) {
return;
}
if (!(object.opt(field) instanceof Boolean)) {
blockingErrors.add(path + "." + field + " must be a boolean.");
}
}
static void validateOptionalResourceKey(String path, JSONObject object, String field,
boolean allowNone, List<String> blockingErrors) {
if (!object.has(field)) {
return;
}
Object rawValue = object.opt(field);
if (!(rawValue instanceof String value)) {
blockingErrors.add(path + "." + field + " must be a string.");
return;
}
String normalized = value.trim();
if (normalized.isEmpty() || allowNone && "NONE".equalsIgnoreCase(normalized)) {
return;
}
if (!PackValidator.RESOURCE_KEY_PATTERN.matcher(normalized).matches()) {
blockingErrors.add(path + "." + field + " must be a namespaced registry key.");
}
}
static void validateOptionalIntegerRange(String path, JSONObject object, String field,
int minimum, int maximum,
List<String> blockingErrors) {
if (!object.has(field) || object.opt(field) == JSONObject.NULL) {
return;
}
Integer value = PackLootValidator.lootInteger(object, field, minimum, path, blockingErrors);
PackLootValidator.requireMinimum(path + "." + field, value, minimum, blockingErrors);
PackLootValidator.requireMaximum(path + "." + field, value, maximum, blockingErrors);
}
static void validateOptionalDoubleRange(String path, JSONObject object, String field,
double minimum, double maximum,
List<String> blockingErrors) {
if (!object.has(field) || object.opt(field) == JSONObject.NULL) {
return;
}
String fieldPath = path + "." + field;
Object rawValue = object.opt(field);
if (!(rawValue instanceof Number number) || !Double.isFinite(number.doubleValue())) {
blockingErrors.add(fieldPath + " must be a number.");
return;
}
double value = number.doubleValue();
if (value < minimum) {
blockingErrors.add(fieldPath + " must be at least " + minimum + ".");
}
if (value > maximum) {
blockingErrors.add(fieldPath + " must be at most " + maximum + ".");
}
}
static void validateOptionalEnum(String path, JSONObject object, String field,
Set<String> values, List<String> blockingErrors) {
if (!object.has(field)) {
return;
}
Object rawValue = object.opt(field);
if (!(rawValue instanceof String value) || !values.contains(value)) {
blockingErrors.add(path + "." + field + " must be one of " + values + ".");
}
}
}
@@ -0,0 +1,240 @@
/*
* 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.pack;
import art.arcane.iris.engine.object.IrisLoot;
import art.arcane.iris.engine.object.IrisLootReference;
import art.arcane.iris.engine.object.IrisLootTable;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import java.io.File;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
final class PackLootValidator {
private PackLootValidator() {
}
static List<String> validateLootGraph(File packFolder) {
List<String> blockingErrors = new ArrayList<>();
if (packFolder == null || !packFolder.isDirectory()) {
return blockingErrors;
}
File lootFolder = new File(packFolder, PackValidator.LOOT_FOLDER);
Set<String> lootKeys = ContentKeyValidator.deriveRegistrantKeysExact(lootFolder);
if (lootFolder.isDirectory()) {
List<File> lootFiles = PackValidationIo.listJsonRecursive(lootFolder);
lootFiles.sort(Comparator.comparing(File::getPath));
for (File lootFile : lootFiles) {
String lootKey = PackValidationIo.deriveKey(lootFolder, lootFile);
JSONObject table = PackStructurePlacementValidator.readGraphJson(
lootFile, "Loot table", lootKey, blockingErrors);
if (table != null) {
validateLootTable(lootKey, table, blockingErrors);
}
}
}
for (String folderName : PackValidator.STRUCTURE_HOST_FOLDERS) {
File resourceFolder = new File(packFolder, folderName);
if (!resourceFolder.isDirectory()) {
continue;
}
List<File> resourceFiles = PackValidationIo.listJsonRecursive(resourceFolder);
resourceFiles.sort(Comparator.comparing(File::getPath));
for (File resourceFile : resourceFiles) {
JSONObject resource = PackValidationIo.readJson(resourceFile);
if (resource == null || !resource.has("loot")) {
continue;
}
String resourceType = PackStructurePlacementValidator.structureHostType(folderName);
String resourceKey = PackValidationIo.deriveKey(resourceFolder, resourceFile);
validateLootReference(resourceType, resourceKey, resource.opt("loot"), lootKeys, blockingErrors);
}
}
return blockingErrors;
}
private static void validateLootTable(String lootKey, JSONObject table, List<String> blockingErrors) {
String path = "Loot table '" + lootKey + "'";
Integer rarity = lootInteger(table, "rarity", 1, path, blockingErrors);
Integer minimumPicked = lootInteger(table, "minPicked", 1, path, blockingErrors);
Integer maximumPicked = lootInteger(table, "maxPicked", 5, path, blockingErrors);
Integer maximumTries = lootInteger(table, "maxTries", 10, path, blockingErrors);
requireMinimum(path + ".rarity", rarity, 1, blockingErrors);
requireMinimum(path + ".minPicked", minimumPicked, 0, blockingErrors);
requireMinimum(path + ".maxPicked", maximumPicked, 1, blockingErrors);
requireMinimum(path + ".maxTries", maximumTries, 1, blockingErrors);
requireMaximum(path + ".minPicked", minimumPicked, IrisLootTable.MAX_PICKED, blockingErrors);
requireMaximum(path + ".maxPicked", maximumPicked, IrisLootTable.MAX_PICKED, blockingErrors);
requireMaximum(path + ".maxTries", maximumTries, IrisLootTable.MAX_TRIES, blockingErrors);
requireOrdered(path + ".minPicked", minimumPicked, path + ".maxPicked", maximumPicked, blockingErrors);
JSONArray entries = table.optJSONArray("loot");
if (entries == null || entries.length() == 0) {
blockingErrors.add(path + ".loot must be a non-empty array.");
return;
}
for (int entryIndex = 0; entryIndex < entries.length(); entryIndex++) {
JSONObject entry = entries.optJSONObject(entryIndex);
String entryPath = path + ".loot[" + entryIndex + "]";
if (entry == null) {
blockingErrors.add(entryPath + " must be an object.");
continue;
}
String type = entry.optString("type", "").trim();
if (type.isEmpty()) {
blockingErrors.add(entryPath + ".type must not be blank.");
}
Integer entryRarity = lootInteger(entry, "rarity", 1, entryPath, blockingErrors);
Integer minimumAmount = lootInteger(entry, "minAmount", 1, entryPath, blockingErrors);
Integer maximumAmount = lootInteger(entry, "maxAmount", 1, entryPath, blockingErrors);
requireMinimum(entryPath + ".rarity", entryRarity, 1, blockingErrors);
requireMinimum(entryPath + ".minAmount", minimumAmount, 1, blockingErrors);
requireMinimum(entryPath + ".maxAmount", maximumAmount, 1, blockingErrors);
requireMaximum(entryPath + ".minAmount", minimumAmount, IrisLoot.MAX_AMOUNT, blockingErrors);
requireMaximum(entryPath + ".maxAmount", maximumAmount, IrisLoot.MAX_AMOUNT, blockingErrors);
requireOrdered(entryPath + ".minAmount", minimumAmount,
entryPath + ".maxAmount", maximumAmount, blockingErrors);
validateLootEnchantments(entryPath, entry.opt("enchantments"), blockingErrors);
}
}
private static void validateLootEnchantments(String entryPath, Object rawEnchantments,
List<String> blockingErrors) {
if (rawEnchantments == null || rawEnchantments == JSONObject.NULL) {
return;
}
if (!(rawEnchantments instanceof JSONArray enchantments)) {
blockingErrors.add(entryPath + ".enchantments must be an array.");
return;
}
for (int enchantmentIndex = 0; enchantmentIndex < enchantments.length(); enchantmentIndex++) {
JSONObject enchantment = enchantments.optJSONObject(enchantmentIndex);
String enchantmentPath = entryPath + ".enchantments[" + enchantmentIndex + "]";
if (enchantment == null) {
blockingErrors.add(enchantmentPath + " must be an object.");
continue;
}
if (enchantment.optString("enchantment", "").isBlank()) {
blockingErrors.add(enchantmentPath + ".enchantment must not be blank.");
}
Integer minimumLevel = lootInteger(enchantment, "minLevel", 1, enchantmentPath, blockingErrors);
Integer maximumLevel = lootInteger(enchantment, "maxLevel", 1, enchantmentPath, blockingErrors);
requireMinimum(enchantmentPath + ".minLevel", minimumLevel, 1, blockingErrors);
requireMinimum(enchantmentPath + ".maxLevel", maximumLevel, 1, blockingErrors);
requireOrdered(enchantmentPath + ".minLevel", minimumLevel,
enchantmentPath + ".maxLevel", maximumLevel, blockingErrors);
if (enchantment.has("chance")) {
Object rawChance = enchantment.opt("chance");
if (!(rawChance instanceof Number number)
|| !Double.isFinite(number.doubleValue())
|| number.doubleValue() < 0D
|| number.doubleValue() > 1D) {
blockingErrors.add(enchantmentPath + ".chance must be a finite number from 0 to 1.");
}
}
}
}
private static void validateLootReference(String resourceType, String resourceKey, Object rawLoot,
Set<String> lootKeys, List<String> blockingErrors) {
String path = resourceType + " '" + resourceKey + "'.loot";
if (!(rawLoot instanceof JSONObject reference)) {
blockingErrors.add(path + " must be an object.");
return;
}
if (reference.has("mode")) {
Object rawMode = reference.opt("mode");
if (!(rawMode instanceof String mode)
|| !Set.of("ADD", "CLEAR", "REPLACE", "FALLBACK").contains(mode)) {
blockingErrors.add(path + ".mode must be ADD, CLEAR, REPLACE, or FALLBACK.");
}
}
if (reference.has("multiplier")) {
Object rawMultiplier = reference.opt("multiplier");
if (!(rawMultiplier instanceof Number multiplier)
|| !Double.isFinite(multiplier.doubleValue())
|| multiplier.doubleValue() < 0D
|| multiplier.doubleValue() > IrisLootReference.MAX_MULTIPLIER) {
blockingErrors.add(path + ".multiplier must be a finite number from 0 to "
+ (int) IrisLootReference.MAX_MULTIPLIER + ".");
}
}
if (!reference.has("tables")) {
return;
}
JSONArray tables = reference.optJSONArray("tables");
if (tables == null) {
blockingErrors.add(path + ".tables must be an array.");
return;
}
for (int tableIndex = 0; tableIndex < tables.length(); tableIndex++) {
Object rawTableKey = tables.opt(tableIndex);
if (!(rawTableKey instanceof String tableKey) || tableKey.isBlank()) {
blockingErrors.add(path + ".tables[" + tableIndex + "] must name a loot table.");
} else if (!lootKeys.contains(tableKey)) {
blockingErrors.add(path + ".tables[" + tableIndex
+ "] references missing loot table '" + tableKey + "'.");
}
}
}
static Integer lootInteger(JSONObject object, String field, int defaultValue,
String path, List<String> blockingErrors) {
if (!object.has(field)) {
return defaultValue;
}
Object rawValue = object.opt(field);
if (!(rawValue instanceof Number number)
|| !Double.isFinite(number.doubleValue())
|| number.doubleValue() != Math.rint(number.doubleValue())
|| number.longValue() < Integer.MIN_VALUE
|| number.longValue() > Integer.MAX_VALUE) {
blockingErrors.add(path + "." + field + " must be an integer.");
return null;
}
return number.intValue();
}
static void requireMinimum(String fieldPath, Integer value, int minimum,
List<String> blockingErrors) {
if (value != null && value < minimum) {
blockingErrors.add(fieldPath + " must be at least " + minimum + ".");
}
}
static void requireMaximum(String fieldPath, Integer value, int maximum,
List<String> blockingErrors) {
if (value != null && value > maximum) {
blockingErrors.add(fieldPath + " must be at most " + maximum + ".");
}
}
static void requireOrdered(String minimumPath, Integer minimum, String maximumPath,
Integer maximum, List<String> blockingErrors) {
if (minimum != null && maximum != null && minimum > maximum) {
blockingErrors.add(minimumPath + " must not exceed " + maximumPath + ".");
}
}
}
@@ -0,0 +1,341 @@
/*
* 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.pack;
import art.arcane.iris.engine.object.ObjectPlaceMode;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import java.io.File;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
final class PackNativeStructureValidator {
private PackNativeStructureValidator() {
}
static List<String> validateNativeStructureReplacements(
File packFolder,
Set<String> replacementOutputStructures,
Map<String, List<StructureGraphPackValidator.SampledVerticalEnvelope>> sampledVerticalEnvelopes
) {
List<String> blockingErrors = new ArrayList<>();
if (packFolder == null || !packFolder.isDirectory()) {
return blockingErrors;
}
Set<String> viableStructures = replacementOutputStructures == null
? Set.of() : replacementOutputStructures;
Map<String, List<StructureGraphPackValidator.SampledVerticalEnvelope>> verticalEnvelopes =
sampledVerticalEnvelopes == null ? Map.of() : sampledVerticalEnvelopes;
File structuresFolder = new File(packFolder, PackValidator.STRUCTURES_FOLDER);
Map<String, JSONObject> structures = new HashMap<>();
for (File structureFile : PackValidationIo.listJsonRecursive(structuresFolder)) {
JSONObject structure = PackValidationIo.readJson(structureFile);
if (structure != null) {
structures.put(PackValidationIo.deriveKey(structuresFolder, structureFile), structure);
}
}
for (String folderName : PackValidator.STRUCTURE_HOST_FOLDERS) {
File resourceFolder = new File(packFolder, folderName);
if (!resourceFolder.isDirectory()) {
continue;
}
List<File> resourceFiles = PackValidationIo.listJsonRecursive(resourceFolder);
resourceFiles.sort(Comparator.comparing(File::getPath));
String resourceType = PackStructurePlacementValidator.structureHostType(folderName);
for (File resourceFile : resourceFiles) {
JSONObject resource = PackValidationIo.readJson(resourceFile);
if (resource == null) {
continue;
}
JSONArray placements = resource.optJSONArray("structures");
if (placements == null) {
continue;
}
String resourceKey = PackValidationIo.deriveKey(resourceFolder, resourceFile);
for (int placementIndex = 0; placementIndex < placements.length(); placementIndex++) {
JSONObject placement = placements.optJSONObject(placementIndex);
if (placement == null || !placement.has("nativeSuppression")) {
continue;
}
Object rawSuppression = placement.opt("nativeSuppression");
if (!(rawSuppression instanceof String suppression)) {
blockingErrors.add(resourceType + " '" + resourceKey + "' structures["
+ placementIndex + "].nativeSuppression must be NONE or REPLACE_SOURCE.");
continue;
}
if ("NONE".equals(suppression)) {
continue;
}
if (!"REPLACE_SOURCE".equals(suppression)) {
blockingErrors.add(resourceType + " '" + resourceKey + "' structures["
+ placementIndex + "].nativeSuppression has unsupported value '"
+ suppression + "'. Use NONE or REPLACE_SOURCE.");
continue;
}
if (!PackValidator.DIMENSIONS_FOLDER.equals(folderName)) {
blockingErrors.add(resourceType + " '" + resourceKey + "' structures["
+ placementIndex + "] requests REPLACE_SOURCE, but native replacement is only"
+ " valid on dimension-level placements.");
continue;
}
JSONArray references = placement.optJSONArray("structures");
JSONArray nativeStructures = placement.optJSONArray("nativeStructures");
if (nativeStructures != null && nativeStructures.length() > 0) {
continue;
}
if (references == null || references.length() == 0) {
blockingErrors.add("Dimension '" + resourceKey + "' structures[" + placementIndex
+ "] requests REPLACE_SOURCE without any structure backend.");
continue;
}
for (int referenceIndex = 0; referenceIndex < references.length(); referenceIndex++) {
Object rawReference = references.opt(referenceIndex);
if (!(rawReference instanceof String structureKey) || structureKey.isBlank()) {
blockingErrors.add("Dimension '" + resourceKey + "' structures[" + placementIndex
+ "].structures[" + referenceIndex
+ "] must name an Iris structure for REPLACE_SOURCE.");
continue;
}
JSONObject structure = structures.get(structureKey);
if (structure == null) {
blockingErrors.add("Dimension '" + resourceKey + "' structures[" + placementIndex
+ "] cannot REPLACE_SOURCE with missing or invalid structure '"
+ structureKey + "'.");
continue;
}
String vanillaSource = structure.optString("vanillaSource", "").trim();
if (!PackValidator.RESOURCE_KEY_PATTERN.matcher(vanillaSource).matches()) {
blockingErrors.add("Dimension '" + resourceKey + "' structures[" + placementIndex
+ "] requests REPLACE_SOURCE for structure '" + structureKey
+ "', but its vanillaSource is not a valid namespaced registry key.");
}
if (!viableStructures.contains(structureKey)) {
blockingErrors.add("Dimension '" + resourceKey + "' structures[" + placementIndex
+ "] requests REPLACE_SOURCE for structure '" + structureKey
+ "', but that structure is not runtime-viable. Native generation will not"
+ " be used as a fallback.");
continue;
}
validateReplacementVerticalEnvelope(
resourceKey,
resource,
placementIndex,
placement,
structureKey,
structure,
verticalEnvelopes.get(structureKey),
blockingErrors);
}
}
}
}
return blockingErrors;
}
private static void validateReplacementVerticalEnvelope(
String dimensionKey,
JSONObject dimension,
int placementIndex,
JSONObject placement,
String structureKey,
JSONObject structure,
List<StructureGraphPackValidator.SampledVerticalEnvelope> sampledVerticalEnvelopes,
List<String> blockingErrors
) {
String context = "Dimension '" + dimensionKey + "' structures[" + placementIndex
+ "] REPLACE_SOURCE structure '" + structureKey + "'";
if (sampledVerticalEnvelopes == null || sampledVerticalEnvelopes.isEmpty()) {
blockingErrors.add(context + " has no sampled vertical envelope. Native generation will not"
+ " be used as a fallback.");
return;
}
DimensionVerticalBounds worldBounds = resolveDimensionVerticalBounds(dimension, context, blockingErrors);
PlacementVerticalBounds placementBounds = resolvePlacementVerticalBounds(placement, context, blockingErrors);
ObjectPlaceMode placeMode = resolvePlaceMode(structure, context, blockingErrors);
if (worldBounds == null || placementBounds == null || placeMode == null) {
return;
}
for (StructureGraphPackValidator.SampledVerticalEnvelope sampled : sampledVerticalEnvelopes) {
boolean exactY = placementBounds.underground()
|| sampled.pieceCount() > 1
|| placeMode == ObjectPlaceMode.STRUCTURE_PIECE
|| placeMode == ObjectPlaceMode.FLOATING;
if (!exactY) {
continue;
}
long minimumYOffset = sampled.minimumYOffset();
long maximumYOffset = sampled.maximumYOffset();
boolean surfaceAligned = !placementBounds.underground()
&& sampled.pieceCount() > 1
&& placeMode != ObjectPlaceMode.STRUCTURE_PIECE
&& placeMode != ObjectPlaceMode.FLOATING;
if (surfaceAligned) {
maximumYOffset -= minimumYOffset;
minimumYOffset = 0L;
}
boolean fitsConfiguredRange;
if (placementBounds.underground()) {
long minimumAnchor = Math.max(
Math.max(placementBounds.minimumY(), worldBounds.minimumY()),
worldBounds.minimumY() - minimumYOffset);
long maximumAnchor = Math.min(
Math.min(placementBounds.maximumY(), worldBounds.maximumY()),
worldBounds.maximumY() - maximumYOffset);
fitsConfiguredRange = minimumAnchor <= maximumAnchor;
} else {
long minimumTerrainY = Math.max(placementBounds.minimumY(), worldBounds.minimumY());
long maximumTerrainY = Math.min(placementBounds.maximumY(), worldBounds.maximumY());
fitsConfiguredRange = minimumTerrainY <= maximumTerrainY
&& minimumTerrainY + minimumYOffset >= worldBounds.minimumY()
&& maximumTerrainY + maximumYOffset <= worldBounds.maximumY();
}
if (fitsConfiguredRange) {
continue;
}
String alignment = surfaceAligned ? "surface-aligned " : "";
blockingErrors.add(context + " sampled seed " + sampled.seed() + " has an " + alignment
+ "exact-Y piece envelope " + minimumYOffset + ".." + maximumYOffset
+ " relative to its anchor, which cannot fit placement band "
+ placementBounds.minimumY() + ".." + placementBounds.maximumY()
+ " inside writable world " + worldBounds.minimumY() + ".." + worldBounds.maximumY()
+ ". Native generation will not be used as a fallback.");
return;
}
}
private static DimensionVerticalBounds resolveDimensionVerticalBounds(
JSONObject dimension,
String context,
List<String> blockingErrors
) {
long dimensionMinimum = -64L;
long dimensionMaximum = 320L;
if (dimension.has("dimensionHeight")) {
JSONObject dimensionHeight = dimension.optJSONObject("dimensionHeight");
if (dimensionHeight == null) {
blockingErrors.add(context + " cannot validate its vertical envelope because dimensionHeight"
+ " must be an object.");
return null;
}
Long configuredMinimum = integralJsonNumber(dimensionHeight, "min", 16L);
Long configuredMaximum = integralJsonNumber(dimensionHeight, "max", 32L);
if (configuredMinimum == null || configuredMaximum == null) {
blockingErrors.add(context + " cannot validate its vertical envelope because dimensionHeight"
+ " min and max must be finite integer values.");
return null;
}
dimensionMinimum = configuredMinimum;
dimensionMaximum = configuredMaximum;
}
long writableMinimum = dimensionMinimum + 1L;
long writableMaximum = dimensionMaximum - 1L;
if (writableMinimum > writableMaximum) {
blockingErrors.add(context + " cannot validate its vertical envelope because dimensionHeight "
+ dimensionMinimum + ".." + dimensionMaximum + " has no writable structure range.");
return null;
}
return new DimensionVerticalBounds(writableMinimum, writableMaximum);
}
private static PlacementVerticalBounds resolvePlacementVerticalBounds(
JSONObject placement,
String context,
List<String> blockingErrors
) {
boolean underground = false;
if (placement.has("underground")) {
Object rawUnderground = placement.opt("underground");
if (!(rawUnderground instanceof Boolean configuredUnderground)) {
blockingErrors.add(context + " cannot validate its vertical envelope because underground"
+ " must be true or false.");
return null;
}
underground = configuredUnderground;
}
Long configuredMinimum = integralJsonNumber(placement, "minHeight", -2032L);
Long configuredMaximum = integralJsonNumber(placement, "maxHeight", 2032L);
if (configuredMinimum == null || configuredMaximum == null) {
blockingErrors.add(context + " cannot validate its vertical envelope because minHeight and"
+ " maxHeight must be finite integer values.");
return null;
}
long minimumY = underground
? Math.min(configuredMinimum, configuredMaximum) : configuredMinimum;
long maximumY = underground
? Math.max(configuredMinimum, configuredMaximum) : configuredMaximum;
return new PlacementVerticalBounds(minimumY, maximumY, underground);
}
private static ObjectPlaceMode resolvePlaceMode(
JSONObject structure,
String context,
List<String> blockingErrors
) {
if (!structure.has("placeMode")) {
return ObjectPlaceMode.STRUCTURE_PIECE;
}
Object rawPlaceMode = structure.opt("placeMode");
if (!(rawPlaceMode instanceof String placeModeName)) {
blockingErrors.add(context + " cannot validate its vertical envelope because placeMode must name"
+ " an Iris object place mode.");
return null;
}
try {
return ObjectPlaceMode.valueOf(placeModeName);
} catch (IllegalArgumentException exception) {
blockingErrors.add(context + " cannot validate its vertical envelope because placeMode '"
+ placeModeName + "' is not supported.");
return null;
}
}
private static Long integralJsonNumber(JSONObject owner, String field, long defaultValue) {
if (!owner.has(field)) {
return defaultValue;
}
Object rawValue = owner.opt(field);
if (!(rawValue instanceof Number number)) {
return null;
}
double value = number.doubleValue();
if (!Double.isFinite(value) || value != Math.rint(value)
|| value < Integer.MIN_VALUE || value > Integer.MAX_VALUE) {
return null;
}
return (long) value;
}
private record DimensionVerticalBounds(long minimumY, long maximumY) {
}
private record PlacementVerticalBounds(long minimumY, long maximumY, boolean underground) {
}
}
@@ -0,0 +1,227 @@
/*
* 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.pack;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import java.io.File;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
final class PackObjectSurfaceValidator {
private PackObjectSurfaceValidator() {
}
static Set<String> collectPlacedStructureKeys(File packFolder) {
Set<String> structureKeys = new LinkedHashSet<>();
if (packFolder == null || !packFolder.isDirectory()) {
return structureKeys;
}
for (String folderName : PackValidator.STRUCTURE_HOST_FOLDERS) {
File resourceFolder = new File(packFolder, folderName);
if (!resourceFolder.isDirectory()) {
continue;
}
for (File resourceFile : PackValidationIo.listJsonRecursive(resourceFolder)) {
JSONObject resource = PackValidationIo.readJson(resourceFile);
if (resource == null) {
continue;
}
JSONArray placements = resource.optJSONArray("structures");
if (placements == null) {
continue;
}
for (int placementIndex = 0; placementIndex < placements.length(); placementIndex++) {
JSONObject placement = placements.optJSONObject(placementIndex);
if (placement == null) {
continue;
}
JSONArray references = placement.optJSONArray("structures");
if (references == null) {
continue;
}
for (int referenceIndex = 0; referenceIndex < references.length(); referenceIndex++) {
Object rawReference = references.opt(referenceIndex);
if (rawReference instanceof String structureKey && !structureKey.isBlank()) {
structureKeys.add(structureKey);
}
}
}
}
}
return Set.copyOf(structureKeys);
}
static List<String> validateUnsupportedStructureTransforms(File packFolder) {
List<String> blockingErrors = new ArrayList<>();
if (packFolder == null || !packFolder.isDirectory()) {
return blockingErrors;
}
for (String folderName : PackValidator.STRUCTURE_HOST_FOLDERS) {
File resourceFolder = new File(packFolder, folderName);
if (!resourceFolder.isDirectory()) {
continue;
}
List<File> resourceFiles = PackValidationIo.listJsonRecursive(resourceFolder);
resourceFiles.sort(Comparator.comparing(File::getPath));
String resourceType = PackStructurePlacementValidator.structureHostType(folderName);
for (File resourceFile : resourceFiles) {
String resourceKey = PackValidationIo.deriveKey(resourceFolder, resourceFile);
JSONObject resource = PackStructurePlacementValidator.readGraphJson(
resourceFile, resourceType, resourceKey, blockingErrors);
if (resource == null) {
continue;
}
JSONArray placements = resource.optJSONArray("structures");
if (placements == null) {
continue;
}
for (int placementIndex = 0; placementIndex < placements.length(); placementIndex++) {
JSONObject placement = placements.optJSONObject(placementIndex);
if (placement == null) {
continue;
}
for (String field : PackValidator.UNSUPPORTED_STRUCTURE_TRANSFORM_FIELDS) {
if (placement.has(field)) {
blockingErrors.add(resourceType + " '" + resourceKey + "' structures[" + placementIndex
+ "] declares unsupported field '" + field
+ "'. Structure placement transforms are not supported; remove the field.");
}
}
}
}
}
return blockingErrors;
}
static List<String> validateStructureGraph(File packFolder) {
List<String> blockingErrors = new ArrayList<>();
if (packFolder == null || !packFolder.isDirectory()) {
return blockingErrors;
}
File structuresFolder = new File(packFolder, PackValidator.STRUCTURES_FOLDER);
File poolsFolder = new File(packFolder, PackValidator.JIGSAW_POOLS_FOLDER);
File piecesFolder = new File(packFolder, PackValidator.JIGSAW_PIECES_FOLDER);
File objectsFolder = new File(packFolder, PackValidator.OBJECTS_FOLDER);
Set<String> structureKeys = ContentKeyValidator.deriveRegistrantKeysExact(structuresFolder);
Set<String> poolKeys = ContentKeyValidator.deriveRegistrantKeysExact(poolsFolder);
Set<String> pieceKeys = ContentKeyValidator.deriveRegistrantKeysExact(piecesFolder);
Set<String> objectKeys = ContentKeyValidator.deriveObjectKeysExact(objectsFolder);
PackStructurePlacementValidator.validateStructurePlacements(packFolder, structureKeys, blockingErrors);
PackStructurePlacementValidator.validateStructureStartPools(structuresFolder, poolKeys, blockingErrors);
PackStructurePlacementValidator.validateJigsawPools(poolsFolder, poolKeys, pieceKeys, blockingErrors);
PackStructurePlacementValidator.validateJigsawPieces(piecesFolder, poolKeys, objectKeys, blockingErrors);
return blockingErrors;
}
static List<String> validateRemovedWorldgenFields(File packFolder) {
List<String> blockingErrors = new ArrayList<>();
if (packFolder == null || !packFolder.isDirectory()) {
return blockingErrors;
}
for (String folderName : PackValidator.STRUCTURE_HOST_FOLDERS) {
File resourceFolder = new File(packFolder, folderName);
if (!resourceFolder.isDirectory()) {
continue;
}
List<File> resourceFiles = PackValidationIo.listJsonRecursive(resourceFolder);
resourceFiles.sort(Comparator.comparing(File::getPath));
String resourceType = PackStructurePlacementValidator.structureHostType(folderName);
for (File resourceFile : resourceFiles) {
JSONObject resource = PackValidationIo.readJson(resourceFile);
if (resource == null) {
continue;
}
String resourceKey = PackValidationIo.deriveKey(resourceFolder, resourceFile);
for (String field : PackValidator.REMOVED_WORLDGEN_FIELDS) {
if (resource.has(field)) {
blockingErrors.add(resourceType + " '" + resourceKey + "' declares removed field '"
+ field + "'. Remove it because fluid-body generation is not supported.");
}
}
}
}
return blockingErrors;
}
static List<String> validateObjectSurfaceSupport(File packFolder) {
List<String> blockingErrors = new ArrayList<>();
if (packFolder == null || !packFolder.isDirectory()) {
return blockingErrors;
}
for (String folderName : PackValidator.STRUCTURE_HOST_FOLDERS) {
File resourceFolder = new File(packFolder, folderName);
if (!resourceFolder.isDirectory()) {
continue;
}
List<File> resourceFiles = PackValidationIo.listJsonRecursive(resourceFolder);
resourceFiles.sort(Comparator.comparing(File::getPath));
String resourceType = PackStructurePlacementValidator.structureHostType(folderName);
for (File resourceFile : resourceFiles) {
JSONObject resource = PackValidationIo.readJson(resourceFile);
if (resource == null) {
continue;
}
String path = resourceType + " '" + PackValidationIo.deriveKey(resourceFolder, resourceFile) + "'";
if ("dimensions".equals(folderName)) {
PackJsonFieldChecks.validateOptionalIntegerRange(
path, resource, "objectSurfaceSupportBuffer", 0, 16, blockingErrors);
PackJsonFieldChecks.validateOptionalBoolean(
path, resource, "requireObjectSurfaceSupport", blockingErrors);
}
validateObjectPlacementSurfaceSupport(path, resource.optJSONArray("objects"), blockingErrors);
}
}
return blockingErrors;
}
private static void validateObjectPlacementSurfaceSupport(String path, JSONArray placements,
List<String> blockingErrors) {
if (placements == null) {
return;
}
for (int i = 0; i < placements.length(); i++) {
JSONObject placement = placements.optJSONObject(i);
if (placement == null) {
continue;
}
String placementPath = path + ".objects[" + i + "]";
if (placement.has("surfaceOpeningClearance")) {
blockingErrors.add(placementPath + " declares removed field 'surfaceOpeningClearance'. "
+ "Use surfaceSupportBuffer instead.");
}
PackJsonFieldChecks.validateOptionalIntegerRange(
placementPath, placement, "surfaceSupportBuffer", 0, 16, blockingErrors);
PackJsonFieldChecks.validateOptionalIntegerRange(
placementPath, placement, "surfaceSupportDepth", 1, 16, blockingErrors);
PackJsonFieldChecks.validateOptionalBoolean(
placementPath, placement, "requireSurfaceSupport", blockingErrors);
}
}
}
@@ -0,0 +1,343 @@
/*
* 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.pack;
import art.arcane.iris.engine.object.IrisBiomeCustomSpawnType;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformEntityType;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
final class PackSpawnValidator {
private PackSpawnValidator() {
}
static List<String> validateSpawnerEntityReferences(File spawnersFolder, File entitiesFolder) {
List<String> blockingErrors = new ArrayList<>();
if (spawnersFolder == null || !spawnersFolder.isDirectory()) {
return blockingErrors;
}
Path entityRoot = entitiesFolder.toPath().toAbsolutePath().normalize();
Set<Path> validEntityFiles = new HashSet<>();
Map<Path, String> invalidEntityFiles = new HashMap<>();
List<File> spawnerFiles = PackValidationIo.listJsonRecursive(spawnersFolder);
spawnerFiles.sort(Comparator.comparing(File::getPath));
for (File spawnerFile : spawnerFiles) {
String spawnerKey = PackValidationIo.deriveKey(spawnersFolder, spawnerFile);
JSONObject spawner;
try {
spawner = new JSONObject(Files.readString(spawnerFile.toPath(), StandardCharsets.UTF_8));
} catch (Throwable e) {
blockingErrors.add("Spawner '" + spawnerKey + "' has invalid JSON: " + e.getMessage());
continue;
}
validateSpawnerSpawnEntries(spawnerKey, spawner, "spawns", entityRoot,
validEntityFiles, invalidEntityFiles, blockingErrors);
validateSpawnerSpawnEntries(spawnerKey, spawner, "initialSpawns", entityRoot,
validEntityFiles, invalidEntityFiles, blockingErrors);
}
return blockingErrors;
}
private static void validateSpawnerSpawnEntries(String spawnerKey,
JSONObject spawner,
String field,
Path entityRoot,
Set<Path> validEntityFiles,
Map<Path, String> invalidEntityFiles,
List<String> blockingErrors) {
if (!spawner.has(field)) {
return;
}
JSONArray entries = spawner.optJSONArray(field);
if (entries == null) {
blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " must be an array.");
return;
}
for (int index = 0; index < entries.length(); index++) {
JSONObject entry = entries.optJSONObject(index);
if (entry == null) {
blockingErrors.add("Spawner '" + spawnerKey + "' " + field
+ " has a non-object entry at index " + index + ".");
continue;
}
if (!entry.has("entity") || entry.isNull("entity")) {
blockingErrors.add("Spawner '" + spawnerKey + "' " + field
+ " has an entry without an entity reference at index " + index + ".");
continue;
}
Object rawEntity = entry.get("entity");
if (!(rawEntity instanceof String entityKey)) {
blockingErrors.add("Spawner '" + spawnerKey + "' " + field
+ " entity reference at index " + index + " must be a string.");
continue;
}
if (entityKey.isBlank()) {
blockingErrors.add("Spawner '" + spawnerKey + "' " + field
+ " has a blank entity reference at index " + index + ".");
continue;
}
Path entityFile;
try {
if (entityKey.indexOf('\\') >= 0) {
throw new IllegalArgumentException("backslash path separators are not portable");
}
entityFile = entityRoot.resolve(entityKey + ".json").normalize();
} catch (RuntimeException e) {
blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " entry at index " + index
+ " has invalid entity reference '" + entityKey + "': " + e.getMessage());
continue;
}
if (!entityFile.startsWith(entityRoot)) {
blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " entry at index " + index
+ " has unsafe entity reference '" + entityKey + "'.");
continue;
}
if (!Files.isRegularFile(entityFile)) {
blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " entry at index " + index
+ " references missing entity '" + entityKey + "'.");
continue;
}
String invalidJson = invalidEntityFiles.get(entityFile);
if (invalidJson != null) {
blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " entry at index " + index
+ " references malformed entity '" + entityKey + "': " + invalidJson);
continue;
}
if (validEntityFiles.contains(entityFile)) {
continue;
}
try {
new JSONObject(Files.readString(entityFile, StandardCharsets.UTF_8));
validEntityFiles.add(entityFile);
} catch (Throwable e) {
String message = e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage();
invalidEntityFiles.put(entityFile, message);
blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " entry at index " + index
+ " references malformed entity '" + entityKey + "': " + message);
}
}
}
static List<String> validateCustomBiomeSpawns(File biomesFolder, Function<String, SpawnCategoryResolution> categoryResolver) {
List<String> blockingErrors = new ArrayList<>();
if (biomesFolder == null || !biomesFolder.isDirectory()) {
return blockingErrors;
}
List<File> biomeFiles = PackValidationIo.listJsonRecursive(biomesFolder);
biomeFiles.sort(Comparator.comparing(File::getPath));
for (File biomeFile : biomeFiles) {
String biomeKey = PackValidationIo.deriveKey(biomesFolder, biomeFile);
JSONObject biome;
try {
biome = new JSONObject(Files.readString(biomeFile.toPath(), StandardCharsets.UTF_8));
} catch (Throwable e) {
blockingErrors.add("Biome '" + biomeKey + "' has invalid JSON: " + e.getMessage());
continue;
}
JSONArray derivatives = biome.optJSONArray("customDerivitives");
if (derivatives == null) {
if (biome.has("customDerivitives") && !biome.isNull("customDerivitives")) {
blockingErrors.add("Biome '" + biomeKey + "' customDerivitives must be an array.");
}
continue;
}
for (int derivativeIndex = 0; derivativeIndex < derivatives.length(); derivativeIndex++) {
JSONObject derivative = derivatives.optJSONObject(derivativeIndex);
if (derivative == null) {
blockingErrors.add("Biome '" + biomeKey + "' has a non-object custom derivative at index " + derivativeIndex + ".");
continue;
}
validateCustomBiomeDerivativeTags(biomeKey, derivative, derivativeIndex, blockingErrors);
validateCustomBiomeDerivativeSpawns(
biomeKey, derivative, derivativeIndex, categoryResolver, blockingErrors);
}
}
return blockingErrors;
}
private static void validateCustomBiomeDerivativeTags(String biomeKey,
JSONObject derivative,
int derivativeIndex,
List<String> blockingErrors) {
if (!derivative.has("tags") || derivative.isNull("tags")) {
return;
}
String derivativeId = derivative.optString("id", "#" + derivativeIndex);
JSONArray tags = derivative.optJSONArray("tags");
if (tags == null) {
blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId
+ "' tags must be an array.");
return;
}
for (int tagIndex = 0; tagIndex < tags.length(); tagIndex++) {
Object rawTag = tags.opt(tagIndex);
if (!(rawTag instanceof String tag)) {
blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId
+ "' has a non-string tag at index " + tagIndex + ".");
continue;
}
String normalized = tag.trim().toLowerCase(Locale.ROOT);
if (normalized.indexOf(':') < 0) {
normalized = "minecraft:" + normalized;
}
if (!isSafeResourceKey(normalized)) {
blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId
+ "' has invalid tag '" + tag + "'.");
}
}
}
private static boolean isSafeResourceKey(String key) {
if (!PackValidator.RESOURCE_KEY_PATTERN.matcher(key).matches()) {
return false;
}
int separator = key.indexOf(':');
String[] segments = key.substring(separator + 1).split("/");
for (String segment : segments) {
if (segment.equals("..")) {
return false;
}
}
return true;
}
private static void validateCustomBiomeDerivativeSpawns(String biomeKey,
JSONObject derivative,
int derivativeIndex,
Function<String, SpawnCategoryResolution> categoryResolver,
List<String> blockingErrors) {
JSONArray spawns = derivative.optJSONArray("spawns");
if (spawns == null) {
if (derivative.has("spawns") && !derivative.isNull("spawns")) {
String derivativeId = derivative.optString("id", "#" + derivativeIndex);
blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId
+ "' spawns must be an array.");
}
return;
}
String derivativeId = derivative.optString("id", "#" + derivativeIndex);
for (int spawnIndex = 0; spawnIndex < spawns.length(); spawnIndex++) {
JSONObject spawn = spawns.optJSONObject(spawnIndex);
if (spawn == null) {
blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId
+ "' has a non-object spawn at index " + spawnIndex + ".");
continue;
}
String type = spawn.optString("type", "").trim().toLowerCase(Locale.ROOT);
if (type.isEmpty()) {
blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId
+ "' has a spawn without an entity type at index " + spawnIndex + ".");
continue;
}
String typeKey = type.indexOf(':') >= 0 ? type : "minecraft:" + type;
SpawnCategoryResolution resolution;
try {
resolution = categoryResolver == null ? null : categoryResolver.apply(typeKey);
} catch (Throwable e) {
IrisLogging.reportError("PackValidator failed to resolve spawn category for '" + typeKey + "'", e);
blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId
+ "' spawn category lookup failed for '" + typeKey + "': " + e.getMessage());
continue;
}
if (resolution != null && !resolution.entityKnown()) {
blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId
+ "' spawn references unknown entity type '" + typeKey + "'.");
continue;
}
String expectedGroup = resolution == null ? null : resolution.category();
String group = spawn.optString("group", "").trim();
if (group.isEmpty()) {
if (expectedGroup != null && !expectedGroup.isBlank()
&& !IrisBiomeCustomSpawnType.MISC.name().equalsIgnoreCase(expectedGroup)) {
blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId
+ "' spawn '" + typeKey + "' must declare group '"
+ expectedGroup.toUpperCase(Locale.ROOT) + "'.");
}
continue;
}
IrisBiomeCustomSpawnType configuredGroup;
try {
configuredGroup = IrisBiomeCustomSpawnType.valueOf(group);
} catch (IllegalArgumentException e) {
blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId
+ "' spawn '" + typeKey + "' declares unknown group '" + group + "'.");
continue;
}
if (expectedGroup != null && !expectedGroup.isBlank()
&& !configuredGroup.name().equalsIgnoreCase(expectedGroup)) {
blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId
+ "' spawn '" + typeKey + "' declares group '" + configuredGroup.name()
+ "' but the live entity registry requires '" + expectedGroup.toUpperCase(Locale.ROOT) + "'.");
}
}
}
static SpawnCategoryResolution resolveEntitySpawnCategory(String typeKey) {
if (!IrisPlatforms.isBound()) {
return null;
}
PlatformRegistries registries = IrisPlatforms.get().registries();
if (registries == null) {
return null;
}
PlatformEntityType entityType = registries.entity(typeKey);
return entityType == null
? SpawnCategoryResolution.unknown()
: SpawnCategoryResolution.known(entityType.spawnCategory());
}
record SpawnCategoryResolution(boolean entityKnown, String category) {
static SpawnCategoryResolution unknown() {
return new SpawnCategoryResolution(false, null);
}
static SpawnCategoryResolution known(String category) {
return new SpawnCategoryResolution(true, category);
}
}
}
@@ -0,0 +1,389 @@
/*
* 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.pack;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
final class PackStructurePlacementValidator {
private PackStructurePlacementValidator() {
}
static void validateStructurePlacements(File packFolder,
Set<String> structureKeys,
List<String> blockingErrors) {
Set<String> registeredStructures = registeredStructureKeys();
Set<String> registeredJigsaws = registeredJigsawKeys();
Set<String> registeredPools = registeredTemplatePoolKeys();
for (String folderName : PackValidator.STRUCTURE_HOST_FOLDERS) {
File resourceFolder = new File(packFolder, folderName);
if (!resourceFolder.isDirectory()) {
continue;
}
List<File> resourceFiles = PackValidationIo.listJsonRecursive(resourceFolder);
resourceFiles.sort(Comparator.comparing(File::getPath));
String resourceType = structureHostType(folderName);
for (File resourceFile : resourceFiles) {
JSONObject resource = PackValidationIo.readJson(resourceFile);
if (resource == null) {
continue;
}
JSONArray placements = resource.optJSONArray("structures");
if (placements == null) {
continue;
}
String resourceKey = PackValidationIo.deriveKey(resourceFolder, resourceFile);
for (int placementIndex = 0; placementIndex < placements.length(); placementIndex++) {
JSONObject placement = placements.optJSONObject(placementIndex);
if (placement == null) {
continue;
}
JSONArray references = placement.optJSONArray("structures");
JSONArray nativeStructures = placement.optJSONArray("nativeStructures");
boolean hasIrisStructures = references != null && references.length() > 0;
boolean hasNativeStructures = nativeStructures != null && nativeStructures.length() > 0;
String placementPath = resourceType + " '" + resourceKey + "' structures["
+ placementIndex + "]";
if (hasIrisStructures == hasNativeStructures) {
blockingErrors.add(placementPath
+ " must declare exactly one non-empty backend: structures or nativeStructures.");
continue;
}
if (hasNativeStructures) {
validateNativeStructures(
placementPath, placement, nativeStructures,
registeredStructures, registeredJigsaws,
registeredPools, blockingErrors);
continue;
}
for (int referenceIndex = 0; referenceIndex < references.length(); referenceIndex++) {
Object rawReference = references.opt(referenceIndex);
if (!(rawReference instanceof String structureKey) || structureKey.isBlank()) {
continue;
}
if (!structureKeys.contains(structureKey)) {
blockingErrors.add(resourceType + " '" + resourceKey + "' structures["
+ placementIndex + "].structures[" + referenceIndex
+ "] references missing structure '" + structureKey + "'.");
}
}
}
}
}
}
private static Set<String> registeredJigsawKeys() {
try {
List<String> registered = IrisPlatforms.get().structureHooks().jigsawStructureKeys();
if (registered == null || registered.isEmpty()) {
return Set.of();
}
Set<String> keys = new HashSet<>();
for (String key : registered) {
if (key != null && !key.isBlank()) {
keys.add(key.toLowerCase(Locale.ROOT));
}
}
return Set.copyOf(keys);
} catch (Throwable ignored) {
return Set.of();
}
}
private static Set<String> registeredStructureKeys() {
try {
List<String> registered = IrisPlatforms.get().structureHooks().structureKeys();
if (registered == null || registered.isEmpty()) {
return Set.of();
}
Set<String> keys = new HashSet<>();
for (String key : registered) {
if (key != null && !key.isBlank()) {
keys.add(key.toLowerCase(Locale.ROOT));
}
}
return Set.copyOf(keys);
} catch (Throwable ignored) {
return Set.of();
}
}
private static Set<String> registeredTemplatePoolKeys() {
try {
List<String> registered = IrisPlatforms.get().structureHooks().templatePoolKeys();
if (registered == null || registered.isEmpty()) {
return Set.of();
}
Set<String> keys = new HashSet<>();
for (String key : registered) {
if (key != null && !key.isBlank()) {
keys.add(key.toLowerCase(Locale.ROOT));
}
}
return Set.copyOf(keys);
} catch (Throwable ignored) {
return Set.of();
}
}
private static void validateNativeStructures(String placementPath, JSONObject placement,
JSONArray nativeStructures,
Set<String> registeredStructures,
Set<String> registeredJigsaws,
Set<String> registeredPools,
List<String> blockingErrors) {
for (int sourceIndex = 0; sourceIndex < nativeStructures.length(); sourceIndex++) {
String sourcePath = placementPath + ".nativeStructures[" + sourceIndex + "]";
JSONObject source = nativeStructures.optJSONObject(sourceIndex);
if (source == null) {
blockingErrors.add(sourcePath + " must be an object.");
continue;
}
String structureKey = source.optString("structure", "").trim();
if (!PackValidator.RESOURCE_KEY_PATTERN.matcher(structureKey).matches()) {
blockingErrors.add(sourcePath + ".structure must be a namespaced registry key.");
} else if (!registeredStructures.isEmpty()
&& !registeredStructures.contains(structureKey.toLowerCase(Locale.ROOT))) {
blockingErrors.add(sourcePath + ".structure '" + structureKey
+ "' is not a registered structure.");
}
Integer weight = PackLootValidator.lootInteger(source, "weight", 1, sourcePath, blockingErrors);
PackLootValidator.requireMinimum(sourcePath + ".weight", weight, 1, blockingErrors);
JSONObject jigsaw = source.optJSONObject("jigsaw");
if (source.has("jigsaw") && source.opt("jigsaw") != JSONObject.NULL && jigsaw == null) {
blockingErrors.add(sourcePath + ".jigsaw must be an object.");
} else if (jigsaw != null) {
if (!registeredJigsaws.isEmpty()
&& !registeredJigsaws.contains(structureKey.toLowerCase(Locale.ROOT))) {
blockingErrors.add(sourcePath
+ ".jigsaw requires a registered jigsaw structure.");
}
validateJigsawAssembly(
sourcePath + ".jigsaw", jigsaw, registeredPools, blockingErrors);
}
}
validateNativeTerrain(placementPath, placement, blockingErrors);
}
private static void validateJigsawAssembly(String path, JSONObject assembly,
Set<String> registeredPools,
List<String> blockingErrors) {
PackJsonFieldChecks.validateOptionalResourceKey(path, assembly, "startPool", false, blockingErrors);
String startPool = assembly.optString("startPool", "").trim();
if (!startPool.isEmpty() && !registeredPools.isEmpty()
&& !registeredPools.contains(startPool.toLowerCase(Locale.ROOT))) {
blockingErrors.add(path + ".startPool '" + startPool
+ "' is not a registered template pool.");
}
PackJsonFieldChecks.validateOptionalResourceKey(path, assembly, "startJigsawName", true, blockingErrors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, assembly, "maxDepth", 0, 20, blockingErrors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, assembly, "maxDistanceHorizontal", 1, 128, blockingErrors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, assembly, "maxDistanceVertical", 1, 4064, blockingErrors);
PackJsonFieldChecks.validateOptionalIntegerRange(
path, assembly, "dimensionPaddingBottom", 0, Integer.MAX_VALUE, blockingErrors);
PackJsonFieldChecks.validateOptionalIntegerRange(
path, assembly, "dimensionPaddingTop", 0, Integer.MAX_VALUE, blockingErrors);
if (assembly.has("useExpansionHack")
&& !(assembly.opt("useExpansionHack") instanceof Boolean)) {
blockingErrors.add(path + ".useExpansionHack must be a boolean.");
}
PackJsonFieldChecks.validateOptionalEnum(path, assembly, "projectStartToHeightmap",
Set.of("SOURCE", "NONE", "WORLD_SURFACE_WG", "WORLD_SURFACE",
"OCEAN_FLOOR_WG", "OCEAN_FLOOR", "MOTION_BLOCKING",
"MOTION_BLOCKING_NO_LEAVES"), blockingErrors);
PackJsonFieldChecks.validateOptionalEnum(path, assembly, "liquidSettings",
Set.of("SOURCE", "IGNORE_WATERLOGGING", "APPLY_WATERLOGGING"), blockingErrors);
}
static void validateNativeTerrain(String path, JSONObject placement,
List<String> blockingErrors) {
JSONObject terrain = placement.optJSONObject("terrain");
if (placement.has("terrain") && placement.opt("terrain") != JSONObject.NULL && terrain == null) {
blockingErrors.add(path + ".terrain must be an object.");
return;
}
if (terrain == null) {
return;
}
PackJsonFieldChecks.validateOptionalEnum(path + ".terrain", terrain, "mode",
Set.of("SOURCE", "PRESERVE", "BORE", "FORCE_CARVE", "VACUUM", "ENCASE"), blockingErrors);
PackJsonFieldChecks.validateOptionalIntegerRange(path + ".terrain", terrain,
"horizontalPadding", 0, 128, blockingErrors);
PackJsonFieldChecks.validateOptionalIntegerRange(path + ".terrain", terrain,
"ceilingPadding", 0, 128, blockingErrors);
PackJsonFieldChecks.validateOptionalIntegerRange(path + ".terrain", terrain,
"floorPadding", 0, 64, blockingErrors);
PackJsonFieldChecks.validateOptionalDoubleRange(path + ".terrain", terrain,
"erosionStrength", 0D, 1D, blockingErrors);
PackJsonFieldChecks.validateOptionalDoubleRange(path + ".terrain", terrain,
"erosionFrequency", 0.001D, 1D, blockingErrors);
PackJsonFieldChecks.validateOptionalDoubleRange(path + ".terrain", terrain,
"lobeFrequency", 0D, 1D, blockingErrors);
PackJsonFieldChecks.validateOptionalDoubleRange(path + ".terrain", terrain,
"lobeStrength", 0D, 1D, blockingErrors);
if (terrain.has("encasePalette") && terrain.opt("encasePalette") != JSONObject.NULL
&& terrain.optJSONObject("encasePalette") == null) {
blockingErrors.add(path + ".terrain.encasePalette must be an object.");
}
}
static void validateStructureStartPools(File structuresFolder,
Set<String> poolKeys,
List<String> blockingErrors) {
if (!structuresFolder.isDirectory()) {
return;
}
List<File> structureFiles = PackValidationIo.listJsonRecursive(structuresFolder);
structureFiles.sort(Comparator.comparing(File::getPath));
for (File structureFile : structureFiles) {
String structureKey = PackValidationIo.deriveKey(structuresFolder, structureFile);
JSONObject structure = readGraphJson(structureFile, "Structure", structureKey, blockingErrors);
if (structure == null || isLegacyStructureIndex(structureKey, structure)) {
continue;
}
String startPool = structure.optString("startPool", "").trim();
if (startPool.isEmpty()) {
blockingErrors.add("Structure '" + structureKey + "' does not declare a startPool.");
} else if (!poolKeys.contains(startPool)) {
blockingErrors.add("Structure '" + structureKey + "' references missing start pool '"
+ startPool + "'.");
}
}
}
static void validateJigsawPools(File poolsFolder,
Set<String> poolKeys,
Set<String> pieceKeys,
List<String> blockingErrors) {
if (!poolsFolder.isDirectory()) {
return;
}
List<File> poolFiles = PackValidationIo.listJsonRecursive(poolsFolder);
poolFiles.sort(Comparator.comparing(File::getPath));
for (File poolFile : poolFiles) {
String poolKey = PackValidationIo.deriveKey(poolsFolder, poolFile);
JSONObject pool = readGraphJson(poolFile, "Jigsaw pool", poolKey, blockingErrors);
if (pool == null) {
continue;
}
JSONArray entries = pool.optJSONArray("pieces");
if (entries != null) {
for (int entryIndex = 0; entryIndex < entries.length(); entryIndex++) {
JSONObject entry = entries.optJSONObject(entryIndex);
if (entry == null) {
continue;
}
String pieceKey = entry.optString("piece", "").trim();
if (!pieceKey.isEmpty() && !pieceKeys.contains(pieceKey)) {
blockingErrors.add("Jigsaw pool '" + poolKey + "' pieces[" + entryIndex
+ "] references missing piece '" + pieceKey + "'.");
}
}
}
String fallback = pool.optString("fallback", "").trim();
if (!fallback.isEmpty() && !poolKeys.contains(fallback)) {
blockingErrors.add("Jigsaw pool '" + poolKey + "' references missing fallback pool '"
+ fallback + "'.");
}
}
}
static void validateJigsawPieces(File piecesFolder,
Set<String> poolKeys,
Set<String> objectKeys,
List<String> blockingErrors) {
if (!piecesFolder.isDirectory()) {
return;
}
List<File> pieceFiles = PackValidationIo.listJsonRecursive(piecesFolder);
pieceFiles.sort(Comparator.comparing(File::getPath));
for (File pieceFile : pieceFiles) {
String pieceKey = PackValidationIo.deriveKey(piecesFolder, pieceFile);
JSONObject piece = readGraphJson(pieceFile, "Jigsaw piece", pieceKey, blockingErrors);
if (piece == null) {
continue;
}
String objectKey = piece.optString("object", "").trim();
if (objectKey.isEmpty()) {
blockingErrors.add("Jigsaw piece '" + pieceKey + "' does not declare an object.");
} else if (!objectKeys.contains(objectKey)) {
blockingErrors.add("Jigsaw piece '" + pieceKey + "' references missing object '"
+ objectKey + "'.");
}
JSONArray connectors = piece.optJSONArray("connectors");
if (connectors == null) {
continue;
}
for (int connectorIndex = 0; connectorIndex < connectors.length(); connectorIndex++) {
JSONObject connector = connectors.optJSONObject(connectorIndex);
if (connector == null) {
continue;
}
String poolKey = connector.optString("pool", "").trim();
if (!poolKey.isEmpty() && !poolKeys.contains(poolKey)) {
blockingErrors.add("Jigsaw piece '" + pieceKey + "' connectors[" + connectorIndex
+ "] references missing pool '" + poolKey + "'.");
}
}
}
}
static JSONObject readGraphJson(File file,
String resourceType,
String resourceKey,
List<String> blockingErrors) {
try {
return new JSONObject(Files.readString(file.toPath(), StandardCharsets.UTF_8));
} catch (IOException | RuntimeException e) {
String reason = e.getMessage();
if (reason == null || reason.isBlank()) {
reason = e.getClass().getSimpleName();
}
blockingErrors.add(resourceType + " '" + resourceKey + "' has invalid JSON: " + reason);
return null;
}
}
private static boolean isLegacyStructureIndex(String structureKey, JSONObject structure) {
return "structure-index".equals(structureKey)
&& structure.has("counts")
&& structure.has("structureSets")
&& structure.has("iris");
}
static String structureHostType(String folderName) {
return switch (folderName) {
case "dimensions" -> "Dimension";
case "regions" -> "Region";
case "biomes" -> "Biome";
default -> "Resource";
};
}
}
@@ -0,0 +1,127 @@
/*
* 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.pack;
import art.arcane.volmlib.util.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
public final class PackValidationIo {
private PackValidationIo() {
}
static JSONObject readJson(File file) {
try {
return new JSONObject(Files.readString(file.toPath(), StandardCharsets.UTF_8));
} catch (IOException | RuntimeException parseFailure) {
// Every folder scanned through readJson is also scanned by a readGraphJson pass
// (validateUnsupportedStructureTransforms / validateStructureStartPools), which records the
// parse failure as a blocking error. Reporting here too would duplicate it per scan.
return null;
}
}
static boolean isScannableJsonPath(Path path) {
String name = path.getFileName().toString();
if (!name.endsWith(".json")) {
return false;
}
String str = path.toString().replace(File.separatorChar, '/');
if (str.contains("/" + PackValidator.TRASH_ROOT + "/")) {
return false;
}
if (str.contains("/" + PackValidator.DATAPACK_IMPORTS + "/")) {
return false;
}
if (str.contains("/" + PackValidator.EXTERNAL_DATAPACKS + "/")) {
return false;
}
if (str.contains("/" + PackValidator.INTERNAL_DATAPACKS + "/")) {
return false;
}
if (str.contains("/" + PackValidator.DATAPACKS_FOLDER + "/")) {
return false;
}
if (str.contains("/" + PackValidator.CACHE_FOLDER + "/")) {
return false;
}
if (str.contains("/" + PackValidator.OBJECTS_FOLDER + "/")) {
return false;
}
if (str.contains("/.iris/")) {
return false;
}
return true;
}
static List<File> listJsonRecursive(File root) {
List<File> out = new ArrayList<>();
try (Stream<Path> stream = Files.walk(root.toPath())) {
stream.filter(Files::isRegularFile)
.filter(p -> p.getFileName().toString().endsWith(".json"))
.forEach(p -> out.add(p.toFile()));
} catch (Throwable ignored) {
}
return out;
}
static String deriveKey(File resourceFolder, File resourceFile) {
Path relative = resourceFolder.toPath().relativize(resourceFile.toPath());
String str = relative.toString().replace(File.separatorChar, '/');
if (!str.endsWith(".json")) {
return null;
}
return str.substring(0, str.length() - ".json".length());
}
static String stripExtension(String name) {
int dot = name.lastIndexOf('.');
return dot <= 0 ? name : name.substring(0, dot);
}
public static Set<String> listReferencedKeysFromCorpus(String corpus) {
Set<String> keys = new HashSet<>();
if (corpus == null) {
return keys;
}
int i = 0;
while (i < corpus.length()) {
int start = corpus.indexOf('"', i);
if (start < 0) {
break;
}
int end = corpus.indexOf('"', start + 1);
if (end < 0) {
break;
}
keys.add(corpus.substring(start + 1, end));
i = end + 1;
}
return keys;
}
}
File diff suppressed because it is too large. Load diff
@@ -118,7 +118,9 @@ public class IrisPregenerator {
generatedLast.set(generated.get());
if (secondCached == 0 || secondGenerated != 0) {
chunksPerSecond.put(secondGenerated);
chunksPerSecondHistory.add((int) secondGenerated);
synchronized (chunksPerSecondHistory) {
chunksPerSecondHistory.add((int) secondGenerated);
}
}
if (minuteLatch.flip()) {
@@ -222,7 +224,13 @@ public class IrisPregenerator {
logIncompleteCompletion(p);
}
if (benchmarking != null) {
benchmarking.finishedBenchmark(chunksPerSecondHistory);
benchmarking.finishedBenchmark(snapshotChunksPerSecondHistory());
}
}
private KList<Integer> snapshotChunksPerSecondHistory() {
synchronized (chunksPerSecondHistory) {
return new KList<>(chunksPerSecondHistory);
}
}
@@ -264,18 +272,31 @@ public class IrisPregenerator {
}
private void shutdown() {
listener.onSaving();
generator.close();
ticker.interrupt();
listener.onClose();
IrisProtocolServer protocolServer = IrisServices.getOrNull(IrisProtocolServer.class);
if (protocolServer != null) {
long total = totalChunks.get();
protocolServer.pregenEnd(jobId, total > 0 && generated.get() >= total);
}
Mantle mantle = getMantle();
if (mantle != null) {
reclaimTectonicPlates(mantle);
shutdownStep("saving", listener::onSaving);
shutdownStep("generator", generator::close);
shutdownStep("ticker", ticker::interrupt);
shutdownStep("listener", listener::onClose);
shutdownStep("protocol", () -> {
IrisProtocolServer protocolServer = IrisServices.getOrNull(IrisProtocolServer.class);
if (protocolServer != null) {
long total = totalChunks.get();
protocolServer.pregenEnd(jobId, total > 0 && generated.get() >= total);
}
});
shutdownStep("mantle", () -> {
Mantle mantle = getMantle();
if (mantle != null) {
reclaimTectonicPlates(mantle);
}
});
}
private void shutdownStep(String step, Runnable action) {
try {
action.run();
} catch (Throwable e) {
IrisLogging.reportError(e);
IrisLogging.warn("Pregen shutdown step " + step + " failed: " + e.getClass().getSimpleName() + ": " + e.getMessage());
}
}
@@ -22,6 +22,7 @@ import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.math.M;
import java.util.function.BooleanSupplier;
import java.util.function.Supplier;
public final class PregenMantleBackpressure {
@@ -31,14 +32,20 @@ public final class PregenMantleBackpressure {
private final long timeoutMs;
private final Runnable onBudgetTimeout;
private final Supplier<String> diagnostics;
private final BooleanSupplier cancelled;
public PregenMantleBackpressure(Supplier<Mantle> mantleSupplier, int maxResidentTectonicPlates, int waitMs, long timeoutMs, Runnable onBudgetTimeout, Supplier<String> diagnostics) {
this(mantleSupplier, maxResidentTectonicPlates, waitMs, timeoutMs, onBudgetTimeout, diagnostics, () -> false);
}
public PregenMantleBackpressure(Supplier<Mantle> mantleSupplier, int maxResidentTectonicPlates, int waitMs, long timeoutMs, Runnable onBudgetTimeout, Supplier<String> diagnostics, BooleanSupplier cancelled) {
this.mantleSupplier = mantleSupplier;
this.maxResidentTectonicPlates = maxResidentTectonicPlates;
this.waitMs = waitMs;
this.timeoutMs = timeoutMs;
this.onBudgetTimeout = onBudgetTimeout;
this.diagnostics = diagnostics;
this.cancelled = cancelled;
}
public void apply() {
@@ -65,6 +72,10 @@ public final class PregenMantleBackpressure {
long waitStart = M.ms();
long lastLog = 0L;
while (mantle.getLoadedRegionCount() > hardCap) {
if (isCancelled()) {
return;
}
int freed;
int resident;
try {
@@ -106,8 +117,13 @@ public final class PregenMantleBackpressure {
public void awaitHeapHeadroom() {
Mantle mantle = resolveMantle();
long waitStart = M.ms();
long lastLog = 0L;
while (MantleHeapPressure.overHighWater()) {
if (isCancelled()) {
return;
}
try {
if (mantle != null && mantle.getLoadedRegionCount() > maxResidentTectonicPlates) {
mantle.trim(0L, 0);
@@ -121,6 +137,15 @@ public final class PregenMantleBackpressure {
MantleHeapPressure.requestPanicReclaim();
}
long elapsed = M.ms() - waitStart;
if (elapsed >= timeoutMs) {
IrisLogging.warn("Pregen heap pressure wait exceeded " + timeoutMs + "ms at "
+ Math.round(MantleHeapPressure.usedFraction() * 100.0D) + "% heap; proceeding to avoid deadlock. "
+ diagnostics.get());
onBudgetTimeout.run();
return;
}
long logNow = M.ms();
if (logNow - lastLog >= 5_000L) {
lastLog = logNow;
@@ -138,6 +163,18 @@ public final class PregenMantleBackpressure {
}
}
private boolean isCancelled() {
if (Thread.currentThread().isInterrupted()) {
return true;
}
try {
return cancelled.getAsBoolean();
} catch (Throwable ignored) {
return false;
}
}
private Mantle resolveMantle() {
try {
return mantleSupplier.get();
@@ -178,12 +178,13 @@ public class PregenCacheImpl implements PregenCache {
return;
}
File file = fileForPlate(plate.x, plate.z);
File file = null;
try {
file = fileForPlate(plate.x, plate.z);
IO.write(file, output -> new DataOutputStream(new LZ4BlockOutputStream(output)), plate::write);
plate.dirty = false;
} catch (IOException e) {
IrisLogging.error("Failed to write preen cache " + file);
} catch (Throwable e) {
IrisLogging.error("Failed to write pregen cache " + (file != null ? file : "c." + plate.x + "." + plate.z));
e.printStackTrace();
IrisLogging.reportError(e);
}
@@ -51,6 +51,8 @@ import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
@@ -58,6 +60,8 @@ public class AsyncPregenMethod implements PregeneratorMethod {
private static final AtomicInteger THREAD_COUNT = new AtomicInteger();
private static final int ADAPTIVE_TIMEOUT_STEP = 3;
private static final int ADAPTIVE_RECOVERY_INTERVAL = 8;
private static final long CLOSE_DRAIN_TIMEOUT_SECONDS = 60L;
private static final long FLUSH_TIMEOUT_SECONDS = 120L;
private final World world;
private final IrisRuntimeSchedulerMode runtimeSchedulerMode;
private final IrisPaperLikeBackendMode paperLikeBackendMode;
@@ -97,6 +101,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
private final AtomicLong completed = new AtomicLong();
private final AtomicLong failed = new AtomicLong();
private final AtomicLong lastProgressAt = new AtomicLong(M.ms());
private final AtomicBoolean closing = new AtomicBoolean();
private final Object permitMonitor = new Object();
private volatile Engine metricsEngine;
private volatile Mantle cachedMantle;
@@ -169,7 +174,8 @@ public class AsyncPregenMethod implements PregeneratorMethod {
pregen.getMantleBackpressureWaitMs(),
pregen.getMantleBackpressureTimeoutMs(),
this::lowerAdaptiveInFlightLimit,
this::metricsSnapshot);
this::metricsSnapshot,
this::isCancelled);
}
public static AsyncPregenMethod strictSerial(World world) {
@@ -408,50 +414,92 @@ public class AsyncPregenMethod implements PregeneratorMethod {
return;
}
try {
J.sfut(() -> {
for (Long rk : keys) {
if (!evictedRegions.add(rk)) {
continue;
}
regionPending.remove(rk);
Queue<Chunk> chunks = regionChunks.remove(rk);
if (chunks == null) {
continue;
}
for (Chunk chunk : chunks) {
if (chunk != null) {
unloadChunkSafely(chunk.getX(), chunk.getZ());
}
}
CompletableFuture<Void> flush = J.sfut(() -> {
for (Long rk : keys) {
if (!evictedRegions.add(rk)) {
continue;
}
world.save();
INMS.get().flushChunkIO(world);
}).get();
regionPending.remove(rk);
Queue<Chunk> chunks = regionChunks.remove(rk);
if (chunks == null) {
continue;
}
for (Chunk chunk : chunks) {
if (chunk != null) {
unloadChunkSafely(chunk.getX(), chunk.getZ());
}
}
}
world.save();
INMS.get().flushChunkIO(world);
});
try {
flush.get(FLUSH_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
IrisLogging.warn("Interrupted while flushing pregen chunks for " + world.getName() + ".");
} catch (TimeoutException e) {
IrisLogging.warn("Pregen chunk flush for " + world.getName() + " did not finish in " + FLUSH_TIMEOUT_SECONDS
+ "s, continuing shutdown. " + keys.size() + " region(s) queued.");
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
private Chunk onChunkFutureFailure(int x, int z, Throwable throwable) {
Throwable root = throwable;
while (root.getCause() != null) {
root = root.getCause();
try {
Throwable root = throwable;
while (root.getCause() != null) {
root = root.getCause();
}
if (root instanceof TimeoutException) {
onTimeout(x, z);
} else {
IrisLogging.warn("Failed async pregen chunk load at " + x + "," + z + ". " + metricsSnapshot());
}
IrisLogging.reportError(throwable);
} catch (Throwable e) {
e.printStackTrace();
}
if (root instanceof java.util.concurrent.TimeoutException) {
onTimeout(x, z);
} else {
IrisLogging.warn("Failed async pregen chunk load at " + x + "," + z + ". " + metricsSnapshot());
}
IrisLogging.reportError(throwable);
return null;
}
private void completeChunk(int x, int z, PregenListener listener, Chunk chunk, Throwable throwable) {
boolean success = false;
try {
if (throwable != null) {
onChunkFutureFailure(x, z, throwable);
onChunkFailedToLoad(x, z);
listener.onChunkFailed(x, z);
} else if (chunk == null) {
onChunkFailedToLoad(x, z);
listener.onChunkFailed(x, z);
} else {
listener.onChunkGenerated(x, z);
cleanupMantleChunk(x, z);
listener.onChunkCleaned(x, z);
onChunkCompleted(x, z, chunk);
success = true;
}
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
} finally {
try {
markFinished(success);
} finally {
semaphore.release();
}
}
}
private void onTimeout(int x, int z) {
int streak = timeoutStreak.incrementAndGet();
if (streak % ADAPTIVE_TIMEOUT_STEP == 0) {
@@ -707,10 +755,36 @@ public class AsyncPregenMethod implements PregeneratorMethod {
@Override
public void close() {
semaphore.acquireUninterruptibly(threads);
flushAllRemainingChunks();
executor.shutdown();
resetWorkerThreads();
closing.set(true);
notifyPermitWaiters();
// A stop request interrupts the pregen worker; shield the drain and flush so chunks still hit disk.
boolean interrupted = Thread.interrupted();
try {
boolean drained = false;
try {
drained = semaphore.tryAcquire(threads, CLOSE_DRAIN_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (InterruptedException e) {
interrupted = true;
}
if (!drained) {
IrisLogging.warn("Async pregen close did not drain in " + CLOSE_DRAIN_TIMEOUT_SECONDS
+ "s, continuing degraded. " + metricsSnapshot());
}
flushAllRemainingChunks();
executor.shutdown();
resetWorkerThreads();
} finally {
if (interrupted) {
Thread.currentThread().interrupt();
}
}
}
private boolean isCancelled() {
return closing.get() || Thread.currentThread().isInterrupted();
}
@Override
@@ -732,10 +806,18 @@ public class AsyncPregenMethod implements PregeneratorMethod {
listener.onChunkGenerating(x, z);
backpressure.enforceMantleBudget();
backpressure.awaitHeapHeadroom();
if (isCancelled()) {
return;
}
try {
long waitStart = M.ms();
synchronized (permitMonitor) {
while (inFlight.get() >= adaptiveInFlightLimit.get()) {
if (isCancelled()) {
return;
}
permitMonitor.wait(500L);
}
}
@@ -746,6 +828,9 @@ public class AsyncPregenMethod implements PregeneratorMethod {
long permitWaitStart = M.ms();
while (!semaphore.tryAcquire(1, TimeUnit.SECONDS)) {
if (isCancelled()) {
return;
}
}
long permitWait = Math.max(0L, M.ms() - permitWaitStart);
if (permitWait > 0L) {
@@ -921,48 +1006,24 @@ public class AsyncPregenMethod implements PregeneratorMethod {
try {
requestChunkAsync(x, z)
.orTimeout(timeoutSeconds, TimeUnit.SECONDS)
.whenComplete((chunk, throwable) -> completeFoliaChunk(x, z, listener, chunk, throwable));
.whenComplete((chunk, throwable) -> completeChunk(x, z, listener, chunk, throwable));
return;
} catch (Throwable ignored) {
}
if (!J.runRegion(world, x, z, () -> requestChunkAsync(x, z)
.orTimeout(timeoutSeconds, TimeUnit.SECONDS)
.whenComplete((chunk, throwable) -> completeFoliaChunk(x, z, listener, chunk, throwable)))) {
markFinished(false);
semaphore.release();
listener.onChunkFailed(x, z);
IrisLogging.warn("Failed to schedule Folia region pregen task at " + x + "," + z + ". " + metricsSnapshot());
}
}
private void completeFoliaChunk(int x, int z, PregenListener listener, Chunk chunk, Throwable throwable) {
boolean success = false;
try {
if (throwable != null) {
onChunkFutureFailure(x, z, throwable);
onChunkFailedToLoad(x, z);
listener.onChunkFailed(x, z);
return;
Runnable regionTask = () -> {
try {
requestChunkAsync(x, z)
.orTimeout(timeoutSeconds, TimeUnit.SECONDS)
.whenComplete((chunk, throwable) -> completeChunk(x, z, listener, chunk, throwable));
} catch (Throwable e) {
completeChunk(x, z, listener, null, e);
}
};
if (chunk == null) {
onChunkFailedToLoad(x, z);
listener.onChunkFailed(x, z);
return;
}
listener.onChunkGenerated(x, z);
cleanupMantleChunk(x, z);
listener.onChunkCleaned(x, z);
onChunkCompleted(x, z, chunk);
success = true;
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
} finally {
markFinished(success);
semaphore.release();
if (!J.runRegion(world, x, z, regionTask)) {
completeChunk(x, z, listener, null,
new IllegalStateException("Failed to schedule Folia region pregen task at " + x + "," + z + ". " + metricsSnapshot()));
}
}
}
@@ -971,35 +1032,23 @@ public class AsyncPregenMethod implements PregeneratorMethod {
private final ExecutorService service = new MultiBurst("Iris Async Pregen");
public void generate(int x, int z, PregenListener listener) {
service.submit(() -> {
boolean success = false;
try {
Chunk i = requestChunkAsync(x, z)
.orTimeout(timeoutSeconds, TimeUnit.SECONDS)
.exceptionally(e -> onChunkFutureFailure(x, z, e))
.get();
if (i == null) {
onChunkFailedToLoad(x, z);
listener.onChunkFailed(x, z);
return;
try {
service.submit(() -> {
try {
Chunk i = requestChunkAsync(x, z)
.orTimeout(timeoutSeconds, TimeUnit.SECONDS)
.get();
completeChunk(x, z, listener, i, null);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
completeChunk(x, z, listener, null, e);
} catch (Throwable e) {
completeChunk(x, z, listener, null, e);
}
listener.onChunkGenerated(x, z);
cleanupMantleChunk(x, z);
listener.onChunkCleaned(x, z);
onChunkCompleted(x, z, i);
success = true;
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
} finally {
markFinished(success);
semaphore.release();
}
});
});
} catch (Throwable e) {
completeChunk(x, z, listener, null, e);
}
}
@Override
@@ -1011,28 +1060,13 @@ public class AsyncPregenMethod implements PregeneratorMethod {
private class TicketExecutor implements Executor {
@Override
public void generate(int x, int z, PregenListener listener) {
requestChunkAsync(x, z)
.orTimeout(timeoutSeconds, TimeUnit.SECONDS)
.exceptionally(e -> onChunkFutureFailure(x, z, e))
.thenAccept(i -> {
boolean success = false;
try {
if (i == null) {
onChunkFailedToLoad(x, z);
listener.onChunkFailed(x, z);
return;
}
listener.onChunkGenerated(x, z);
cleanupMantleChunk(x, z);
listener.onChunkCleaned(x, z);
onChunkCompleted(x, z, i);
success = true;
} finally {
markFinished(success);
semaphore.release();
}
});
try {
requestChunkAsync(x, z)
.orTimeout(timeoutSeconds, TimeUnit.SECONDS)
.whenComplete((chunk, throwable) -> completeChunk(x, z, listener, chunk, throwable));
} catch (Throwable e) {
completeChunk(x, z, listener, null, e);
}
}
}
@@ -1,65 +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.pregenerator.methods;
import art.arcane.iris.core.pregenerator.PregenListener;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
public class DummyPregenMethod implements PregeneratorMethod {
@Override
public void init() {
}
@Override
public void close() {
}
@Override
public String getMethod(int x, int z) {
return "Dummy";
}
@Override
public void save() {
}
@Override
public boolean supportsRegions(int x, int z, PregenListener listener) {
return false;
}
@Override
public void generateRegion(int x, int z, PregenListener listener) {
}
@Override
public void generateChunk(int x, int z, PregenListener listener) {
}
@Override
public Mantle getMantle() {
return null;
}
}
@@ -41,10 +41,14 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public class MedievalPregenMethod implements PregeneratorMethod {
private static final long CHUNK_WAIT_TIMEOUT_SECONDS = 60L;
private static final long UNLOAD_TIMEOUT_SECONDS = 120L;
private final World world;
private final KList<CompletableFuture<?>> futures;
private final Map<Chunk, Long> lastUse;
@@ -105,9 +109,14 @@ public class MedievalPregenMethod implements PregeneratorMethod {
private void waitForChunks() {
for (CompletableFuture<?> i : futures) {
try {
i.get();
i.get(CHUNK_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (TimeoutException e) {
IrisLogging.warn("Medieval pregen chunk did not finish in " + CHUNK_WAIT_TIMEOUT_SECONDS + "s, abandoning it.");
} catch (Throwable e) {
e.printStackTrace();
IrisLogging.reportError(e);
}
}
@@ -120,26 +129,32 @@ public class MedievalPregenMethod implements PregeneratorMethod {
return;
}
try {
J.sfut(() -> {
if (world == null) {
IrisLogging.warn("World was null somehow...");
return;
}
CompletableFuture<Void> unload = J.sfut(() -> {
if (world == null) {
IrisLogging.warn("World was null somehow...");
return;
}
for (Chunk i : new ArrayList<>(lastUse.keySet())) {
Long lastUseTime = lastUse.get(i);
if (lastUseTime != null && M.ms() - lastUseTime >= 10) {
i.unload();
lastUse.remove(i);
}
for (Chunk i : new ArrayList<>(lastUse.keySet())) {
Long lastUseTime = lastUse.get(i);
if (lastUseTime != null && M.ms() - lastUseTime >= 10) {
i.unload();
lastUse.remove(i);
}
if (saveWorld) {
world.save();
}
}).get();
}
if (saveWorld) {
world.save();
}
});
try {
unload.get(UNLOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (TimeoutException e) {
IrisLogging.warn("Medieval pregen chunk unload did not finish in " + UNLOAD_TIMEOUT_SECONDS + "s, continuing.");
} catch (Throwable e) {
e.printStackTrace();
IrisLogging.reportError(e);
}
}
@@ -150,11 +165,19 @@ public class MedievalPregenMethod implements PregeneratorMethod {
@Override
public void close() {
waitForChunks();
if (prefetchPool != null) {
prefetchPool.shutdownNow();
// A stop request interrupts the pregen worker; shield the drain and save so chunks still hit disk.
boolean interrupted = Thread.interrupted();
try {
waitForChunks();
if (prefetchPool != null) {
prefetchPool.shutdownNow();
}
unloadAndSaveAllChunks(true);
} finally {
if (interrupted) {
Thread.currentThread().interrupt();
}
}
unloadAndSaveAllChunks(true);
}
@Override
@@ -212,22 +235,26 @@ public class MedievalPregenMethod implements PregeneratorMethod {
try {
prefetchPool.submit(() -> {
try {
prefetchMantle(engine, x, z);
} catch (Throwable e) {
if (prefetchDisabled.compareAndSet(false, true)) {
IrisLogging.warn("Mantle prefetch failed at chunk " + x + "," + z + "; disabling prefetch for this pregen.");
IrisLogging.reportError(e);
try {
prefetchMantle(engine, x, z);
} catch (Throwable e) {
if (prefetchDisabled.compareAndSet(false, true)) {
IrisLogging.warn("Mantle prefetch failed at chunk " + x + "," + z + "; disabling prefetch for this pregen.");
IrisLogging.reportError(e);
}
}
}
CompletableFuture<?> chunkFuture = runChunkLoad(x, z, listener);
chunkFuture.whenComplete((r, err) -> {
if (err != null) {
aggregate.completeExceptionally(err);
} else {
aggregate.complete(null);
}
});
CompletableFuture<?> chunkFuture = runChunkLoad(x, z, listener);
chunkFuture.whenComplete((r, err) -> {
if (err != null) {
aggregate.completeExceptionally(err);
} else {
aggregate.complete(null);
}
});
} catch (Throwable e) {
aggregate.completeExceptionally(e);
}
});
} catch (Throwable rejected) {
if (prefetchDisabled.compareAndSet(false, true)) {
@@ -268,7 +295,7 @@ public class MedievalPregenMethod implements PregeneratorMethod {
}
try {
generateChunkSync(x, z, listener).get();
generateChunkSync(x, z, listener).get(CHUNK_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
future.complete(null);
} catch (Throwable fallbackError) {
future.completeExceptionally(fallbackError);
@@ -0,0 +1,298 @@
/*
* 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.project;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.core.localization.BukkitRuntimeMessages;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.annotations.Snippet;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import org.dom4j.Document;
import org.dom4j.Element;
import java.awt.Desktop;
import java.awt.GraphicsEnvironment;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
import java.util.UUID;
@SuppressWarnings("ALL")
public class IrisCodeWorkspace {
private final IrisProject project;
public IrisCodeWorkspace(IrisProject project) {
this.project = project;
}
public void openVSCode(VolmitSender sender) {
IrisDimension d = IrisData.loadAnyDimension(project.getName(), null);
J.attemptAsync(() ->
{
try {
if (d == null) {
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_COULD_NOT_LOAD_DIMENSION, MessageArgument.untrusted("value", String.valueOf(project.getName()))));
return;
}
if (d.getLoader() == null) {
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_COULD_NOT_GET_DIMENSION_LOADER));
return;
}
File f = d.getLoader().getDataFolder();
if (!doOpenVSCode(f)) {
File ff = new File(d.getLoader().getDataFolder(), d.getLoadKey() + ".code-workspace");
IrisLogging.warn("Project missing code-workspace: " + ff.getAbsolutePath() + " Re-creating code workspace.");
try {
IO.writeAll(ff, createCodeWorkspaceConfig(false));
} catch (IOException e1) {
IrisLogging.reportError(e1);
e1.printStackTrace();
}
if (!doOpenVSCode(f)) {
IrisLogging.warn("Tried creating code workspace but failed a second time. Your project is likely corrupt.");
}
}
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
}
});
}
private boolean doOpenVSCode(File f) throws IOException {
boolean foundWork = false;
for (File i : Objects.requireNonNull(f.listFiles())) {
if (i.getName().endsWith(".code-workspace")) {
foundWork = true;
if (IrisSettings.get().getStudio().isOpenVSCode()) {
if (!GraphicsEnvironment.isHeadless()) {
IrisLogging.msg("Opening VSCode. You may see the output from VSCode.");
IrisLogging.msg("VSCode output always starts with: '(node:#####) electron'");
Thread launcherThread = new Thread(() -> {
try {
Desktop.getDesktop().open(i);
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}, "Iris-VSCode-Launcher");
launcherThread.setDaemon(true);
launcherThread.start();
}
}
break;
}
}
return foundWork;
}
public File getCodeWorkspaceFile() {
return new File(project.getPath(), project.getName() + ".code-workspace");
}
public boolean updateWorkspace() {
project.getPath().mkdirs();
File ws = getCodeWorkspaceFile();
try {
PrecisionStopwatch p = PrecisionStopwatch.start();
JSONObject j = createCodeWorkspaceConfig();
IO.writeAll(ws, j.toString(4));
p.end();
return true;
} catch (Throwable e) {
IrisLogging.reportError(e);
IrisLogging.warn("Project invalid: " + ws.getAbsolutePath() + " Re-creating. You may loose some vs-code workspace settings! But not your actual project!");
ws.delete();
try {
IO.writeAll(ws, createCodeWorkspaceConfig());
} catch (IOException e1) {
IrisLogging.reportError(e1);
e1.printStackTrace();
}
}
return false;
}
public JSONObject createCodeWorkspaceConfig() {
return createCodeWorkspaceConfig(true);
}
private JSONObject createCodeWorkspaceConfig(boolean includeSchemas) {
JSONObject ws = new JSONObject();
JSONArray folders = new JSONArray();
JSONObject folder = new JSONObject();
folder.put("path", ".");
folders.put(folder);
ws.put("folders", folders);
JSONObject settings = new JSONObject();
settings.put("workbench.colorTheme", "Monokai");
settings.put("workbench.preferredDarkColorTheme", "Solarized Dark");
settings.put("workbench.tips.enabled", false);
settings.put("workbench.tree.indent", 24);
settings.put("files.autoSave", "onFocusChange");
JSONObject jc = new JSONObject();
jc.put("editor.autoIndent", "brackets");
jc.put("editor.acceptSuggestionOnEnter", "smart");
jc.put("editor.cursorSmoothCaretAnimation", true);
jc.put("editor.dragAndDrop", false);
jc.put("files.trimTrailingWhitespace", true);
jc.put("diffEditor.ignoreTrimWhitespace", true);
jc.put("files.trimFinalNewlines", true);
jc.put("editor.suggest.showKeywords", false);
jc.put("editor.suggest.showSnippets", false);
jc.put("editor.suggest.showWords", false);
JSONObject st = new JSONObject();
st.put("strings", true);
jc.put("editor.quickSuggestions", st);
jc.put("editor.suggest.insertMode", "replace");
settings.put("[json]", jc);
settings.put("json.maxItemsComputed", 30000);
JSONArray schemas = new JSONArray();
IrisData dm = null;
if (includeSchemas) {
dm = IrisData.get(project.getPath());
for (ResourceLoader<?> r : dm.getLoaders().v()) {
if (r.supportsSchemas()) {
schemas.put(r.buildSchema());
}
}
for (Class<?> i : dm.resolveSnippets()) {
try {
String snipType = i.getDeclaredAnnotation(Snippet.class).value();
JSONObject o = new JSONObject();
KList<String> fm = new KList<>();
for (int g = 1; g < 8; g++) {
fm.add("/snippet/" + snipType + Form.repeat("/*", g) + ".json");
}
o.put("fileMatch", new JSONArray(fm.toArray()));
o.put("url", "./.iris/schema/snippet/" + snipType + "-schema.json");
schemas.put(o);
IrisData snippetData = dm;
File a = new File(snippetData.getDataFolder(), ".iris/schema/snippet/" + snipType + "-schema.json");
J.attemptAsync(() -> {
try {
IO.writeAll(a, new SchemaBuilder(i, snippetData).construct().toString(4));
} catch (Throwable e) {
e.printStackTrace();
}
});
} catch (Throwable e) {
e.printStackTrace();
}
}
}
settings.put("json.schemas", schemas);
ws.put("settings", settings);
if (!includeSchemas) {
return ws;
}
File schemasFile = new File(project.getPath(), ".idea" + File.separator + "jsonSchemas.xml");
Document doc = IO.read(schemasFile);
Element mappings = (Element) doc.selectSingleNode("//component[@name='JsonSchemaMappingsProjectConfiguration']");
if (mappings == null) {
mappings = doc.getRootElement()
.addElement("component")
.addAttribute("name", "JsonSchemaMappingsProjectConfiguration");
}
Element state = (Element) mappings.selectSingleNode("state");
if (state == null) state = mappings.addElement("state");
Element map = (Element) state.selectSingleNode("map");
if (map == null) map = state.addElement("map");
var schemaMap = new KMap<String, String>();
schemas.forEach(element -> {
if (!(element instanceof JSONObject obj))
return;
String url = obj.getString("url");
String dir = obj.getJSONArray("fileMatch").getString(0);
schemaMap.put(url, dir.substring(1, dir.indexOf("/*")));
});
map.selectNodes("entry/value/SchemaInfo/option[@name='relativePathToSchema']")
.stream()
.map(node -> node.valueOf("@value"))
.forEach(schemaMap::remove);
var ideaSchemas = map;
schemaMap.forEach((url, dir) -> {
var genName = UUID.randomUUID().toString();
var info = ideaSchemas.addElement("entry")
.addAttribute("key", genName)
.addElement("value")
.addElement("SchemaInfo");
info.addElement("option")
.addAttribute("name", "generatedName")
.addAttribute("value", genName);
info.addElement("option")
.addAttribute("name", "name")
.addAttribute("value", dir);
info.addElement("option")
.addAttribute("name", "relativePathToSchema")
.addAttribute("value", url);
var item = info.addElement("option")
.addAttribute("name", "patterns")
.addElement("list")
.addElement("Item");
item.addElement("option")
.addAttribute("name", "directory")
.addAttribute("value", "true");
item.addElement("option")
.addAttribute("name", "path")
.addAttribute("value", dir);
item.addElement("option")
.addAttribute("name", "mappingKind")
.addAttribute("value", "Directory");
});
if (!schemaMap.isEmpty()) {
IO.write(schemasFile, doc);
}
return ws;
}
}
@@ -0,0 +1,252 @@
/*
* 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.project;
import com.google.gson.Gson;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.localization.BukkitRuntimeMessages;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.pack.StructurePackageClosure;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBlockData;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisEntity;
import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.IrisLootTable;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisSpawner;
import art.arcane.iris.engine.object.IrisStructurePlacement;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.volmlib.util.scheduling.O;
import org.zeroturnaround.zip.ZipUtil;
import java.io.File;
import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.UUID;
@SuppressWarnings("ALL")
public class IrisPackageCompiler {
private final IrisProject project;
public IrisPackageCompiler(IrisProject project) {
this.project = project;
}
public File compilePackage(VolmitSender sender, boolean obfuscate, boolean minify) {
String dimm = project.getName();
IrisData dm = IrisData.get(project.getPath());
IrisDimension dimension = dm.getDimensionLoader().load(dimm);
File folder = new File(IrisPlatforms.get().dataFolder(), "exports/" + dimension.getLoadKey());
IO.delete(folder);
if (folder.exists()) {
throw new IllegalStateException("Failed to clear structure package staging folder " + folder.getAbsolutePath());
}
if (!folder.mkdirs() && !folder.isDirectory()) {
throw new IllegalStateException("Failed to create structure package staging folder " + folder.getAbsolutePath());
}
IrisLogging.info("Packaging Dimension " + dimension.getName() + " " + (obfuscate ? "(Obfuscated)" : ""));
KSet<IrisRegion> regions = new KSet<>();
KSet<IrisBiome> biomes = new KSet<>();
KSet<IrisEntity> entities = new KSet<>();
KSet<IrisSpawner> spawners = new KSet<>();
KSet<IrisGenerator> generators = new KSet<>();
KSet<IrisLootTable> loot = new KSet<>();
KSet<IrisBlockData> blocks = new KSet<>();
for (String i : dm.getBlockLoader().getPossibleKeys()) {
blocks.add(dm.getBlockLoader().load(i));
}
dimension.getRegions().forEach((i) -> regions.add(dm.getRegionLoader().load(i)));
dimension.getLoot().getTables().forEach((i) -> loot.add(dm.getLootLoader().load(i)));
regions.forEach((i) -> biomes.addAll(i.getAllBiomes(() -> dm)));
regions.forEach((r) -> r.getLoot().getTables().forEach((i) -> loot.add(dm.getLootLoader().load(i))));
regions.forEach((r) -> r.getEntitySpawners().forEach((sp) -> spawners.add(dm.getSpawnerLoader().load(sp))));
dimension.getEntitySpawners().forEach((sp) -> spawners.add(dm.getSpawnerLoader().load(sp)));
biomes.forEach((i) -> i.getGenerators().forEach((j) -> generators.add(j.getCachedGenerator(() -> dm))));
biomes.forEach((r) -> r.getLoot().getTables().forEach((i) -> loot.add(dm.getLootLoader().load(i))));
biomes.forEach((r) -> r.getEntitySpawners().forEach((sp) -> spawners.add(dm.getSpawnerLoader().load(sp))));
collectSpawnerEntityKeys(spawners).forEach((i) -> entities.add(dm.getEntityLoader().load(i)));
Set<String> structureKeys = new LinkedHashSet<>();
collectStructureKeys(structureKeys, dimension.getStructures());
regions.forEach((region) -> collectStructureKeys(structureKeys, region.getStructures()));
biomes.forEach((biome) -> collectStructureKeys(structureKeys, biome.getStructures()));
KMap<String, String> renameObjects = new KMap<>();
String a;
StringBuilder b = new StringBuilder();
StringBuilder c = new StringBuilder();
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_SERIALIZING_OBJECTS));
for (IrisBiome i : biomes) {
for (IrisObjectPlacement j : i.getObjects()) {
b.append(j.hashCode());
KList<String> newNames = new KList<>();
for (String k : j.getPlace()) {
if (renameObjects.containsKey(k)) {
newNames.add(renameObjects.get(k));
continue;
}
String name = !obfuscate ? k : UUID.randomUUID().toString().replaceAll("-", "");
b.append(name);
newNames.add(name);
renameObjects.put(k, name);
}
j.setPlace(newNames);
}
}
KMap<String, KList<String>> lookupObjects = renameObjects.flip();
StringBuilder gb = new StringBuilder();
ChronoLatch cl = new ChronoLatch(1000);
O<Integer> ggg = new O<>();
ggg.set(0);
biomes.forEach((i) -> i.getObjects().forEach((j) -> j.getPlace().forEach((k) ->
{
try {
File f = dm.getObjectLoader().findFile(lookupObjects.get(k).get(0));
IO.copyFile(f, new File(folder, "objects/" + k + ".iob"));
gb.append(IO.hash(f));
ggg.set(ggg.get() + 1);
if (cl.flip()) {
int g = ggg.get();
ggg.set(0);
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_WROTE_ANOTHER_OBJECTS, MessageArgument.untrusted("g", String.valueOf(g))));
}
} catch (Throwable e) {
IrisLogging.reportError(e);
}
})));
b.append(IO.hash(gb.toString()));
c.append(IO.hash(b.toString()));
b = new StringBuilder();
IrisLogging.info("Writing Dimensional Scaffold");
try {
StructurePackageClosure structureClosure = StructurePackageClosure.collect(project.getPath(), structureKeys);
if (!structureClosure.isValid()) {
throw new IOException("Structure package closure is invalid: " + String.join("; ", structureClosure.errors()));
}
b.append(structureClosure.writeTo(folder, minify));
a = new JSONObject(new Gson().toJson(dimension)).toString(minify ? 0 : 4);
IO.writeAll(new File(folder, "dimensions/" + dimension.getLoadKey() + ".json"), a);
b.append(IO.hash(a));
for (IrisGenerator i : generators) {
a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4);
IO.writeAll(new File(folder, "generators/" + i.getLoadKey() + ".json"), a);
b.append(IO.hash(a));
}
c.append(IO.hash(b.toString()));
b = new StringBuilder();
for (IrisRegion i : regions) {
a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4);
IO.writeAll(new File(folder, "regions/" + i.getLoadKey() + ".json"), a);
b.append(IO.hash(a));
}
for (IrisBlockData i : blocks) {
a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4);
IO.writeAll(new File(folder, "blocks/" + i.getLoadKey() + ".json"), a);
b.append(IO.hash(a));
}
for (IrisBiome i : biomes) {
a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4);
IO.writeAll(new File(folder, "biomes/" + i.getLoadKey() + ".json"), a);
b.append(IO.hash(a));
}
for (IrisEntity i : entities) {
a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4);
IO.writeAll(new File(folder, "entities/" + i.getLoadKey() + ".json"), a);
b.append(IO.hash(a));
}
for (IrisLootTable i : loot) {
a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4);
IO.writeAll(new File(folder, "loot/" + i.getLoadKey() + ".json"), a);
b.append(IO.hash(a));
}
c.append(IO.hash(b.toString()));
String finalHash = IO.hash(c.toString());
JSONObject meta = new JSONObject();
meta.put("hash", finalHash);
meta.put("time", M.ms());
meta.put("version", dimension.getVersion());
IO.writeAll(new File(folder, "package.json"), meta.toString(minify ? 0 : 4));
File p = new File(IrisPlatforms.get().dataFolder(), "exports/" + dimension.getLoadKey() + ".iris");
IrisLogging.info("Compressing Package");
ZipUtil.pack(folder, p, 9);
IO.delete(folder);
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_PACKAGE_COMPILED));
return p;
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
}
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_FAILED));
return null;
}
static KSet<String> collectSpawnerEntityKeys(KSet<IrisSpawner> spawners) {
KSet<String> entityKeys = new KSet<>();
for (IrisSpawner spawner : spawners) {
spawner.getSpawns().forEach((spawn) -> entityKeys.add(spawn.getEntity()));
spawner.getInitialSpawns().forEach((spawn) -> entityKeys.add(spawn.getEntity()));
}
return entityKeys;
}
private static void collectStructureKeys(Set<String> keys, KList<IrisStructurePlacement> placements) {
if (placements == null) {
return;
}
for (IrisStructurePlacement placement : placements) {
if (placement == null || placement.getStructures() == null) {
continue;
}
for (String structureKey : placement.getStructures()) {
keys.add(structureKey);
}
}
}
}
@@ -18,85 +18,26 @@
package art.arcane.iris.core.project;
import com.google.gson.Gson;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.core.pack.StructurePackageClosure;
import art.arcane.iris.core.runtime.StudioOpenCoordinator;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBlockData;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisEntity;
import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.IrisLootTable;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisSpawner;
import art.arcane.iris.engine.object.IrisStructurePlacement;
import art.arcane.iris.engine.object.annotations.Snippet;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.volmlib.util.exceptions.IrisException;
import art.arcane.volmlib.util.hud.HudPriority;
import art.arcane.volmlib.util.hud.HudSlotClaim;
import art.arcane.volmlib.util.hud.HudSlotRequest;
import art.arcane.volmlib.util.hud.HudSurface;
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.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.volmlib.util.math.M;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.scheduling.O;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import art.arcane.iris.util.common.scheduling.jobs.Job;
import art.arcane.iris.util.common.scheduling.jobs.JobCollection;
import art.arcane.iris.util.common.scheduling.jobs.ParallelQueueJob;
import lombok.Data;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.World;
import org.bukkit.boss.BarColor;
import org.bukkit.boss.BarStyle;
import org.dom4j.Document;
import org.dom4j.Element;
import org.zeroturnaround.zip.ZipUtil;
import java.awt.Desktop;
import java.awt.GraphicsEnvironment;
import java.io.File;
import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import art.arcane.iris.core.localization.BukkitRuntimeMessages;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.RuntimeProgressMessages;
import art.arcane.iris.core.localization.RuntimeUiMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
@SuppressWarnings("ALL")
@Data
@@ -110,62 +51,6 @@ public class IrisProject {
this.name = path.getName();
}
public static int clean(VolmitSender s, File clean) {
int c = 0;
if (clean.isDirectory()) {
for (File i : clean.listFiles()) {
c += clean(s, i);
}
} else if (clean.getName().endsWith(".json")) {
try {
clean(clean);
} catch (Throwable e) {
IrisLogging.reportError(e);
IrisLogging.error("Failed to beautify " + clean.getAbsolutePath() + " You may have errors in your json!");
}
c++;
}
return c;
}
public static void clean(File clean) throws IOException {
JSONObject obj = new JSONObject(IO.readAll(clean));
fixBlocks(obj, clean);
IO.writeAll(clean, obj.toString(4));
}
public static void fixBlocks(JSONObject obj, File f) {
for (String i : obj.keySet()) {
Object o = obj.get(i);
if (i.equals("block") && o instanceof String && !o.toString().trim().isEmpty() && !o.toString().contains(":")) {
obj.put(i, "minecraft:" + o);
IrisLogging.debug("Updated Block Key: " + o + " to " + obj.getString(i) + " in " + f.getPath());
}
if (o instanceof JSONObject) {
fixBlocks((JSONObject) o, f);
} else if (o instanceof JSONArray) {
fixBlocks((JSONArray) o, f);
}
}
}
public static void fixBlocks(JSONArray obj, File f) {
for (int i = 0; i < obj.length(); i++) {
Object o = obj.get(i);
if (o instanceof JSONObject) {
fixBlocks((JSONObject) o, f);
} else if (o instanceof JSONArray) {
fixBlocks((JSONArray) o, f);
}
}
}
public boolean isOpen() {
return activeProvider != null;
}
@@ -194,72 +79,6 @@ public class IrisProject {
});
}
public void openVSCode(VolmitSender sender) {
IrisDimension d = IrisData.loadAnyDimension(getName(), null);
J.attemptAsync(() ->
{
try {
if (d == null) {
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_COULD_NOT_LOAD_DIMENSION, MessageArgument.untrusted("value", String.valueOf(getName()))));
return;
}
if (d.getLoader() == null) {
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_COULD_NOT_GET_DIMENSION_LOADER));
return;
}
File f = d.getLoader().getDataFolder();
if (!doOpenVSCode(f)) {
File ff = new File(d.getLoader().getDataFolder(), d.getLoadKey() + ".code-workspace");
IrisLogging.warn("Project missing code-workspace: " + ff.getAbsolutePath() + " Re-creating code workspace.");
try {
IO.writeAll(ff, createCodeWorkspaceConfig(false));
} catch (IOException e1) {
IrisLogging.reportError(e1);
e1.printStackTrace();
}
if (!doOpenVSCode(f)) {
IrisLogging.warn("Tried creating code workspace but failed a second time. Your project is likely corrupt.");
}
}
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
}
});
}
private boolean doOpenVSCode(File f) throws IOException {
boolean foundWork = false;
for (File i : Objects.requireNonNull(f.listFiles())) {
if (i.getName().endsWith(".code-workspace")) {
foundWork = true;
if (IrisSettings.get().getStudio().isOpenVSCode()) {
if (!GraphicsEnvironment.isHeadless()) {
IrisLogging.msg("Opening VSCode. You may see the output from VSCode.");
IrisLogging.msg("VSCode output always starts with: '(node:#####) electron'");
Thread launcherThread = new Thread(() -> {
try {
Desktop.getDesktop().open(i);
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}, "Iris-VSCode-Launcher");
launcherThread.setDaemon(true);
launcherThread.start();
}
}
break;
}
}
return foundWork;
}
public CompletableFuture<StudioOpenCoordinator.StudioOpenResult> open(VolmitSender sender, long seed, Consumer<World> onDone) throws IrisException {
if (isOpen()) {
return close().thenCompose(ignored -> openInternal(sender, seed, onDone));
@@ -287,7 +106,7 @@ public class IrisProject {
onDone
)
);
startStudioOpenReporter(sender, stage, progress, complete, failed);
StudioOpenProgressReporter.startStudioOpenReporter(sender, stage, progress, complete, failed);
future.whenComplete((result, throwable) -> {
World maintenanceWorld = null;
boolean maintenanceActive = false;
@@ -322,208 +141,6 @@ public class IrisProject {
return future;
}
private static final int STUDIO_PROGRESS_BAR_WIDTH = 44;
private void startStudioOpenReporter(VolmitSender sender, AtomicReference<String> stage, AtomicReference<Double> progress, AtomicBoolean complete, AtomicBoolean failed) {
AtomicLong nextConsoleUpdate = new AtomicLong(0L);
AtomicLong startMs = new AtomicLong(System.currentTimeMillis());
AtomicInteger taskId = new AtomicInteger(-1);
org.bukkit.boss.BossBar bossBar;
HudSlotClaim loaderClaim;
if (sender.isPlayer() && sender.player() != null) {
bossBar = Bukkit.createBossBar(
IrisLanguage.text(RuntimeProgressMessages.STUDIO_OPENING),
org.bukkit.boss.BarColor.BLUE,
org.bukkit.boss.BarStyle.SEGMENTED_20
);
bossBar.setProgress(0.0D);
bossBar.addPlayer(sender.player());
bossBar.setVisible(true);
loaderClaim = BukkitPlatform.hudSlots().open(sender.player(), new HudSlotRequest(
"iris:studio-open",
HudPriority.PROGRESS,
1200L,
List.of(HudSurface.ACTION_BAR, HudSurface.BOSS_BAR)
));
} else {
bossBar = null;
loaderClaim = null;
}
int scheduledTaskId = J.ar(() -> {
double currentProgress = Math.max(0D, Math.min(0.99D, progress.get()));
String currentStage = describeStage(stage.get());
int percent = (int) Math.round(currentProgress * 100.0D);
long elapsed = System.currentTimeMillis() - startMs.get();
if (complete.get()) {
J.car(taskId.get());
if (failed.get()) {
if (bossBar != null) {
bossBar.setProgress(Math.max(0.0D, Math.min(1.0D, currentProgress)));
bossBar.setColor(org.bukkit.boss.BarColor.RED);
bossBar.setTitle(IrisLanguage.text(
RuntimeProgressMessages.STUDIO_FAILED_PROGRESS,
MessageArgument.trusted("percent", percent)
));
J.a(() -> {
bossBar.removeAll();
bossBar.setVisible(false);
loaderClaim.release();
BukkitPlatform.hudLanes().hide(sender.player(), "iris:studio-open");
}, 60);
}
if (sender.isPlayer()) {
HudSurface loaderSurface = loaderClaim.resolve();
if (loaderSurface == HudSurface.ACTION_BAR) {
BukkitPlatform.hudLanes().hide(sender.player(), "iris:studio-open");
sender.sendAction(IrisLanguage.text(
RuntimeProgressMessages.STUDIO_ACTION_FAILED,
MessageArgument.trusted("bar", buildStudioProgressBar(currentProgress)),
MessageArgument.trusted("stage", currentStage)
));
} else if (loaderSurface == HudSurface.BOSS_BAR) {
BukkitPlatform.hudLanes().show(sender.player(), "iris:studio-open", IrisLanguage.text(
RuntimeProgressMessages.STUDIO_ACTION_FAILED,
MessageArgument.trusted("bar", ""),
MessageArgument.trusted("stage", currentStage)
), currentProgress, BarColor.RED, BarStyle.SOLID, 4000L);
}
} else {
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_STUDIO_OPEN_FAILED_2));
}
} else {
if (bossBar != null) {
bossBar.setProgress(1.0D);
bossBar.setColor(org.bukkit.boss.BarColor.GREEN);
bossBar.setTitle(IrisLanguage.text(RuntimeProgressMessages.STUDIO_READY_PROGRESS));
J.a(() -> {
bossBar.removeAll();
bossBar.setVisible(false);
loaderClaim.release();
BukkitPlatform.hudLanes().hide(sender.player(), "iris:studio-open");
}, 60);
}
if (sender.isPlayer()) {
HudSurface loaderSurface = loaderClaim.resolve();
if (loaderSurface == HudSurface.ACTION_BAR) {
BukkitPlatform.hudLanes().hide(sender.player(), "iris:studio-open");
sender.sendAction(IrisLanguage.text(
RuntimeProgressMessages.STUDIO_ACTION_READY,
MessageArgument.trusted("bar", buildStudioProgressBar(1.0D)),
MessageArgument.trusted("elapsed", Form.duration(elapsed, 1))
));
} else if (loaderSurface == HudSurface.BOSS_BAR) {
BukkitPlatform.hudLanes().show(sender.player(), "iris:studio-open", IrisLanguage.text(
RuntimeProgressMessages.STUDIO_ACTION_READY,
MessageArgument.trusted("bar", ""),
MessageArgument.trusted("elapsed", Form.duration(elapsed, 1))
), 1.0D, BarColor.GREEN, BarStyle.SOLID, 4000L);
}
} else {
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_STUDIO_READY, MessageArgument.untrusted("value", String.valueOf(Form.duration(elapsed, 1)))));
}
}
return;
}
if (sender.isPlayer() && sender.player() != null) {
if (bossBar != null) {
bossBar.setProgress(Math.max(0.0D, Math.min(1.0D, currentProgress)));
bossBar.setTitle(IrisLanguage.text(
RuntimeProgressMessages.STUDIO_OPENING_PROGRESS,
MessageArgument.trusted("percent", percent)
));
}
HudSurface loaderSurface = loaderClaim.resolve();
if (loaderSurface == HudSurface.ACTION_BAR) {
BukkitPlatform.hudLanes().hide(sender.player(), "iris:studio-open");
sender.sendAction(IrisLanguage.text(
RuntimeProgressMessages.STUDIO_ACTION_PROGRESS,
MessageArgument.trusted("bar", buildStudioProgressBar(currentProgress)),
MessageArgument.trusted("percent", percent),
MessageArgument.trusted("stage", currentStage),
MessageArgument.trusted("elapsed", Form.duration(elapsed, 0))
));
} else if (loaderSurface == HudSurface.BOSS_BAR) {
BukkitPlatform.hudLanes().show(sender.player(), "iris:studio-open", IrisLanguage.text(
RuntimeProgressMessages.STUDIO_ACTION_PROGRESS,
MessageArgument.trusted("bar", ""),
MessageArgument.trusted("percent", percent),
MessageArgument.trusted("stage", currentStage),
MessageArgument.trusted("elapsed", Form.duration(elapsed, 0))
), currentProgress, BarColor.GREEN, BarStyle.SOLID, 4000L);
}
} else {
long now = System.currentTimeMillis();
long nextUpdate = nextConsoleUpdate.get();
if (now >= nextUpdate) {
String bar = buildStudioConsoleBar(currentProgress);
sender.sendMessage(IrisLanguage.text(
RuntimeProgressMessages.STUDIO_CONSOLE_PROGRESS,
MessageArgument.trusted("bar", bar),
MessageArgument.trusted("percent", percent),
MessageArgument.trusted("stage", currentStage),
MessageArgument.trusted("elapsed", Form.duration(elapsed, 0))
));
nextConsoleUpdate.set(now + 1500L);
}
}
}, 3);
taskId.set(scheduledTaskId);
if (complete.get()) {
J.car(taskId.get());
}
}
private static String buildStudioProgressBar(double progress) {
int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, progress)) * STUDIO_PROGRESS_BAR_WIDTH);
StringBuilder bar = new StringBuilder(STUDIO_PROGRESS_BAR_WIDTH * 3 + 4);
bar.append(C.DARK_GRAY).append("[");
for (int i = 0; i < STUDIO_PROGRESS_BAR_WIDTH; i++) {
bar.append(i < filled ? C.GREEN : C.DARK_GRAY).append("|");
}
bar.append(C.DARK_GRAY).append("]");
return bar.toString();
}
private static String buildStudioConsoleBar(double progress) {
int width = 20;
int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, progress)) * width);
StringBuilder bar = new StringBuilder();
bar.append("[");
for (int i = 0; i < width; i++) {
bar.append(i < filled ? "#" : "-");
}
bar.append("]");
return bar.toString();
}
private static String describeStage(String stage) {
if (stage == null || stage.isBlank()) {
return IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_INITIALIZING);
}
return switch (stage) {
case "Queued" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_QUEUED);
case "resolve_dimension" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_RESOLVE_DIMENSION);
case "prepare_world_pack" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_PREPARE_WORLD_PACK);
case "install_datapacks" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_INSTALL_DATAPACKS);
case "create_world" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_CREATE_WORLD);
case "apply_world_rules" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_APPLY_WORLD_RULES);
case "prepare_generator" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_PREPARE_GENERATOR);
case "request_entry_chunk" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_REQUEST_ENTRY_CHUNK);
case "resolve_safe_entry" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_RESOLVE_SAFE_ENTRY);
case "teleport_player" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_TELEPORT_PLAYER);
case "finalize_open" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_FINALIZE_OPEN);
case "cleanup" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_CLEANUP);
default -> Form.capitalizeWords(stage.replace('_', ' '));
};
}
public CompletableFuture<StudioOpenCoordinator.StudioCloseResult> close() {
if (activeProvider == null) {
return CompletableFuture.completedFuture(new StudioOpenCoordinator.StudioCloseResult(null, true, true, false, null));
@@ -531,528 +148,4 @@ public class IrisProject {
return StudioOpenCoordinator.get().closeProject(this);
}
public File getCodeWorkspaceFile() {
return new File(path, getName() + ".code-workspace");
}
public boolean updateWorkspace() {
getPath().mkdirs();
File ws = getCodeWorkspaceFile();
try {
PrecisionStopwatch p = PrecisionStopwatch.start();
JSONObject j = createCodeWorkspaceConfig();
IO.writeAll(ws, j.toString(4));
p.end();
return true;
} catch (Throwable e) {
IrisLogging.reportError(e);
IrisLogging.warn("Project invalid: " + ws.getAbsolutePath() + " Re-creating. You may loose some vs-code workspace settings! But not your actual project!");
ws.delete();
try {
IO.writeAll(ws, createCodeWorkspaceConfig());
} catch (IOException e1) {
IrisLogging.reportError(e1);
e1.printStackTrace();
}
}
return false;
}
public JSONObject createCodeWorkspaceConfig() {
return createCodeWorkspaceConfig(true);
}
private JSONObject createCodeWorkspaceConfig(boolean includeSchemas) {
JSONObject ws = new JSONObject();
JSONArray folders = new JSONArray();
JSONObject folder = new JSONObject();
folder.put("path", ".");
folders.put(folder);
ws.put("folders", folders);
JSONObject settings = new JSONObject();
settings.put("workbench.colorTheme", "Monokai");
settings.put("workbench.preferredDarkColorTheme", "Solarized Dark");
settings.put("workbench.tips.enabled", false);
settings.put("workbench.tree.indent", 24);
settings.put("files.autoSave", "onFocusChange");
JSONObject jc = new JSONObject();
jc.put("editor.autoIndent", "brackets");
jc.put("editor.acceptSuggestionOnEnter", "smart");
jc.put("editor.cursorSmoothCaretAnimation", true);
jc.put("editor.dragAndDrop", false);
jc.put("files.trimTrailingWhitespace", true);
jc.put("diffEditor.ignoreTrimWhitespace", true);
jc.put("files.trimFinalNewlines", true);
jc.put("editor.suggest.showKeywords", false);
jc.put("editor.suggest.showSnippets", false);
jc.put("editor.suggest.showWords", false);
JSONObject st = new JSONObject();
st.put("strings", true);
jc.put("editor.quickSuggestions", st);
jc.put("editor.suggest.insertMode", "replace");
settings.put("[json]", jc);
settings.put("json.maxItemsComputed", 30000);
JSONArray schemas = new JSONArray();
IrisData dm = null;
if (includeSchemas) {
dm = IrisData.get(getPath());
for (ResourceLoader<?> r : dm.getLoaders().v()) {
if (r.supportsSchemas()) {
schemas.put(r.buildSchema());
}
}
for (Class<?> i : dm.resolveSnippets()) {
try {
String snipType = i.getDeclaredAnnotation(Snippet.class).value();
JSONObject o = new JSONObject();
KList<String> fm = new KList<>();
for (int g = 1; g < 8; g++) {
fm.add("/snippet/" + snipType + Form.repeat("/*", g) + ".json");
}
o.put("fileMatch", new JSONArray(fm.toArray()));
o.put("url", "./.iris/schema/snippet/" + snipType + "-schema.json");
schemas.put(o);
IrisData snippetData = dm;
File a = new File(snippetData.getDataFolder(), ".iris/schema/snippet/" + snipType + "-schema.json");
J.attemptAsync(() -> {
try {
IO.writeAll(a, new SchemaBuilder(i, snippetData).construct().toString(4));
} catch (Throwable e) {
e.printStackTrace();
}
});
} catch (Throwable e) {
e.printStackTrace();
}
}
}
settings.put("json.schemas", schemas);
ws.put("settings", settings);
if (!includeSchemas) {
return ws;
}
File schemasFile = new File(path, ".idea" + File.separator + "jsonSchemas.xml");
Document doc = IO.read(schemasFile);
Element mappings = (Element) doc.selectSingleNode("//component[@name='JsonSchemaMappingsProjectConfiguration']");
if (mappings == null) {
mappings = doc.getRootElement()
.addElement("component")
.addAttribute("name", "JsonSchemaMappingsProjectConfiguration");
}
Element state = (Element) mappings.selectSingleNode("state");
if (state == null) state = mappings.addElement("state");
Element map = (Element) state.selectSingleNode("map");
if (map == null) map = state.addElement("map");
var schemaMap = new KMap<String, String>();
schemas.forEach(element -> {
if (!(element instanceof JSONObject obj))
return;
String url = obj.getString("url");
String dir = obj.getJSONArray("fileMatch").getString(0);
schemaMap.put(url, dir.substring(1, dir.indexOf("/*")));
});
map.selectNodes("entry/value/SchemaInfo/option[@name='relativePathToSchema']")
.stream()
.map(node -> node.valueOf("@value"))
.forEach(schemaMap::remove);
var ideaSchemas = map;
schemaMap.forEach((url, dir) -> {
var genName = UUID.randomUUID().toString();
var info = ideaSchemas.addElement("entry")
.addAttribute("key", genName)
.addElement("value")
.addElement("SchemaInfo");
info.addElement("option")
.addAttribute("name", "generatedName")
.addAttribute("value", genName);
info.addElement("option")
.addAttribute("name", "name")
.addAttribute("value", dir);
info.addElement("option")
.addAttribute("name", "relativePathToSchema")
.addAttribute("value", url);
var item = info.addElement("option")
.addAttribute("name", "patterns")
.addElement("list")
.addElement("Item");
item.addElement("option")
.addAttribute("name", "directory")
.addAttribute("value", "true");
item.addElement("option")
.addAttribute("name", "path")
.addAttribute("value", dir);
item.addElement("option")
.addAttribute("name", "mappingKind")
.addAttribute("value", "Directory");
});
if (!schemaMap.isEmpty()) {
IO.write(schemasFile, doc);
}
return ws;
}
public File compilePackage(VolmitSender sender, boolean obfuscate, boolean minify) {
String dimm = getName();
IrisData dm = IrisData.get(path);
IrisDimension dimension = dm.getDimensionLoader().load(dimm);
File folder = new File(IrisPlatforms.get().dataFolder(), "exports/" + dimension.getLoadKey());
IO.delete(folder);
if (folder.exists()) {
throw new IllegalStateException("Failed to clear structure package staging folder " + folder.getAbsolutePath());
}
if (!folder.mkdirs() && !folder.isDirectory()) {
throw new IllegalStateException("Failed to create structure package staging folder " + folder.getAbsolutePath());
}
IrisLogging.info("Packaging Dimension " + dimension.getName() + " " + (obfuscate ? "(Obfuscated)" : ""));
KSet<IrisRegion> regions = new KSet<>();
KSet<IrisBiome> biomes = new KSet<>();
KSet<IrisEntity> entities = new KSet<>();
KSet<IrisSpawner> spawners = new KSet<>();
KSet<IrisGenerator> generators = new KSet<>();
KSet<IrisLootTable> loot = new KSet<>();
KSet<IrisBlockData> blocks = new KSet<>();
for (String i : dm.getBlockLoader().getPossibleKeys()) {
blocks.add(dm.getBlockLoader().load(i));
}
dimension.getRegions().forEach((i) -> regions.add(dm.getRegionLoader().load(i)));
dimension.getLoot().getTables().forEach((i) -> loot.add(dm.getLootLoader().load(i)));
regions.forEach((i) -> biomes.addAll(i.getAllBiomes(() -> dm)));
regions.forEach((r) -> r.getLoot().getTables().forEach((i) -> loot.add(dm.getLootLoader().load(i))));
regions.forEach((r) -> r.getEntitySpawners().forEach((sp) -> spawners.add(dm.getSpawnerLoader().load(sp))));
dimension.getEntitySpawners().forEach((sp) -> spawners.add(dm.getSpawnerLoader().load(sp)));
biomes.forEach((i) -> i.getGenerators().forEach((j) -> generators.add(j.getCachedGenerator(() -> dm))));
biomes.forEach((r) -> r.getLoot().getTables().forEach((i) -> loot.add(dm.getLootLoader().load(i))));
biomes.forEach((r) -> r.getEntitySpawners().forEach((sp) -> spawners.add(dm.getSpawnerLoader().load(sp))));
collectSpawnerEntityKeys(spawners).forEach((i) -> entities.add(dm.getEntityLoader().load(i)));
Set<String> structureKeys = new LinkedHashSet<>();
collectStructureKeys(structureKeys, dimension.getStructures());
regions.forEach((region) -> collectStructureKeys(structureKeys, region.getStructures()));
biomes.forEach((biome) -> collectStructureKeys(structureKeys, biome.getStructures()));
KMap<String, String> renameObjects = new KMap<>();
String a;
StringBuilder b = new StringBuilder();
StringBuilder c = new StringBuilder();
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_SERIALIZING_OBJECTS));
for (IrisBiome i : biomes) {
for (IrisObjectPlacement j : i.getObjects()) {
b.append(j.hashCode());
KList<String> newNames = new KList<>();
for (String k : j.getPlace()) {
if (renameObjects.containsKey(k)) {
newNames.add(renameObjects.get(k));
continue;
}
String name = !obfuscate ? k : UUID.randomUUID().toString().replaceAll("-", "");
b.append(name);
newNames.add(name);
renameObjects.put(k, name);
}
j.setPlace(newNames);
}
}
KMap<String, KList<String>> lookupObjects = renameObjects.flip();
StringBuilder gb = new StringBuilder();
ChronoLatch cl = new ChronoLatch(1000);
O<Integer> ggg = new O<>();
ggg.set(0);
biomes.forEach((i) -> i.getObjects().forEach((j) -> j.getPlace().forEach((k) ->
{
try {
File f = dm.getObjectLoader().findFile(lookupObjects.get(k).get(0));
IO.copyFile(f, new File(folder, "objects/" + k + ".iob"));
gb.append(IO.hash(f));
ggg.set(ggg.get() + 1);
if (cl.flip()) {
int g = ggg.get();
ggg.set(0);
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_WROTE_ANOTHER_OBJECTS, MessageArgument.untrusted("g", String.valueOf(g))));
}
} catch (Throwable e) {
IrisLogging.reportError(e);
}
})));
b.append(IO.hash(gb.toString()));
c.append(IO.hash(b.toString()));
b = new StringBuilder();
IrisLogging.info("Writing Dimensional Scaffold");
try {
StructurePackageClosure structureClosure = StructurePackageClosure.collect(path, structureKeys);
if (!structureClosure.isValid()) {
throw new IOException("Structure package closure is invalid: " + String.join("; ", structureClosure.errors()));
}
b.append(structureClosure.writeTo(folder, minify));
a = new JSONObject(new Gson().toJson(dimension)).toString(minify ? 0 : 4);
IO.writeAll(new File(folder, "dimensions/" + dimension.getLoadKey() + ".json"), a);
b.append(IO.hash(a));
for (IrisGenerator i : generators) {
a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4);
IO.writeAll(new File(folder, "generators/" + i.getLoadKey() + ".json"), a);
b.append(IO.hash(a));
}
c.append(IO.hash(b.toString()));
b = new StringBuilder();
for (IrisRegion i : regions) {
a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4);
IO.writeAll(new File(folder, "regions/" + i.getLoadKey() + ".json"), a);
b.append(IO.hash(a));
}
for (IrisBlockData i : blocks) {
a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4);
IO.writeAll(new File(folder, "blocks/" + i.getLoadKey() + ".json"), a);
b.append(IO.hash(a));
}
for (IrisBiome i : biomes) {
a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4);
IO.writeAll(new File(folder, "biomes/" + i.getLoadKey() + ".json"), a);
b.append(IO.hash(a));
}
for (IrisEntity i : entities) {
a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4);
IO.writeAll(new File(folder, "entities/" + i.getLoadKey() + ".json"), a);
b.append(IO.hash(a));
}
for (IrisLootTable i : loot) {
a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4);
IO.writeAll(new File(folder, "loot/" + i.getLoadKey() + ".json"), a);
b.append(IO.hash(a));
}
c.append(IO.hash(b.toString()));
String finalHash = IO.hash(c.toString());
JSONObject meta = new JSONObject();
meta.put("hash", finalHash);
meta.put("time", M.ms());
meta.put("version", dimension.getVersion());
IO.writeAll(new File(folder, "package.json"), meta.toString(minify ? 0 : 4));
File p = new File(IrisPlatforms.get().dataFolder(), "exports/" + dimension.getLoadKey() + ".iris");
IrisLogging.info("Compressing Package");
ZipUtil.pack(folder, p, 9);
IO.delete(folder);
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_PACKAGE_COMPILED));
return p;
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
}
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_FAILED));
return null;
}
static KSet<String> collectSpawnerEntityKeys(KSet<IrisSpawner> spawners) {
KSet<String> entityKeys = new KSet<>();
for (IrisSpawner spawner : spawners) {
spawner.getSpawns().forEach((spawn) -> entityKeys.add(spawn.getEntity()));
spawner.getInitialSpawns().forEach((spawn) -> entityKeys.add(spawn.getEntity()));
}
return entityKeys;
}
private static void collectStructureKeys(Set<String> keys, KList<IrisStructurePlacement> placements) {
if (placements == null) {
return;
}
for (IrisStructurePlacement placement : placements) {
if (placement == null || placement.getStructures() == null) {
continue;
}
for (String structureKey : placement.getStructures()) {
keys.add(structureKey);
}
}
}
public void compile(VolmitSender sender) {
IrisData data = IrisData.get(getPath());
KList<Job> jobs = new KList<>();
KList<File> files = new KList<>();
KList<File> objects = new KList<>();
files(getPath(), files);
filesObjects(getPath(), objects);
jobs.add(new ParallelQueueJob<File>() {
@Override
public void execute(File f) {
try {
IrisObject o = new IrisObject(0, 0, 0);
o.read(f);
if (o.getBlocks().isEmpty()) {
sender.sendComponent(Component.text(IrisLanguage.plain(
RuntimeUiMessages.COMPILE_IOB_EMPTY,
MessageArgument.untrusted("file", f.getName())
), NamedTextColor.RED)
.hoverEvent(HoverEvent.showText(Component.text(IrisLanguage.plain(
RuntimeUiMessages.COMPILE_IOB_EMPTY_HOVER,
MessageArgument.untrusted("path", f.getPath())
), NamedTextColor.YELLOW))));
}
if (o.getW() == 0 || o.getH() == 0 || o.getD() == 0) {
sender.sendComponent(Component.text(IrisLanguage.plain(
RuntimeUiMessages.COMPILE_IOB_NOT_3D,
MessageArgument.untrusted("file", f.getName())
), NamedTextColor.RED)
.hoverEvent(HoverEvent.showText(Component.text(IrisLanguage.plain(
RuntimeUiMessages.COMPILE_IOB_NOT_3D_HOVER,
MessageArgument.untrusted("path", f.getPath())
), NamedTextColor.YELLOW))));
}
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public String getName() {
return "IOB";
}
}.queue(objects));
jobs.add(new ParallelQueueJob<File>() {
@Override
public void execute(File f) {
try {
JSONObject p = new JSONObject(IO.readAll(f));
fixBlocks(p);
scanForErrors(data, f, p, sender);
IO.writeAll(f, p.toString(4));
} catch (Throwable e) {
sender.sendComponent(Component.text(IrisLanguage.plain(
RuntimeUiMessages.COMPILE_JSON_ERROR,
MessageArgument.untrusted("file", f.getName())
), NamedTextColor.RED)
.hoverEvent(HoverEvent.showText(Component.text(IrisLanguage.plain(
RuntimeUiMessages.COMPILE_JSON_ERROR_HOVER,
MessageArgument.untrusted("path", f.getPath()),
MessageArgument.untrusted("error", String.valueOf(e.getMessage()))
), NamedTextColor.YELLOW))));
}
}
@Override
public String getName() {
return "JSON";
}
}.queue(files));
new JobCollection(IrisLanguage.text(RuntimeUiMessages.JOB_COMPILE), jobs).execute(sender);
}
private void scanForErrors(IrisData data, File f, JSONObject p, VolmitSender sender) {
String key = data.toLoadKey(f);
ResourceLoader<?> loader = data.getTypedLoaderFor(f);
if (loader == null) {
sender.sendMessage(IrisLanguage.text(
RuntimeUiMessages.COMPILE_LOADER_NOT_FOUND,
MessageArgument.untrusted("path", f.getPath())
));
return;
}
IrisRegistrant load = loader.load(key);
compare(load.getClass(), p, sender, new KList<>());
load.scanForErrors(p, sender);
}
public void compare(Class<?> c, JSONObject j, VolmitSender sender, KList<String> path) {
try {
Object o = c.getClass().getConstructor().newInstance();
} catch (Throwable e) {
}
}
public void files(File clean, KList<File> files) {
if (clean.isDirectory()) {
for (File i : clean.listFiles()) {
files(i, files);
}
} else if (clean.getName().endsWith(".json")) {
try {
files.add(clean);
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
}
public void filesObjects(File clean, KList<File> files) {
if (clean.isDirectory()) {
for (File i : clean.listFiles()) {
filesObjects(i, files);
}
} else if (clean.getName().endsWith(".iob")) {
try {
files.add(clean);
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
}
private void fixBlocks(JSONObject obj) {
for (String i : obj.keySet()) {
Object o = obj.get(i);
if (i.equals("block") && o instanceof String && !o.toString().trim().isEmpty() && !o.toString().contains(":")) {
obj.put(i, "minecraft:" + o);
}
if (o instanceof JSONObject) {
fixBlocks((JSONObject) o);
} else if (o instanceof JSONArray) {
fixBlocks((JSONArray) o);
}
}
}
private void fixBlocks(JSONArray obj) {
for (int i = 0; i < obj.length(); i++) {
Object o = obj.get(i);
if (o instanceof JSONObject) {
fixBlocks((JSONObject) o);
} else if (o instanceof JSONArray) {
fixBlocks((JSONArray) o);
}
}
}
}
@@ -0,0 +1,147 @@
/*
* 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.project;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import java.io.File;
import java.io.IOException;
@SuppressWarnings("ALL")
public final class IrisProjectCleaner {
private IrisProjectCleaner() {
}
public static int clean(VolmitSender s, File clean) {
int c = 0;
if (clean.isDirectory()) {
for (File i : clean.listFiles()) {
c += clean(s, i);
}
} else if (clean.getName().endsWith(".json")) {
try {
clean(clean);
} catch (Throwable e) {
IrisLogging.reportError(e);
IrisLogging.error("Failed to beautify " + clean.getAbsolutePath() + " You may have errors in your json!");
}
c++;
}
return c;
}
public static void clean(File clean) throws IOException {
JSONObject obj = new JSONObject(IO.readAll(clean));
fixBlocks(obj, clean);
IO.writeAll(clean, obj.toString(4));
}
public static void fixBlocks(JSONObject obj, File f) {
for (String i : obj.keySet()) {
Object o = obj.get(i);
if (i.equals("block") && o instanceof String && !o.toString().trim().isEmpty() && !o.toString().contains(":")) {
obj.put(i, "minecraft:" + o);
IrisLogging.debug("Updated Block Key: " + o + " to " + obj.getString(i) + " in " + f.getPath());
}
if (o instanceof JSONObject) {
fixBlocks((JSONObject) o, f);
} else if (o instanceof JSONArray) {
fixBlocks((JSONArray) o, f);
}
}
}
public static void fixBlocks(JSONArray obj, File f) {
for (int i = 0; i < obj.length(); i++) {
Object o = obj.get(i);
if (o instanceof JSONObject) {
fixBlocks((JSONObject) o, f);
} else if (o instanceof JSONArray) {
fixBlocks((JSONArray) o, f);
}
}
}
static void fixBlocks(JSONObject obj) {
for (String i : obj.keySet()) {
Object o = obj.get(i);
if (i.equals("block") && o instanceof String && !o.toString().trim().isEmpty() && !o.toString().contains(":")) {
obj.put(i, "minecraft:" + o);
}
if (o instanceof JSONObject) {
fixBlocks((JSONObject) o);
} else if (o instanceof JSONArray) {
fixBlocks((JSONArray) o);
}
}
}
static void fixBlocks(JSONArray obj) {
for (int i = 0; i < obj.length(); i++) {
Object o = obj.get(i);
if (o instanceof JSONObject) {
fixBlocks((JSONObject) o);
} else if (o instanceof JSONArray) {
fixBlocks((JSONArray) o);
}
}
}
public static void files(File clean, KList<File> files) {
if (clean.isDirectory()) {
for (File i : clean.listFiles()) {
files(i, files);
}
} else if (clean.getName().endsWith(".json")) {
try {
files.add(clean);
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
}
public static void filesObjects(File clean, KList<File> files) {
if (clean.isDirectory()) {
for (File i : clean.listFiles()) {
filesObjects(i, files);
}
} else if (clean.getName().endsWith(".iob")) {
try {
files.add(clean);
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
}
}
@@ -0,0 +1,152 @@
/*
* 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.project;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.RuntimeUiMessages;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.jobs.Job;
import art.arcane.iris.util.common.scheduling.jobs.JobCollection;
import art.arcane.iris.util.common.scheduling.jobs.ParallelQueueJob;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.volmlib.util.localization.MessageArgument;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import java.io.File;
import java.io.IOException;
@SuppressWarnings("ALL")
public class IrisProjectCompiler {
private final IrisProject project;
public IrisProjectCompiler(IrisProject project) {
this.project = project;
}
public void compile(VolmitSender sender) {
IrisData data = IrisData.get(project.getPath());
KList<Job> jobs = new KList<>();
KList<File> files = new KList<>();
KList<File> objects = new KList<>();
IrisProjectCleaner.files(project.getPath(), files);
IrisProjectCleaner.filesObjects(project.getPath(), objects);
jobs.add(new ParallelQueueJob<File>() {
@Override
public void execute(File f) {
try {
IrisObject o = new IrisObject(0, 0, 0);
o.read(f);
if (o.getBlocks().isEmpty()) {
sender.sendComponent(Component.text(IrisLanguage.plain(
RuntimeUiMessages.COMPILE_IOB_EMPTY,
MessageArgument.untrusted("file", f.getName())
), NamedTextColor.RED)
.hoverEvent(HoverEvent.showText(Component.text(IrisLanguage.plain(
RuntimeUiMessages.COMPILE_IOB_EMPTY_HOVER,
MessageArgument.untrusted("path", f.getPath())
), NamedTextColor.YELLOW))));
}
if (o.getW() == 0 || o.getH() == 0 || o.getD() == 0) {
sender.sendComponent(Component.text(IrisLanguage.plain(
RuntimeUiMessages.COMPILE_IOB_NOT_3D,
MessageArgument.untrusted("file", f.getName())
), NamedTextColor.RED)
.hoverEvent(HoverEvent.showText(Component.text(IrisLanguage.plain(
RuntimeUiMessages.COMPILE_IOB_NOT_3D_HOVER,
MessageArgument.untrusted("path", f.getPath())
), NamedTextColor.YELLOW))));
}
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public String getName() {
return "IOB";
}
}.queue(objects));
jobs.add(new ParallelQueueJob<File>() {
@Override
public void execute(File f) {
try {
JSONObject p = new JSONObject(IO.readAll(f));
IrisProjectCleaner.fixBlocks(p);
scanForErrors(data, f, p, sender);
IO.writeAll(f, p.toString(4));
} catch (Throwable e) {
sender.sendComponent(Component.text(IrisLanguage.plain(
RuntimeUiMessages.COMPILE_JSON_ERROR,
MessageArgument.untrusted("file", f.getName())
), NamedTextColor.RED)
.hoverEvent(HoverEvent.showText(Component.text(IrisLanguage.plain(
RuntimeUiMessages.COMPILE_JSON_ERROR_HOVER,
MessageArgument.untrusted("path", f.getPath()),
MessageArgument.untrusted("error", String.valueOf(e.getMessage()))
), NamedTextColor.YELLOW))));
}
}
@Override
public String getName() {
return "JSON";
}
}.queue(files));
new JobCollection(IrisLanguage.text(RuntimeUiMessages.JOB_COMPILE), jobs).execute(sender);
}
private void scanForErrors(IrisData data, File f, JSONObject p, VolmitSender sender) {
String key = data.toLoadKey(f);
ResourceLoader<?> loader = data.getTypedLoaderFor(f);
if (loader == null) {
sender.sendMessage(IrisLanguage.text(
RuntimeUiMessages.COMPILE_LOADER_NOT_FOUND,
MessageArgument.untrusted("path", f.getPath())
));
return;
}
IrisRegistrant load = loader.load(key);
compare(load.getClass(), p, sender, new KList<>());
load.scanForErrors(p, sender);
}
public void compare(Class<?> c, JSONObject j, VolmitSender sender, KList<String> path) {
try {
Object o = c.getClass().getConstructor().newInstance();
} catch (Throwable e) {
}
}
}
@@ -0,0 +1,250 @@
/*
* 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.project;
import art.arcane.iris.core.localization.BukkitRuntimeMessages;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.RuntimeProgressMessages;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.hud.HudPriority;
import art.arcane.volmlib.util.hud.HudSlotClaim;
import art.arcane.volmlib.util.hud.HudSlotRequest;
import art.arcane.volmlib.util.hud.HudSurface;
import art.arcane.volmlib.util.localization.MessageArgument;
import org.bukkit.Bukkit;
import org.bukkit.boss.BarColor;
import org.bukkit.boss.BarStyle;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
@SuppressWarnings("ALL")
final class StudioOpenProgressReporter {
private static final int STUDIO_PROGRESS_BAR_WIDTH = 44;
private StudioOpenProgressReporter() {
}
static void startStudioOpenReporter(VolmitSender sender, AtomicReference<String> stage, AtomicReference<Double> progress, AtomicBoolean complete, AtomicBoolean failed) {
AtomicLong nextConsoleUpdate = new AtomicLong(0L);
AtomicLong startMs = new AtomicLong(System.currentTimeMillis());
AtomicInteger taskId = new AtomicInteger(-1);
org.bukkit.boss.BossBar bossBar;
HudSlotClaim loaderClaim;
if (sender.isPlayer() && sender.player() != null) {
bossBar = Bukkit.createBossBar(
IrisLanguage.text(RuntimeProgressMessages.STUDIO_OPENING),
org.bukkit.boss.BarColor.BLUE,
org.bukkit.boss.BarStyle.SEGMENTED_20
);
bossBar.setProgress(0.0D);
bossBar.addPlayer(sender.player());
bossBar.setVisible(true);
loaderClaim = BukkitPlatform.hudSlots().open(sender.player(), new HudSlotRequest(
"iris:studio-open",
HudPriority.PROGRESS,
1200L,
List.of(HudSurface.ACTION_BAR, HudSurface.BOSS_BAR)
));
} else {
bossBar = null;
loaderClaim = null;
}
int scheduledTaskId = J.ar(() -> {
double currentProgress = Math.max(0D, Math.min(0.99D, progress.get()));
String currentStage = describeStage(stage.get());
int percent = (int) Math.round(currentProgress * 100.0D);
long elapsed = System.currentTimeMillis() - startMs.get();
if (complete.get()) {
J.car(taskId.get());
if (failed.get()) {
if (bossBar != null) {
bossBar.setProgress(Math.max(0.0D, Math.min(1.0D, currentProgress)));
bossBar.setColor(org.bukkit.boss.BarColor.RED);
bossBar.setTitle(IrisLanguage.text(
RuntimeProgressMessages.STUDIO_FAILED_PROGRESS,
MessageArgument.trusted("percent", percent)
));
J.a(() -> {
bossBar.removeAll();
bossBar.setVisible(false);
loaderClaim.release();
BukkitPlatform.hudLanes().hide(sender.player(), "iris:studio-open");
}, 60);
}
if (sender.isPlayer()) {
HudSurface loaderSurface = loaderClaim.resolve();
if (loaderSurface == HudSurface.ACTION_BAR) {
BukkitPlatform.hudLanes().hide(sender.player(), "iris:studio-open");
sender.sendAction(IrisLanguage.text(
RuntimeProgressMessages.STUDIO_ACTION_FAILED,
MessageArgument.trusted("bar", buildStudioProgressBar(currentProgress)),
MessageArgument.trusted("stage", currentStage)
));
} else if (loaderSurface == HudSurface.BOSS_BAR) {
BukkitPlatform.hudLanes().show(sender.player(), "iris:studio-open", IrisLanguage.text(
RuntimeProgressMessages.STUDIO_ACTION_FAILED,
MessageArgument.trusted("bar", ""),
MessageArgument.trusted("stage", currentStage)
), currentProgress, BarColor.RED, BarStyle.SOLID, 4000L);
}
} else {
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_STUDIO_OPEN_FAILED_2));
}
} else {
if (bossBar != null) {
bossBar.setProgress(1.0D);
bossBar.setColor(org.bukkit.boss.BarColor.GREEN);
bossBar.setTitle(IrisLanguage.text(RuntimeProgressMessages.STUDIO_READY_PROGRESS));
J.a(() -> {
bossBar.removeAll();
bossBar.setVisible(false);
loaderClaim.release();
BukkitPlatform.hudLanes().hide(sender.player(), "iris:studio-open");
}, 60);
}
if (sender.isPlayer()) {
HudSurface loaderSurface = loaderClaim.resolve();
if (loaderSurface == HudSurface.ACTION_BAR) {
BukkitPlatform.hudLanes().hide(sender.player(), "iris:studio-open");
sender.sendAction(IrisLanguage.text(
RuntimeProgressMessages.STUDIO_ACTION_READY,
MessageArgument.trusted("bar", buildStudioProgressBar(1.0D)),
MessageArgument.trusted("elapsed", Form.duration(elapsed, 1))
));
} else if (loaderSurface == HudSurface.BOSS_BAR) {
BukkitPlatform.hudLanes().show(sender.player(), "iris:studio-open", IrisLanguage.text(
RuntimeProgressMessages.STUDIO_ACTION_READY,
MessageArgument.trusted("bar", ""),
MessageArgument.trusted("elapsed", Form.duration(elapsed, 1))
), 1.0D, BarColor.GREEN, BarStyle.SOLID, 4000L);
}
} else {
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_STUDIO_READY, MessageArgument.untrusted("value", String.valueOf(Form.duration(elapsed, 1)))));
}
}
return;
}
if (sender.isPlayer() && sender.player() != null) {
if (bossBar != null) {
bossBar.setProgress(Math.max(0.0D, Math.min(1.0D, currentProgress)));
bossBar.setTitle(IrisLanguage.text(
RuntimeProgressMessages.STUDIO_OPENING_PROGRESS,
MessageArgument.trusted("percent", percent)
));
}
HudSurface loaderSurface = loaderClaim.resolve();
if (loaderSurface == HudSurface.ACTION_BAR) {
BukkitPlatform.hudLanes().hide(sender.player(), "iris:studio-open");
sender.sendAction(IrisLanguage.text(
RuntimeProgressMessages.STUDIO_ACTION_PROGRESS,
MessageArgument.trusted("bar", buildStudioProgressBar(currentProgress)),
MessageArgument.trusted("percent", percent),
MessageArgument.trusted("stage", currentStage),
MessageArgument.trusted("elapsed", Form.duration(elapsed, 0))
));
} else if (loaderSurface == HudSurface.BOSS_BAR) {
BukkitPlatform.hudLanes().show(sender.player(), "iris:studio-open", IrisLanguage.text(
RuntimeProgressMessages.STUDIO_ACTION_PROGRESS,
MessageArgument.trusted("bar", ""),
MessageArgument.trusted("percent", percent),
MessageArgument.trusted("stage", currentStage),
MessageArgument.trusted("elapsed", Form.duration(elapsed, 0))
), currentProgress, BarColor.GREEN, BarStyle.SOLID, 4000L);
}
} else {
long now = System.currentTimeMillis();
long nextUpdate = nextConsoleUpdate.get();
if (now >= nextUpdate) {
String bar = buildStudioConsoleBar(currentProgress);
sender.sendMessage(IrisLanguage.text(
RuntimeProgressMessages.STUDIO_CONSOLE_PROGRESS,
MessageArgument.trusted("bar", bar),
MessageArgument.trusted("percent", percent),
MessageArgument.trusted("stage", currentStage),
MessageArgument.trusted("elapsed", Form.duration(elapsed, 0))
));
nextConsoleUpdate.set(now + 1500L);
}
}
}, 3);
taskId.set(scheduledTaskId);
if (complete.get()) {
J.car(taskId.get());
}
}
private static String buildStudioProgressBar(double progress) {
int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, progress)) * STUDIO_PROGRESS_BAR_WIDTH);
StringBuilder bar = new StringBuilder(STUDIO_PROGRESS_BAR_WIDTH * 3 + 4);
bar.append(C.DARK_GRAY).append("[");
for (int i = 0; i < STUDIO_PROGRESS_BAR_WIDTH; i++) {
bar.append(i < filled ? C.GREEN : C.DARK_GRAY).append("|");
}
bar.append(C.DARK_GRAY).append("]");
return bar.toString();
}
private static String buildStudioConsoleBar(double progress) {
int width = 20;
int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, progress)) * width);
StringBuilder bar = new StringBuilder();
bar.append("[");
for (int i = 0; i < width; i++) {
bar.append(i < filled ? "#" : "-");
}
bar.append("]");
return bar.toString();
}
private static String describeStage(String stage) {
if (stage == null || stage.isBlank()) {
return IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_INITIALIZING);
}
return switch (stage) {
case "Queued" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_QUEUED);
case "resolve_dimension" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_RESOLVE_DIMENSION);
case "prepare_world_pack" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_PREPARE_WORLD_PACK);
case "install_datapacks" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_INSTALL_DATAPACKS);
case "create_world" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_CREATE_WORLD);
case "apply_world_rules" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_APPLY_WORLD_RULES);
case "prepare_generator" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_PREPARE_GENERATOR);
case "request_entry_chunk" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_REQUEST_ENTRY_CHUNK);
case "resolve_safe_entry" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_RESOLVE_SAFE_ENTRY);
case "teleport_player" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_TELEPORT_PLAYER);
case "finalize_open" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_FINALIZE_OPEN);
case "cleanup" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_CLEANUP);
default -> Form.capitalizeWords(stage.replace('_', ' '));
};
}
}
@@ -6,6 +6,7 @@ import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.project.IrisCodeWorkspace;
import art.arcane.iris.core.tools.IrisCreator;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
@@ -185,7 +186,7 @@ public final class StudioOpenCoordinator {
request.project().setActiveProvider(provider);
}
if (request.openWorkspace() && request.project() != null) {
request.project().openVSCode(request.sender());
new IrisCodeWorkspace(request.project()).openVSCode(request.sender());
}
if (request.onDone() != null) {
request.onDone().accept(world);
@@ -40,7 +40,6 @@ import org.bukkit.event.server.PluginEnableEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.MissingResourceException;
@@ -112,7 +111,13 @@ public class ExternalDataSVC implements IrisService {
}
public Optional<BlockData> getBlockData(final Identifier key) {
Pair<Identifier, KMap<String, String>> pair = parseState(key);
Pair<Identifier, KMap<String, String>> pair;
try {
pair = parseState(key);
} catch (IllegalArgumentException e) {
IrisLogging.error(e.getMessage());
return Optional.empty();
}
Identifier mod = pair.getA();
Optional<ExternalDataProvider> provider = activeProviders.stream().filter(p -> p.isValidProvider(mod, DataType.BLOCK)).findFirst();
@@ -190,15 +195,25 @@ public class ExternalDataSVC implements IrisService {
}
public static Pair<Identifier, KMap<String, String>> parseState(Identifier key) {
if (!key.key().contains("[") || !key.key().contains("]")) {
String raw = key.key();
int open = raw.indexOf('[');
int close = raw.lastIndexOf(']');
if (open < 0 || close < open) {
return new Pair<>(key, new KMap<>());
}
String state = key.key().split("\\Q[\\E")[1].split("\\Q]\\E")[0];
String state = raw.substring(open + 1, close);
KMap<String, String> stateMap = new KMap<>();
if (!state.isEmpty()) {
Arrays.stream(state.split(",")).forEach(s -> stateMap.put(s.split("=")[0], s.split("=")[1]));
for (String entry : state.split(",")) {
String[] pair = entry.split("=", 2);
if (pair.length != 2 || pair[0].isBlank() || pair[1].isBlank()) {
throw new IllegalArgumentException("Malformed block state \"" + entry + "\" in \"" + key + "\" (expected key=value)");
}
stateMap.put(pair[0], pair[1]);
}
}
return new Pair<>(new Identifier(key.namespace(), key.key().split("\\Q[\\E")[0]), stateMap);
return new Pair<>(new Identifier(key.namespace(), raw.substring(0, open)), stateMap);
}
public static Identifier buildState(Identifier key, KMap<String, String> state) {
@@ -1,5 +1,6 @@
package art.arcane.iris.core.service;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.IrisWorldStorage;
import art.arcane.iris.core.pregenerator.cache.PregenCache;
@@ -24,10 +25,11 @@ import java.lang.ref.WeakReference;
import java.util.function.Function;
public class GlobalCacheSVC implements IrisService {
private static final long TRIMMER_JOIN_MS = 2_000;
private static final KMap<String, Reference<PregenCache>> REFERENCE_CACHE = new KMap<>();
private final KMap<String, PregenCache> globalCache = new KMap<>();
private transient boolean lastState;
private static boolean disabled = true;
private static volatile boolean disabled = true;
private Looper trimmer;
@Override
@@ -55,11 +57,16 @@ public class GlobalCacheSVC implements IrisService {
public void onDisable() {
disabled = true;
Looper activeTrimmer = trimmer;
trimmer = null;
if (activeTrimmer != null) {
activeTrimmer.interrupt();
try {
activeTrimmer.join();
activeTrimmer.join(TRIMMER_JOIN_MS);
} catch (InterruptedException ignored) {
}
if (activeTrimmer.isAlive()) {
IrisLogging.warn("Global cache trimmer did not stop within " + TRIMMER_JOIN_MS + "ms.");
}
}
globalCache.qclear((world, cache) -> cache.write());
}
@@ -86,6 +86,10 @@ public class PreservationSVC implements IrisService, PreservationRegistry {
return 60000;
}
};
dereferencer.setName("Iris Preservation");
dereferencer.setDaemon(true);
dereferencer.setPriority(Thread.MIN_PRIORITY);
dereferencer.start();
}
@Override
@@ -31,6 +31,8 @@ import art.arcane.iris.core.pack.PackDownloader;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.project.IrisPackageCompiler;
import art.arcane.iris.core.project.IrisCodeWorkspace;
import art.arcane.iris.core.project.IrisProjectCopier;
import art.arcane.iris.core.runtime.TransientWorldCleanupSupport;
import art.arcane.iris.core.tools.IrisToolbelt;
@@ -378,7 +380,7 @@ public class StudioSVC implements IrisService {
}
public void openVSCode(VolmitSender sender, String dim) {
new IrisProject(new File(getWorkspaceFolder(), dim)).openVSCode(sender);
new IrisCodeWorkspace(new IrisProject(new File(getWorkspaceFolder(), dim))).openVSCode(sender);
}
public File getWorkspaceFolder(String... sub) {
@@ -475,7 +477,7 @@ public class StudioSVC implements IrisService {
}
public File compilePackage(VolmitSender sender, String d, boolean obfuscate, boolean minify) {
return new IrisProject(new File(getWorkspaceFolder(), d)).compilePackage(sender, obfuscate, minify);
return new IrisPackageCompiler(new IrisProject(new File(getWorkspaceFolder(), d))).compilePackage(sender, obfuscate, minify);
}
public void createFrom(String existingPack, String newName) {
@@ -496,7 +498,7 @@ public class StudioSVC implements IrisService {
try {
IrisProject p = new IrisProject(getWorkspaceFolder(newName));
JSONObject ws = p.createCodeWorkspaceConfig();
JSONObject ws = new IrisCodeWorkspace(p).createCodeWorkspaceConfig();
IO.writeAll(getWorkspaceFile(newName, newName + ".code-workspace"), ws.toString(0));
} catch (JSONException | IOException e) {
IrisLogging.reportError(e);
@@ -548,7 +550,7 @@ public class StudioSVC implements IrisService {
public void updateWorkspace() {
if (isProjectOpen()) {
activeProject.updateWorkspace();
new IrisCodeWorkspace(activeProject).updateWorkspace();
}
}
}
@@ -1,315 +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.structure.studio;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
public record SimpleStructureStudioCell(
int x,
int z,
SimpleStructureStudioTopology topology,
int quarterTurns,
SimpleStructureStudioRotationPolicy rotationPolicy,
String connectorChannel,
int connectorHeight,
List<SimpleStructureStudioVariant> variants,
int activeVariantIndex
) {
public static final String DEFAULT_CONNECTOR_CHANNEL = "path";
private static final Pattern CONNECTOR_CHANNEL_PATTERN = Pattern.compile(
"(?:[a-z0-9._-]+:)?[a-z0-9._-]+(?:/[a-z0-9._-]+)*"
);
public SimpleStructureStudioCell {
if (x < 0 || z < 0) {
throw new IllegalArgumentException("Cell coordinates cannot be negative: " + x + ", " + z);
}
Objects.requireNonNull(topology, "topology");
Objects.requireNonNull(rotationPolicy, "rotationPolicy");
quarterTurns = Math.floorMod(quarterTurns, 4);
if (!rotationPolicy.allows(quarterTurns)) {
throw new IllegalArgumentException(
"Rotation policy " + rotationPolicy + " does not allow quarter turn " + quarterTurns
);
}
Objects.requireNonNull(connectorChannel, "connectorChannel");
if (!CONNECTOR_CHANNEL_PATTERN.matcher(connectorChannel).matches()) {
throw new IllegalArgumentException("Invalid connector channel: " + connectorChannel);
}
if (connectorHeight < 0) {
throw new IllegalArgumentException("Connector height cannot be negative: " + connectorHeight);
}
Objects.requireNonNull(variants, "variants");
variants = List.copyOf(variants);
validateVariants(variants);
if (topology == SimpleStructureStudioTopology.EMPTY && !variants.isEmpty()) {
throw new IllegalArgumentException("Empty cells cannot contain variants");
}
if (variants.isEmpty() && activeVariantIndex != -1) {
throw new IllegalArgumentException("A cell without variants must use activeVariantIndex -1");
}
if (!variants.isEmpty() && (activeVariantIndex < 0 || activeVariantIndex >= variants.size())) {
throw new IllegalArgumentException("Active variant index is outside the variant list: " + activeVariantIndex);
}
}
public static SimpleStructureStudioCell empty(int x, int z) {
return new SimpleStructureStudioCell(
x,
z,
SimpleStructureStudioTopology.EMPTY,
0,
SimpleStructureStudioRotationPolicy.FIXED,
DEFAULT_CONNECTOR_CHANNEL,
0,
List.of(),
-1
);
}
public static SimpleStructureStudioCell create(int x, int z, SimpleStructureStudioTopology topology) {
Objects.requireNonNull(topology, "topology");
if (topology == SimpleStructureStudioTopology.EMPTY) {
return empty(x, z);
}
return new SimpleStructureStudioCell(
x,
z,
topology,
0,
SimpleStructureStudioRotationPolicy.QUARTER_TURNS,
DEFAULT_CONNECTOR_CHANNEL,
0,
List.of(),
-1
);
}
public boolean isEmpty() {
return topology == SimpleStructureStudioTopology.EMPTY;
}
public int connectorMask() {
return topology.connectorMask(quarterTurns);
}
public boolean connects(SimpleStructureStudioDirection direction) {
return topology.connects(direction, quarterTurns);
}
public Optional<SimpleStructureStudioVariant> activeVariant() {
if (activeVariantIndex < 0) {
return Optional.empty();
}
return Optional.of(variants.get(activeVariantIndex));
}
public SimpleStructureStudioCell withTopology(SimpleStructureStudioTopology newTopology) {
Objects.requireNonNull(newTopology, "newTopology");
if (newTopology == SimpleStructureStudioTopology.EMPTY) {
return empty(x, z);
}
if (isEmpty()) {
return create(x, z, newTopology);
}
return new SimpleStructureStudioCell(
x,
z,
newTopology,
quarterTurns,
rotationPolicy,
connectorChannel,
connectorHeight,
variants,
activeVariantIndex
);
}
public SimpleStructureStudioCell withQuarterTurns(int newQuarterTurns) {
return new SimpleStructureStudioCell(
x,
z,
topology,
newQuarterTurns,
rotationPolicy,
connectorChannel,
connectorHeight,
variants,
activeVariantIndex
);
}
public SimpleStructureStudioCell rotateClockwise() {
return withQuarterTurns(rotationPolicy.next(quarterTurns));
}
public SimpleStructureStudioCell rotateCounterClockwise() {
return withQuarterTurns(rotationPolicy.previous(quarterTurns));
}
public SimpleStructureStudioCell withRotationPolicy(SimpleStructureStudioRotationPolicy newPolicy) {
Objects.requireNonNull(newPolicy, "newPolicy");
int newQuarterTurns = newPolicy.allows(quarterTurns) ? quarterTurns : 0;
return new SimpleStructureStudioCell(
x,
z,
topology,
newQuarterTurns,
newPolicy,
connectorChannel,
connectorHeight,
variants,
activeVariantIndex
);
}
public SimpleStructureStudioCell withConnector(String newChannel, int newHeight) {
return new SimpleStructureStudioCell(
x,
z,
topology,
quarterTurns,
rotationPolicy,
newChannel,
newHeight,
variants,
activeVariantIndex
);
}
public SimpleStructureStudioCell addVariant(SimpleStructureStudioVariant variant) {
Objects.requireNonNull(variant, "variant");
List<SimpleStructureStudioVariant> updatedVariants = new ArrayList<>(variants);
updatedVariants.add(variant);
int newActiveIndex = activeVariantIndex < 0 ? 0 : activeVariantIndex;
return new SimpleStructureStudioCell(
x,
z,
topology,
quarterTurns,
rotationPolicy,
connectorChannel,
connectorHeight,
updatedVariants,
newActiveIndex
);
}
public SimpleStructureStudioCell setVariantWeight(String variantId, int weight) {
int variantIndex = requireVariantIndex(variantId);
List<SimpleStructureStudioVariant> updatedVariants = new ArrayList<>(variants);
updatedVariants.set(variantIndex, updatedVariants.get(variantIndex).withWeight(weight));
return new SimpleStructureStudioCell(
x,
z,
topology,
quarterTurns,
rotationPolicy,
connectorChannel,
connectorHeight,
updatedVariants,
activeVariantIndex
);
}
public SimpleStructureStudioCell removeVariant(String variantId) {
int variantIndex = requireVariantIndex(variantId);
List<SimpleStructureStudioVariant> updatedVariants = new ArrayList<>(variants);
updatedVariants.remove(variantIndex);
int newActiveIndex = activeVariantIndex;
if (updatedVariants.isEmpty()) {
newActiveIndex = -1;
} else if (variantIndex < activeVariantIndex) {
newActiveIndex--;
} else if (newActiveIndex >= updatedVariants.size()) {
newActiveIndex = updatedVariants.size() - 1;
}
return new SimpleStructureStudioCell(
x,
z,
topology,
quarterTurns,
rotationPolicy,
connectorChannel,
connectorHeight,
updatedVariants,
newActiveIndex
);
}
public SimpleStructureStudioCell selectVariant(String variantId) {
int variantIndex = requireVariantIndex(variantId);
return new SimpleStructureStudioCell(
x,
z,
topology,
quarterTurns,
rotationPolicy,
connectorChannel,
connectorHeight,
variants,
variantIndex
);
}
public SimpleStructureStudioCell cycleVariant(int offset) {
if (variants.isEmpty()) {
return this;
}
int newActiveIndex = Math.floorMod((long) activeVariantIndex + offset, variants.size());
return new SimpleStructureStudioCell(
x,
z,
topology,
quarterTurns,
rotationPolicy,
connectorChannel,
connectorHeight,
variants,
newActiveIndex
);
}
private static void validateVariants(List<SimpleStructureStudioVariant> variants) {
Set<String> variantIds = new HashSet<>();
for (SimpleStructureStudioVariant variant : variants) {
Objects.requireNonNull(variant, "variant");
if (!variantIds.add(variant.id())) {
throw new IllegalArgumentException("Duplicate variant id: " + variant.id());
}
}
}
private int requireVariantIndex(String variantId) {
Objects.requireNonNull(variantId, "variantId");
for (int i = 0; i < variants.size(); i++) {
if (variants.get(i).id().equals(variantId)) {
return i;
}
}
throw new IllegalArgumentException("Unknown variant id: " + variantId);
}
}
@@ -1,401 +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.structure.studio;
import art.arcane.iris.core.structure.authoring.StructureBackend;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import art.arcane.iris.core.structure.authoring.StructureResourceBundle;
import art.arcane.iris.core.structure.authoring.StructureSource;
import art.arcane.iris.engine.framework.structure.StructureGraphCompilation;
import art.arcane.iris.engine.framework.structure.StructureGraphCompiler;
import art.arcane.iris.engine.framework.structure.StructureGraphDiagnostic;
import art.arcane.iris.engine.framework.structure.StructureGraphResolver;
import art.arcane.iris.engine.object.IrisDirection;
import art.arcane.iris.engine.object.IrisJigsawConnector;
import art.arcane.iris.engine.object.IrisJigsawPiece;
import art.arcane.iris.engine.object.IrisJigsawPieceEntry;
import art.arcane.iris.engine.object.IrisJigsawPool;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisStructure;
import art.arcane.iris.engine.object.JigsawJoint;
import art.arcane.volmlib.util.collection.KList;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
public final class SimpleStructureStudioCompiler {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private SimpleStructureStudioCompiler() {
}
public static StructureResourceBundle compile(
SimpleStructureStudioDraft draft,
SimpleStructureStudioPublishConfig config,
Map<SimpleStructureStudioVariantKey, IrisObject> resolvedVariants
) throws IOException {
CompilationInput input = new CompilationInput(draft, config, resolvedVariants);
return new CompilationState(input).compile();
}
private static final class CompilationState {
private final SimpleStructureStudioDraft draft;
private final SimpleStructureStudioPublishConfig config;
private final Map<SimpleStructureStudioVariantKey, IrisObject> resolvedVariants;
private final Map<String, IrisJigsawPool> pools;
private final Map<String, IrisJigsawPiece> pieces;
private final Map<String, IrisObject> objects;
private final List<PieceEntry> startEntries;
private final List<PieceEntry> mainEntries;
private final List<PieceEntry> terminalEntries;
private final String startPoolKey;
private final String mainPoolKey;
private final String terminalPoolKey;
private CompilationState(CompilationInput input) {
draft = input.draft();
config = input.config();
resolvedVariants = input.resolvedVariants();
pools = new LinkedHashMap<>();
pieces = new LinkedHashMap<>();
objects = new LinkedHashMap<>();
startEntries = new ArrayList<>();
mainEntries = new ArrayList<>();
terminalEntries = new ArrayList<>();
startPoolKey = config.resourceKey() + "/start";
mainPoolKey = config.resourceKey() + "/main";
terminalPoolKey = config.resourceKey() + "/terminal";
}
private StructureResourceBundle compile() throws IOException {
validateDraft();
validateResolvedVariants();
compilePieces();
compilePools();
IrisStructure structure = compileStructure();
validateGraph(structure);
return bundle(structure);
}
private void validateDraft() {
if (!draft.hasContent()) {
throw new IllegalStateException("A Studio structure must contain authored tiles");
}
TreeSet<String> startChannels = new TreeSet<>();
TreeSet<String> mainChannels = new TreeSet<>();
TreeSet<String> terminalChannels = new TreeSet<>();
for (SimpleStructureStudioCell cell : draft.cells()) {
if (cell.rotationPolicy() == SimpleStructureStudioRotationPolicy.HALF_TURNS) {
throw new IllegalStateException(
"HALF_TURNS cannot be represented by the Iris jigsaw rotatable contract at cell "
+ cell.x() + ", " + cell.z()
);
}
if (cell.variants().isEmpty()) {
throw new IllegalStateException(
"Studio cell " + cell.x() + ", " + cell.z() + " has no captured variants"
);
}
channelsFor(cell.topology(), startChannels, mainChannels, terminalChannels).add(
cell.connectorChannel()
);
}
requireChannels("START", startChannels);
requireChannels("main", mainChannels);
requireChannels("TERMINAL", terminalChannels);
if (!startChannels.equals(mainChannels) || !startChannels.equals(terminalChannels)) {
throw new IllegalStateException(
"START, main, and TERMINAL tiles must cover the same connector channels: start="
+ startChannels + ", main=" + mainChannels + ", terminal=" + terminalChannels
);
}
}
private Set<String> channelsFor(
SimpleStructureStudioTopology topology,
Set<String> startChannels,
Set<String> mainChannels,
Set<String> terminalChannels
) {
return switch (topology) {
case START -> startChannels;
case TERMINAL -> terminalChannels;
case EMPTY -> throw new IllegalStateException("Drafts cannot publish empty cells");
default -> mainChannels;
};
}
private void requireChannels(String category, Set<String> channels) {
if (channels.isEmpty()) {
throw new IllegalStateException("A Studio structure must contain at least one " + category + " tile");
}
}
private void validateResolvedVariants() {
LinkedHashSet<SimpleStructureStudioVariantKey> expected = new LinkedHashSet<>();
for (SimpleStructureStudioCell cell : draft.cells()) {
for (SimpleStructureStudioVariant variant : cell.variants()) {
expected.add(SimpleStructureStudioVariantKey.of(cell, variant));
}
}
for (Map.Entry<SimpleStructureStudioVariantKey, IrisObject> entry : resolvedVariants.entrySet()) {
if (entry.getKey() == null || entry.getValue() == null) {
throw new IllegalArgumentException("Resolved Studio variants cannot contain null keys or objects");
}
}
LinkedHashSet<SimpleStructureStudioVariantKey> actual = new LinkedHashSet<>(resolvedVariants.keySet());
if (!actual.equals(expected)) {
LinkedHashSet<SimpleStructureStudioVariantKey> missing = new LinkedHashSet<>(expected);
missing.removeAll(actual);
LinkedHashSet<SimpleStructureStudioVariantKey> unexpected = new LinkedHashSet<>(actual);
unexpected.removeAll(expected);
throw new IllegalStateException(
"Resolved Studio variants do not match the draft: missing=" + describe(missing)
+ ", unexpected=" + describe(unexpected)
);
}
}
private List<String> describe(Set<SimpleStructureStudioVariantKey> keys) {
TreeSet<String> descriptions = new TreeSet<>();
for (SimpleStructureStudioVariantKey key : keys) {
descriptions.add(key.cellX() + "," + key.cellZ() + ":" + key.variantId());
}
return List.copyOf(descriptions);
}
private void compilePieces() {
for (SimpleStructureStudioCell cell : draft.cells()) {
for (SimpleStructureStudioVariant variant : cell.variants()) {
SimpleStructureStudioVariantKey variantKey = SimpleStructureStudioVariantKey.of(cell, variant);
IrisObject object = resolvedVariants.get(variantKey);
validateObject(variantKey, object);
String resourceKey = variantResourceKey(cell, variant);
IrisJigsawPiece piece = new IrisJigsawPiece()
.setObject(resourceKey)
.setConnectors(connectors(cell, object))
.setRotatable(cell.rotationPolicy() == SimpleStructureStudioRotationPolicy.QUARTER_TURNS);
objects.put(resourceKey, object);
pieces.put(resourceKey, piece);
entriesFor(cell.topology()).add(new PieceEntry(resourceKey, variant.weight()));
}
}
}
private void validateObject(SimpleStructureStudioVariantKey key, IrisObject object) {
SimpleStructureStudioLayout layout = draft.layout();
if (object.getW() != layout.cellWidth()
|| object.getH() != layout.captureHeight()
|| object.getD() != layout.cellDepth()) {
throw new IllegalStateException(
"Resolved object " + key.cellX() + "," + key.cellZ() + ":" + key.variantId()
+ " has dimensions " + object.getW() + "x" + object.getH() + "x" + object.getD()
+ "; expected " + layout.cellWidth() + "x" + layout.captureHeight() + "x"
+ layout.cellDepth()
);
}
}
private String variantResourceKey(
SimpleStructureStudioCell cell,
SimpleStructureStudioVariant variant
) {
return config.resourceKey() + "/cells/" + cell.x() + "-" + cell.z() + "/" + variant.id();
}
private KList<IrisJigsawConnector> connectors(SimpleStructureStudioCell cell, IrisObject object) {
KList<IrisJigsawConnector> connectors = new KList<>();
for (SimpleStructureStudioDirection direction : SimpleStructureStudioDirection.values()) {
if (!cell.connects(direction)) {
continue;
}
connectors.add(new IrisJigsawConnector()
.setPosition(connectorPosition(direction, cell.connectorHeight(), object))
.setDirection(irisDirection(direction))
.setPool(mainPoolKey)
.setName(cell.connectorChannel())
.setTargetName(cell.connectorChannel())
.setJoint(JigsawJoint.ALIGNED));
}
return connectors;
}
private IrisPosition connectorPosition(
SimpleStructureStudioDirection direction,
int height,
IrisObject object
) {
return switch (direction) {
case NORTH -> new IrisPosition(object.getW() / 2, height, 0);
case EAST -> new IrisPosition(object.getW() - 1, height, object.getD() / 2);
case SOUTH -> new IrisPosition(object.getW() / 2, height, object.getD() - 1);
case WEST -> new IrisPosition(0, height, object.getD() / 2);
};
}
private IrisDirection irisDirection(SimpleStructureStudioDirection direction) {
return switch (direction) {
case NORTH -> IrisDirection.NORTH_NEGATIVE_Z;
case EAST -> IrisDirection.EAST_POSITIVE_X;
case SOUTH -> IrisDirection.SOUTH_POSITIVE_Z;
case WEST -> IrisDirection.WEST_NEGATIVE_X;
};
}
private List<PieceEntry> entriesFor(SimpleStructureStudioTopology topology) {
return switch (topology) {
case START -> startEntries;
case TERMINAL -> terminalEntries;
case EMPTY -> throw new IllegalStateException("Drafts cannot publish empty cells");
default -> mainEntries;
};
}
private void compilePools() {
pools.put(startPoolKey, pool(startEntries, ""));
pools.put(mainPoolKey, pool(mainEntries, terminalPoolKey));
pools.put(terminalPoolKey, pool(terminalEntries, ""));
}
private IrisJigsawPool pool(List<PieceEntry> entries, String fallback) {
KList<IrisJigsawPieceEntry> weightedPieces = new KList<>();
for (PieceEntry entry : entries) {
weightedPieces.add(new IrisJigsawPieceEntry(entry.pieceKey(), entry.weight()));
}
return new IrisJigsawPool().setPieces(weightedPieces).setFallback(fallback);
}
private IrisStructure compileStructure() {
IrisStructure structure = new IrisStructure()
.setStartPool(startPoolKey)
.setMaxDepth(config.maxDepth())
.setMaxSizeChunks(config.maxSizeChunks())
.setPlaceMode(config.placeMode());
structure.setLoadKey(config.resourceKey());
return structure;
}
private void validateGraph(IrisStructure structure) {
StructureGraphCompilation compilation = StructureGraphCompiler.compile(
structure,
new BundleGraphResolver(pools, pieces, objects)
);
if (compilation.isAssemblyViable() && compilation.getDiagnostics().isEmpty()) {
return;
}
StringBuilder failure = new StringBuilder("Studio structure graph is not safely assemblable");
for (StructureGraphDiagnostic diagnostic : compilation.getDiagnostics()) {
failure.append("; ").append(diagnostic.code()).append(": ").append(diagnostic.message());
}
if (!compilation.isAssemblyViable() && compilation.getDiagnostics().isEmpty()) {
failure.append("; deterministic assembly samples did not complete");
}
throw new IllegalStateException(failure.toString());
}
private StructureResourceBundle bundle(IrisStructure structure) throws IOException {
StructureResourceBundle.Builder bundle = StructureResourceBundle.builder(config.structureKey())
.source(StructureSource.of(StructureSource.Kind.IRIS, config.structureKey()))
.backend(StructureBackend.IRIS_ASSEMBLY)
.capability(StructureCapability.BLOCKS)
.capability(StructureCapability.BLOCK_ENTITIES)
.capability(StructureCapability.CONNECTORS)
.capability(StructureCapability.IRIS_PLACEMENT)
.textResource("structures/" + config.resourceKey() + ".json", GSON.toJson(structure));
for (Map.Entry<String, IrisObject> entry : objects.entrySet()) {
bundle.resource("objects/" + entry.getKey() + ".iob", serialize(entry.getValue()));
}
for (Map.Entry<String, IrisJigsawPiece> entry : pieces.entrySet()) {
bundle.textResource("jigsaw-pieces/" + entry.getKey() + ".json", GSON.toJson(entry.getValue()));
}
for (Map.Entry<String, IrisJigsawPool> entry : pools.entrySet()) {
bundle.textResource("jigsaw-pools/" + entry.getKey() + ".json", GSON.toJson(entry.getValue()));
}
return bundle.build();
}
private byte[] serialize(IrisObject object) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
object.write(output);
return output.toByteArray();
}
}
private record CompilationInput(
SimpleStructureStudioDraft draft,
SimpleStructureStudioPublishConfig config,
Map<SimpleStructureStudioVariantKey, IrisObject> resolvedVariants
) {
private CompilationInput {
Objects.requireNonNull(draft, "draft");
Objects.requireNonNull(config, "config");
Objects.requireNonNull(resolvedVariants, "resolvedVariants");
}
}
private record PieceEntry(String pieceKey, int weight) {
}
private static final class BundleGraphResolver implements StructureGraphResolver {
private final Map<String, IrisJigsawPool> pools;
private final Map<String, IrisJigsawPiece> pieces;
private final Map<String, IrisObject> objects;
private BundleGraphResolver(
Map<String, IrisJigsawPool> pools,
Map<String, IrisJigsawPiece> pieces,
Map<String, IrisObject> objects
) {
this.pools = pools;
this.pieces = pieces;
this.objects = objects;
}
@Override
public IrisJigsawPool loadPool(String key) {
return pools.get(key);
}
@Override
public IrisJigsawPiece loadPiece(String key) {
return pieces.get(key);
}
@Override
public IrisObject loadObject(String key) {
return objects.get(key);
}
}
}
@@ -1,74 +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.structure.studio;
public enum SimpleStructureStudioDirection {
NORTH(1, 0, -1),
EAST(2, 1, 0),
SOUTH(4, 0, 1),
WEST(8, -1, 0);
public static final int ALL_MASK = 15;
private static final SimpleStructureStudioDirection[] ORDERED = values();
private final int mask;
private final int offsetX;
private final int offsetZ;
SimpleStructureStudioDirection(int mask, int offsetX, int offsetZ) {
this.mask = mask;
this.offsetX = offsetX;
this.offsetZ = offsetZ;
}
public int mask() {
return mask;
}
public int offsetX() {
return offsetX;
}
public int offsetZ() {
return offsetZ;
}
public SimpleStructureStudioDirection rotateClockwise(int quarterTurns) {
int rotatedIndex = Math.floorMod((long) ordinal() + quarterTurns, ORDERED.length);
return ORDERED[rotatedIndex];
}
public static int rotateMask(int connectorMask, int quarterTurns) {
if ((connectorMask & ~ALL_MASK) != 0) {
throw new IllegalArgumentException("Connector mask uses unsupported direction bits: " + connectorMask);
}
int normalizedTurns = Math.floorMod(quarterTurns, ORDERED.length);
if (normalizedTurns == 0 || connectorMask == 0) {
return connectorMask;
}
int rotatedMask = 0;
for (SimpleStructureStudioDirection direction : ORDERED) {
if ((connectorMask & direction.mask) != 0) {
rotatedMask |= direction.rotateClockwise(normalizedTurns).mask;
}
}
return rotatedMask;
}
}
@@ -1,140 +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.structure.studio;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
public record SimpleStructureStudioDraft(
SimpleStructureStudioLayout layout,
long previewSeed,
List<SimpleStructureStudioCell> cells
) {
private static final Comparator<SimpleStructureStudioCell> CELL_ORDER = Comparator
.comparingInt(SimpleStructureStudioCell::z)
.thenComparingInt(SimpleStructureStudioCell::x);
public SimpleStructureStudioDraft {
Objects.requireNonNull(layout, "layout");
Objects.requireNonNull(cells, "cells");
List<SimpleStructureStudioCell> orderedCells = new ArrayList<>(cells);
orderedCells.sort(CELL_ORDER);
validateCells(layout, orderedCells);
cells = List.copyOf(orderedCells);
}
public static SimpleStructureStudioDraft empty(SimpleStructureStudioLayout layout, long previewSeed) {
return new SimpleStructureStudioDraft(layout, previewSeed, List.of());
}
public boolean hasContent() {
return !cells.isEmpty();
}
public Optional<SimpleStructureStudioCell> cellAt(int x, int z) {
requirePosition(x, z);
for (SimpleStructureStudioCell cell : cells) {
if (cell.x() == x && cell.z() == z) {
return Optional.of(cell);
}
}
return Optional.empty();
}
public SimpleStructureStudioCell cellOrEmpty(int x, int z) {
return cellAt(x, z).orElseGet(() -> SimpleStructureStudioCell.empty(x, z));
}
public SimpleStructureStudioDraft withLayout(SimpleStructureStudioLayout newLayout) {
Objects.requireNonNull(newLayout, "newLayout");
if (hasContent() && !layout.equals(newLayout)) {
throw new IllegalStateException("The studio layout cannot be resized after content has been added");
}
return new SimpleStructureStudioDraft(newLayout, previewSeed, cells);
}
public SimpleStructureStudioDraft withPreviewSeed(long newPreviewSeed) {
return new SimpleStructureStudioDraft(layout, newPreviewSeed, cells);
}
public SimpleStructureStudioDraft withCell(SimpleStructureStudioCell updatedCell) {
Objects.requireNonNull(updatedCell, "updatedCell");
requirePosition(updatedCell.x(), updatedCell.z());
List<SimpleStructureStudioCell> updatedCells = new ArrayList<>(cells.size() + 1);
for (SimpleStructureStudioCell cell : cells) {
if (cell.x() != updatedCell.x() || cell.z() != updatedCell.z()) {
updatedCells.add(cell);
}
}
if (!updatedCell.isEmpty()) {
updatedCells.add(updatedCell);
}
return new SimpleStructureStudioDraft(layout, previewSeed, updatedCells);
}
public SimpleStructureStudioDraft withoutCell(int x, int z) {
requirePosition(x, z);
List<SimpleStructureStudioCell> updatedCells = new ArrayList<>(cells.size());
for (SimpleStructureStudioCell cell : cells) {
if (cell.x() != x || cell.z() != z) {
updatedCells.add(cell);
}
}
return new SimpleStructureStudioDraft(layout, previewSeed, updatedCells);
}
private static void validateCells(
SimpleStructureStudioLayout layout,
List<SimpleStructureStudioCell> cells
) {
Set<Long> positions = new HashSet<>();
for (SimpleStructureStudioCell cell : cells) {
Objects.requireNonNull(cell, "cell");
if (cell.isEmpty()) {
throw new IllegalArgumentException("Drafts store only populated cells");
}
if (!layout.contains(cell.x(), cell.z())) {
throw new IllegalArgumentException(
"Cell is outside the studio grid: " + cell.x() + ", " + cell.z()
);
}
if (cell.connectorHeight() >= layout.captureHeight()) {
throw new IllegalArgumentException(
"Connector height " + cell.connectorHeight()
+ " is outside capture height " + layout.captureHeight()
);
}
long position = ((long) cell.x() << 32) ^ (cell.z() & 0xffffffffL);
if (!positions.add(position)) {
throw new IllegalArgumentException("Duplicate studio cell: " + cell.x() + ", " + cell.z());
}
}
}
private void requirePosition(int x, int z) {
if (!layout.contains(x, z)) {
throw new IndexOutOfBoundsException("Cell is outside the studio grid: " + x + ", " + z);
}
}
}
@@ -1,68 +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.structure.studio;
public record SimpleStructureStudioLayout(
int gridWidth,
int gridDepth,
int cellWidth,
int cellDepth,
int captureHeight
) {
public SimpleStructureStudioLayout {
requirePositive("gridWidth", gridWidth);
requirePositive("gridDepth", gridDepth);
requirePositive("cellWidth", cellWidth);
requirePositive("cellDepth", cellDepth);
requirePositive("captureHeight", captureHeight);
multiply("grid cell count", gridWidth, gridDepth);
multiply("studio width", gridWidth, cellWidth);
multiply("studio depth", gridDepth, cellDepth);
}
public int cellCount() {
return gridWidth * gridDepth;
}
public int studioWidth() {
return gridWidth * cellWidth;
}
public int studioDepth() {
return gridDepth * cellDepth;
}
public boolean contains(int x, int z) {
return x >= 0 && x < gridWidth && z >= 0 && z < gridDepth;
}
private static void requirePositive(String name, int value) {
if (value <= 0) {
throw new IllegalArgumentException(name + " must be greater than zero: " + value);
}
}
private static void multiply(String name, int first, int second) {
try {
Math.multiplyExact(first, second);
} catch (ArithmeticException e) {
throw new IllegalArgumentException(name + " exceeds the supported integer range", e);
}
}
}
@@ -1,66 +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.structure.studio;
import art.arcane.iris.core.structure.authoring.StructureKey;
import art.arcane.iris.core.structure.authoring.StructureResourceBundle;
import art.arcane.iris.engine.object.ObjectPlaceMode;
import java.util.Objects;
public record SimpleStructureStudioPublishConfig(
StructureKey structureKey,
String resourceKey,
int maxDepth,
int maxSizeChunks,
ObjectPlaceMode placeMode
) {
public SimpleStructureStudioPublishConfig {
Objects.requireNonNull(structureKey, "structureKey");
Objects.requireNonNull(resourceKey, "resourceKey");
Objects.requireNonNull(placeMode, "placeMode");
if (resourceKey.isBlank()) {
throw new IllegalArgumentException("resourceKey cannot be blank");
}
StructureResourceBundle.validateRelativePath("structures/" + resourceKey + ".json");
if (!structureKey.path().equals(resourceKey)) {
throw new IllegalArgumentException("structureKey path must match resourceKey: "
+ structureKey.path() + " != " + resourceKey);
}
if (maxDepth < 1 || maxDepth > 30) {
throw new IllegalArgumentException("maxDepth must be between 1 and 30: " + maxDepth);
}
if (maxSizeChunks < 1 || maxSizeChunks > 32) {
throw new IllegalArgumentException("maxSizeChunks must be between 1 and 32: " + maxSizeChunks);
}
}
public static SimpleStructureStudioPublishConfig defaults(
StructureKey structureKey,
String resourceKey
) {
return new SimpleStructureStudioPublishConfig(
structureKey,
resourceKey,
7,
8,
ObjectPlaceMode.STRUCTURE_PIECE
);
}
}
@@ -1,113 +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.structure.studio;
import art.arcane.iris.core.structure.authoring.StructureKey;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.ReentrantLock;
public final class SimpleStructureStudioRepository {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final ConcurrentMap<Path, ReentrantLock> ROOT_LOCKS = new ConcurrentHashMap<>();
private final Path packRoot;
private final Path draftsRoot;
private final ReentrantLock rootLock;
public SimpleStructureStudioRepository(Path packRoot) {
this.packRoot = Objects.requireNonNull(packRoot, "packRoot").toAbsolutePath().normalize();
draftsRoot = this.packRoot.resolve(".iris/structure-studio").normalize();
rootLock = ROOT_LOCKS.computeIfAbsent(this.packRoot, ignored -> new ReentrantLock());
}
public Path packRoot() {
return packRoot;
}
public Path draftPath(StructureKey key) {
StructureKey activeKey = Objects.requireNonNull(key, "key");
Path path = draftsRoot.resolve(activeKey.namespace()).resolve(activeKey.path() + ".json").normalize();
if (!path.startsWith(draftsRoot)) {
throw new IllegalArgumentException("Studio draft key escapes the pack: " + key);
}
return path;
}
public Optional<SimpleStructureStudioDraft> load(StructureKey key) throws IOException {
Path target = draftPath(key);
rootLock.lock();
try {
if (!Files.isRegularFile(target)) {
return Optional.empty();
}
try {
SimpleStructureStudioDraft draft = GSON.fromJson(
Files.readString(target, StandardCharsets.UTF_8), SimpleStructureStudioDraft.class);
if (draft == null) {
throw new IOException("Studio draft is empty: " + target);
}
return Optional.of(draft);
} catch (RuntimeException e) {
throw new IOException("Invalid Studio draft " + target + ": " + e.getMessage(), e);
}
} finally {
rootLock.unlock();
}
}
public void save(StructureKey key, SimpleStructureStudioDraft draft) throws IOException {
Path target = draftPath(key);
byte[] content = GSON.toJson(Objects.requireNonNull(draft, "draft")).getBytes(StandardCharsets.UTF_8);
rootLock.lock();
Path staged = target.resolveSibling(target.getFileName() + "." + UUID.randomUUID() + ".tmp");
try {
Files.createDirectories(target.getParent());
Files.write(staged, content, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
moveReplace(staged, target);
} finally {
try {
Files.deleteIfExists(staged);
} finally {
rootLock.unlock();
}
}
}
private void moveReplace(Path source, Path target) throws IOException {
try {
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
} catch (AtomicMoveNotSupportedException e) {
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
}
}
}
@@ -1,52 +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.structure.studio;
public enum SimpleStructureStudioRotationPolicy {
FIXED,
HALF_TURNS,
QUARTER_TURNS;
public boolean allows(int quarterTurns) {
int normalizedTurns = Math.floorMod(quarterTurns, 4);
return switch (this) {
case FIXED -> normalizedTurns == 0;
case HALF_TURNS -> normalizedTurns == 0 || normalizedTurns == 2;
case QUARTER_TURNS -> true;
};
}
public int next(int quarterTurns) {
int normalizedTurns = Math.floorMod(quarterTurns, 4);
return switch (this) {
case FIXED -> 0;
case HALF_TURNS -> Math.floorMod(normalizedTurns + 2, 4);
case QUARTER_TURNS -> Math.floorMod(normalizedTurns + 1, 4);
};
}
public int previous(int quarterTurns) {
int normalizedTurns = Math.floorMod(quarterTurns, 4);
return switch (this) {
case FIXED -> 0;
case HALF_TURNS -> Math.floorMod(normalizedTurns - 2, 4);
case QUARTER_TURNS -> Math.floorMod(normalizedTurns - 1, 4);
};
}
}
@@ -1,233 +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.structure.studio;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Objects;
import java.util.function.UnaryOperator;
public final class SimpleStructureStudioSession {
public static final int DEFAULT_HISTORY_LIMIT = 64;
public static final int MAX_HISTORY_LIMIT = 256;
private static final long PREVIEW_SEED_STEP = 0x9E3779B97F4A7C15L;
private final int historyLimit;
private final Deque<SimpleStructureStudioDraft> undoHistory;
private final Deque<SimpleStructureStudioDraft> redoHistory;
private SimpleStructureStudioDraft draft;
private SimpleStructureStudioDraft savedDraft;
private SimpleStructureStudioSession(SimpleStructureStudioDraft draft, int historyLimit) {
this.draft = Objects.requireNonNull(draft, "draft");
if (historyLimit <= 0 || historyLimit > MAX_HISTORY_LIMIT) {
throw new IllegalArgumentException(
"History limit must be between 1 and " + MAX_HISTORY_LIMIT + ": " + historyLimit
);
}
this.historyLimit = historyLimit;
undoHistory = new ArrayDeque<>(historyLimit);
redoHistory = new ArrayDeque<>(historyLimit);
savedDraft = draft;
}
public static SimpleStructureStudioSession open(SimpleStructureStudioDraft draft, int historyLimit) {
return new SimpleStructureStudioSession(draft, historyLimit);
}
public static SimpleStructureStudioSession createNew(SimpleStructureStudioDraft draft, int historyLimit) {
SimpleStructureStudioSession session = new SimpleStructureStudioSession(draft, historyLimit);
session.savedDraft = null;
return session;
}
public synchronized SimpleStructureStudioDraft draft() {
return draft;
}
public int historyLimit() {
return historyLimit;
}
public synchronized boolean isDirty() {
return savedDraft == null || !draft.equals(savedDraft);
}
public synchronized boolean canUndo() {
return !undoHistory.isEmpty();
}
public synchronized boolean canRedo() {
return !redoHistory.isEmpty();
}
public synchronized int undoDepth() {
return undoHistory.size();
}
public synchronized int redoDepth() {
return redoHistory.size();
}
public synchronized void markSaved() {
savedDraft = draft;
}
public synchronized boolean resize(SimpleStructureStudioLayout newLayout) {
Objects.requireNonNull(newLayout, "newLayout");
if (draft.layout().equals(newLayout)) {
return false;
}
if (draft.hasContent()) {
throw new IllegalStateException("The studio layout cannot be resized after content has been added");
}
return applyDraft(draft.withLayout(newLayout));
}
public synchronized boolean replaceCell(SimpleStructureStudioCell cell) {
return applyDraft(draft.withCell(Objects.requireNonNull(cell, "cell")));
}
public synchronized boolean clearCell(int x, int z) {
return applyDraft(draft.withoutCell(x, z));
}
public synchronized boolean setTopology(int x, int z, SimpleStructureStudioTopology topology) {
Objects.requireNonNull(topology, "topology");
if (topology == SimpleStructureStudioTopology.EMPTY) {
return clearCell(x, z);
}
SimpleStructureStudioCell cell = draft.cellOrEmpty(x, z).withTopology(topology);
return applyDraft(draft.withCell(cell));
}
public synchronized boolean setQuarterTurns(int x, int z, int quarterTurns) {
return updatePopulatedCell(x, z, cell -> cell.withQuarterTurns(quarterTurns));
}
public synchronized boolean rotateClockwise(int x, int z) {
return updatePopulatedCell(x, z, SimpleStructureStudioCell::rotateClockwise);
}
public synchronized boolean rotateCounterClockwise(int x, int z) {
return updatePopulatedCell(x, z, SimpleStructureStudioCell::rotateCounterClockwise);
}
public synchronized boolean setRotationPolicy(
int x,
int z,
SimpleStructureStudioRotationPolicy rotationPolicy
) {
Objects.requireNonNull(rotationPolicy, "rotationPolicy");
return updatePopulatedCell(x, z, cell -> cell.withRotationPolicy(rotationPolicy));
}
public synchronized boolean setConnector(int x, int z, String channel, int height) {
return updatePopulatedCell(x, z, cell -> cell.withConnector(channel, height));
}
public synchronized boolean addVariant(int x, int z, SimpleStructureStudioVariant variant) {
Objects.requireNonNull(variant, "variant");
return updatePopulatedCell(x, z, cell -> cell.addVariant(variant));
}
public synchronized boolean setVariantWeight(int x, int z, String variantId, int weight) {
return updatePopulatedCell(x, z, cell -> cell.setVariantWeight(variantId, weight));
}
public synchronized boolean removeVariant(int x, int z, String variantId) {
return updatePopulatedCell(x, z, cell -> cell.removeVariant(variantId));
}
public synchronized boolean selectVariant(int x, int z, String variantId) {
return updatePopulatedCell(x, z, cell -> cell.selectVariant(variantId));
}
public synchronized boolean cycleVariant(int x, int z, int offset) {
return updatePopulatedCell(x, z, cell -> cell.cycleVariant(offset));
}
public synchronized boolean setPreviewSeed(long previewSeed) {
return applyDraft(draft.withPreviewSeed(previewSeed));
}
public synchronized long advancePreviewSeed() {
long nextSeed = nextPreviewSeed(draft.previewSeed());
applyDraft(draft.withPreviewSeed(nextSeed));
return nextSeed;
}
public synchronized boolean undo() {
if (undoHistory.isEmpty()) {
return false;
}
redoHistory.addLast(draft);
trimHistory(redoHistory);
draft = undoHistory.removeLast();
return true;
}
public synchronized boolean redo() {
if (redoHistory.isEmpty()) {
return false;
}
undoHistory.addLast(draft);
trimHistory(undoHistory);
draft = redoHistory.removeLast();
return true;
}
public static long nextPreviewSeed(long previewSeed) {
return previewSeed + PREVIEW_SEED_STEP;
}
private boolean updatePopulatedCell(
int x,
int z,
UnaryOperator<SimpleStructureStudioCell> update
) {
SimpleStructureStudioCell cell = draft.cellAt(x, z).orElseThrow(
() -> new IllegalStateException("Studio cell is empty: " + x + ", " + z)
);
SimpleStructureStudioCell updatedCell = Objects.requireNonNull(update.apply(cell), "updatedCell");
if (updatedCell.x() != x || updatedCell.z() != z) {
throw new IllegalArgumentException("Cell updates cannot change the cell position");
}
return applyDraft(draft.withCell(updatedCell));
}
private boolean applyDraft(SimpleStructureStudioDraft updatedDraft) {
Objects.requireNonNull(updatedDraft, "updatedDraft");
if (draft.equals(updatedDraft)) {
return false;
}
undoHistory.addLast(draft);
trimHistory(undoHistory);
draft = updatedDraft;
redoHistory.clear();
return true;
}
private void trimHistory(Deque<SimpleStructureStudioDraft> history) {
while (history.size() > historyLimit) {
history.removeFirst();
}
}
}
@@ -1,54 +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.structure.studio;
public enum SimpleStructureStudioTopology {
EMPTY(0),
END(SimpleStructureStudioDirection.NORTH.mask()),
STRAIGHT(SimpleStructureStudioDirection.NORTH.mask() | SimpleStructureStudioDirection.SOUTH.mask()),
CORNER(SimpleStructureStudioDirection.NORTH.mask() | SimpleStructureStudioDirection.EAST.mask()),
T(SimpleStructureStudioDirection.NORTH.mask()
| SimpleStructureStudioDirection.EAST.mask()
| SimpleStructureStudioDirection.WEST.mask()),
CROSS(SimpleStructureStudioDirection.ALL_MASK),
START(SimpleStructureStudioDirection.NORTH.mask()),
TERMINAL(SimpleStructureStudioDirection.NORTH.mask());
private final int baseConnectorMask;
SimpleStructureStudioTopology(int baseConnectorMask) {
this.baseConnectorMask = baseConnectorMask;
}
public int baseConnectorMask() {
return baseConnectorMask;
}
public int connectorMask(int quarterTurns) {
return SimpleStructureStudioDirection.rotateMask(baseConnectorMask, quarterTurns);
}
public int connectorCount() {
return Integer.bitCount(baseConnectorMask);
}
public boolean connects(SimpleStructureStudioDirection direction, int quarterTurns) {
return (connectorMask(quarterTurns) & direction.mask()) != 0;
}
}
@@ -1,43 +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.structure.studio;
import art.arcane.iris.core.structure.authoring.StructureResourceBundle;
import java.util.Objects;
import java.util.regex.Pattern;
public record SimpleStructureStudioVariant(String id, int weight) {
private static final Pattern ID_PATTERN = Pattern.compile("[a-z0-9._-]+(?:/[a-z0-9._-]+)*");
public SimpleStructureStudioVariant {
Objects.requireNonNull(id, "id");
if (!ID_PATTERN.matcher(id).matches()) {
throw new IllegalArgumentException("Variant id must be a portable lowercase resource path: " + id);
}
StructureResourceBundle.validateRelativePath(id);
if (weight <= 0) {
throw new IllegalArgumentException("Variant weight must be greater than zero: " + weight);
}
}
public SimpleStructureStudioVariant withWeight(int newWeight) {
return new SimpleStructureStudioVariant(id, newWeight);
}
}
@@ -1,42 +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.structure.studio;
import java.util.Objects;
public record SimpleStructureStudioVariantKey(int cellX, int cellZ, String variantId) {
public SimpleStructureStudioVariantKey {
if (cellX < 0 || cellZ < 0) {
throw new IllegalArgumentException("Variant cell coordinates cannot be negative: " + cellX + ", " + cellZ);
}
Objects.requireNonNull(variantId, "variantId");
if (variantId.isBlank()) {
throw new IllegalArgumentException("Variant id cannot be blank");
}
}
public static SimpleStructureStudioVariantKey of(
SimpleStructureStudioCell cell,
SimpleStructureStudioVariant variant
) {
Objects.requireNonNull(cell, "cell");
Objects.requireNonNull(variant, "variant");
return new SimpleStructureStudioVariantKey(cell.x(), cell.z(), variant.id());
}
}
@@ -162,13 +162,14 @@ public class IrisPackBenchmarking {
}
private double calculateMedian(KList<Integer> list) {
Collections.sort(list);
int middle = list.size() / 2;
KList<Integer> sorted = new KList<>(list);
Collections.sort(sorted);
int middle = sorted.size() / 2;
if (list.size() % 2 == 1) {
return list.get(middle);
if (sorted.size() % 2 == 1) {
return sorted.get(middle);
} else {
return (list.get(middle - 1) + list.get(middle)) / 2.0;
return (sorted.get(middle - 1) + sorted.get(middle)) / 2.0;
}
}
@@ -0,0 +1,158 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine;
import art.arcane.iris.util.common.scheduling.J;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import static art.arcane.iris.engine.EngineShutdownSequence.appendFailure;
import static art.arcane.iris.engine.EngineShutdownSequence.propagate;
/**
* Admission gate and drain tracker for the asynchronous work an {@link IrisEngine} schedules.
* Admission is closed before any lifecycle transition so a draining engine can never accrue new
* background work, and every tracked task is awaited (or cancelled) before resources are released.
*/
final class EngineBackgroundTasks {
private static final long BACKGROUND_TASK_TIMEOUT_MILLIS = 15000L;
private final Object backgroundTaskLock = new Object();
private final List<TrackedBackgroundTask> backgroundTasks = new ArrayList<>();
private boolean backgroundTaskAdmission;
boolean scheduleTrackedTask(Runnable task) {
synchronized (backgroundTaskLock) {
backgroundTasks.removeIf(tracked -> tracked.completion.isDone()
&& !tracked.completion.isCompletedExceptionally());
if (!backgroundTaskAdmission) {
return false;
}
TrackedBackgroundTask tracked = new TrackedBackgroundTask();
Future<Void> future = J.a(() -> {
tracked.started.set(true);
try {
task.run();
tracked.completion.complete(null);
return null;
} catch (Throwable exception) {
tracked.completion.completeExceptionally(exception);
throw propagate(exception);
}
});
if (future == null) {
throw new IllegalStateException("Iris background task scheduler returned no task handle.");
}
tracked.future = future;
backgroundTasks.add(tracked);
return true;
}
}
BackgroundTaskDrain drainBackgroundTasks(String reason) {
List<TrackedBackgroundTask> tasks;
synchronized (backgroundTaskLock) {
tasks = List.copyOf(backgroundTasks);
}
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(BACKGROUND_TASK_TIMEOUT_MILLIS);
Throwable failure = null;
for (TrackedBackgroundTask task : tasks) {
long remaining = deadline - System.nanoTime();
if (remaining <= 0L) {
cancelBackgroundTask(task, reason);
failure = appendFailure(failure, new TimeoutException("Timed out waiting for Iris background tasks during " + reason + "."));
continue;
}
try {
task.completion.get(remaining, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
cancelBackgroundTask(task, reason);
failure = appendFailure(failure, e);
} catch (ExecutionException | TimeoutException e) {
cancelBackgroundTask(task, reason);
failure = appendFailure(failure, e);
}
}
boolean complete;
synchronized (backgroundTaskLock) {
backgroundTasks.removeIf(tracked -> tracked.completion.isDone());
complete = backgroundTasks.isEmpty();
}
return new BackgroundTaskDrain(failure, complete);
}
private void cancelBackgroundTask(TrackedBackgroundTask task, String reason) {
Future<?> future = task.future;
if (future != null && future.cancel(true) && !task.started.get()) {
task.completion.completeExceptionally(
new IllegalStateException("Iris background task was cancelled before starting during " + reason + "."));
}
}
void openBackgroundTaskAdmission() {
synchronized (backgroundTaskLock) {
backgroundTaskAdmission = true;
}
}
void closeBackgroundTaskAdmission() {
synchronized (backgroundTaskLock) {
backgroundTaskAdmission = false;
}
}
void cancelBackgroundTasks(String reason) {
List<TrackedBackgroundTask> tasks;
synchronized (backgroundTaskLock) {
tasks = List.copyOf(backgroundTasks);
}
for (TrackedBackgroundTask task : tasks) {
cancelBackgroundTask(task, reason);
}
}
private static final class TrackedBackgroundTask {
private final AtomicBoolean started = new AtomicBoolean();
private final CompletableFuture<Void> completion = new CompletableFuture<>();
private volatile Future<?> future;
}
record BackgroundTaskDrain(Throwable failure, boolean complete) {
boolean allowsResourceRelease() {
return complete;
}
void requireComplete(String reason) {
if (failure != null) {
throw new IllegalStateException("Iris background tasks failed to drain during " + reason + ".", failure);
}
if (!complete) {
throw new IllegalStateException("Iris background tasks remain active during " + reason + ".");
}
}
}
}
@@ -0,0 +1,138 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.IrisEngineData;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.volmlib.util.io.IO;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
/**
* On-disk persistence for a single engine's {@link IrisEngineData}. Loads are double-checked against
* the engine's volatile field and serialized on a dedicated lock, and every write goes through an
* atomic temp-file move so a crash mid-save can never truncate the live engine data.
*/
final class EngineDataStore {
private final IrisEngine engine;
private final Object engineDataLock = new Object();
EngineDataStore(IrisEngine engine) {
this.engine = engine;
}
IrisEngineData getEngineData() {
IrisEngineData loaded = engine.engineData;
if (loaded != null) {
return loaded;
}
synchronized (engineDataLock) {
loaded = engine.engineData;
if (loaded != null) {
return loaded;
}
File f = new File(engine.getWorld().worldFolder(), "iris/engine-data/" + engine.getDimension().getLoadKey() + ".json");
if (f.exists()) {
try {
loaded = new Gson().fromJson(IO.readAll(f), IrisEngineData.class);
if (loaded == null) {
throw new IllegalStateException("Engine data file contains no JSON object: " + f.getAbsolutePath());
}
} catch (IOException | JsonParseException e) {
IrisLogging.reportError(e);
e.printStackTrace();
throw new IllegalStateException("Failed to read Iris engine data without modifying it: " + f.getAbsolutePath(), e);
}
}
if (loaded == null) {
loaded = new IrisEngineData();
loaded.getStatistics().setVersion(IrisPlatforms.get().irisVersionNumber());
loaded.getStatistics().setMCVersion(IrisPlatforms.get().minecraftVersionNumber());
loaded.getStatistics().setUpgradedVersion(IrisPlatforms.get().irisVersionNumber());
if (loaded.getStatistics().getVersion() == -1 || loaded.getStatistics().getMCVersion() == -1) {
IrisLogging.error("Failed to setup Engine Data!");
}
try {
writeEngineDataAtomically(f, loaded);
} catch (IOException e) {
IrisLogging.reportError(e);
e.printStackTrace();
throw new IllegalStateException("Failed to create Iris engine data: " + f.getAbsolutePath(), e);
}
}
engine.engineData = loaded;
return loaded;
}
}
void saveEngineData() {
synchronized (engineDataLock) {
File f = new File(engine.getWorld().worldFolder(), "iris/engine-data/" + engine.getDimension().getLoadKey() + ".json");
try {
writeEngineDataAtomically(f, engine.getEngineData());
IrisLogging.debug("Saved Engine Data");
} catch (IOException e) {
IrisLogging.error("Failed to save Engine Data");
IrisLogging.reportError(e);
e.printStackTrace();
throw new IllegalStateException("Failed to save Iris engine data: " + f.getAbsolutePath(), e);
}
}
}
void releaseEngineData() {
IrisData data = engine.getData();
data.unregisterEngine(engine);
if (data.getEngines().isEmpty()) {
data.close();
data.clearLists();
}
}
static void writeEngineDataAtomically(File file, IrisEngineData data) throws IOException {
Path output = file.toPath();
Path parent = output.getParent();
if (parent == null) {
throw new IOException("Engine data path has no parent: " + output);
}
Files.createDirectories(parent);
Path temporary = Files.createTempFile(parent, output.getFileName().toString(), ".tmp");
try {
Files.writeString(temporary, new Gson().toJson(data), StandardCharsets.UTF_8);
try {
Files.move(temporary, output, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
} catch (AtomicMoveNotSupportedException e) {
Files.move(temporary, output, StandardCopyOption.REPLACE_EXISTING);
}
} finally {
Files.deleteIfExists(temporary);
}
}
}
@@ -0,0 +1,194 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.localization.ClientUiMessages;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.protocol.IrisProtocolServer;
import art.arcane.iris.engine.EngineRuntime.BiomeMaxes;
import art.arcane.iris.engine.EngineRuntimeBuilder.RuntimeAssembly;
import art.arcane.iris.engine.IrisEngine.LifecycleState;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.StructureReachability;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.protocol.IrisMessage;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.volmlib.util.math.RNG;
import static art.arcane.iris.engine.EngineShutdownSequence.runCleanup;
/**
* Performs the two supported live reload transitions for an {@link IrisEngine}: a full pack hotload
* and a biome-complex-only rebuild. Both run entirely under the engine lifecycle lock, seal and
* drain generation before swapping anything, and roll the previous runtime back on failure.
*/
final class EngineHotloader {
private final IrisEngine engine;
EngineHotloader(IrisEngine engine) {
this.engine = engine;
}
void hotload() {
hotloadSilently();
engine.getPlatformHooks().fireHotloadEvent(engine);
}
void hotloadComplex() {
synchronized (engine.lifecycleLock) {
engine.requireRunning("rebuild the biome complex");
engine.lifecycleState = LifecycleState.HOTLOADING;
EngineRuntime previous = engine.runtime;
IrisComplex nextComplex = null;
try {
engine.sealForTransition("complex hotload", false);
RuntimeAssembly assembly = new RuntimeAssembly(RNG.r.nextInt(), previous.target());
engine.runtimeAssembly.set(assembly);
EngineRuntime next;
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
assembly.complex = new IrisComplex(engine);
nextComplex = assembly.complex;
assembly.upperContext = engine.runtimeBuilder.buildUpperContext();
BiomeMaxes biomeMaxes = engine.runtimeBuilder.computeBiomeMaxes();
next = previous.withComplex(assembly.cacheId, assembly.complex, assembly.upperContext, biomeMaxes);
} finally {
engine.runtimeAssembly.remove();
}
Throwable retirementFailure = runCleanup(null, previous.complex()::close);
if (retirementFailure != null) {
retirementFailure = runCleanup(retirementFailure, nextComplex::close);
nextComplex = null;
engine.lifecycleState = LifecycleState.FAILED;
throw new IllegalStateException("Failed to retire the previous Iris biome complex.", retirementFailure);
}
engine.runtime = next;
engine.getGenerationSessions().activateNextSession();
engine.lifecycleState = LifecycleState.RUNNING;
engine.getClosing().set(false);
engine.backgroundTasks.openBackgroundTaskAdmission();
} catch (Throwable e) {
if (nextComplex != null && nextComplex != previous.complex()) {
Throwable cleanupFailure = runCleanup(null, nextComplex::close);
if (cleanupFailure != null) {
e.addSuppressed(cleanupFailure);
}
}
if (engine.lifecycleState != LifecycleState.FAILED) {
engine.runtimeBuilder.restoreRuntimeAfterFailedTransition(previous);
}
throw new IllegalStateException("Failed to rebuild the Iris biome complex.", e);
}
}
}
void hotloadSilently() {
synchronized (engine.lifecycleLock) {
engine.requireRunning("hotload");
engine.lifecycleState = LifecycleState.HOTLOADING;
EngineRuntime previousRuntime = engine.runtime;
IrisDimension previousDimension = engine.getDimension();
IrisData previousData = engine.getData();
IrisData replacementData = null;
boolean published = false;
try {
engine.sealForTransition("hotload", false);
replacementData = IrisData.openRuntime(previousData.getDataFolder());
IrisDimension replacement = replacementData.getDimensionLoader().load(previousDimension.getLoadKey());
if (replacement == null) {
throw new IllegalStateException("Studio hotload could not reload Iris dimension '" + previousDimension.getLoadKey() + "'");
}
engine.getPlatformHooks().validateDimensionHotload(engine, replacement);
replacementData.registerEngine(engine);
IrisStructureLocator.invalidate(engine);
StructureReachability.invalidate(engine);
EngineTarget replacementTarget = new EngineTarget(engine.getWorld(), replacement, replacementData);
EngineRuntime nextRuntime = engine.runtimeBuilder.buildRuntime(replacementTarget);
engine.runtimeBuilder.publishRuntime(nextRuntime, previousRuntime);
published = true;
previousData.unregisterEngine(engine);
Throwable previousDataFailure = runCleanup(null, previousData::close);
if (previousDataFailure != null) {
IrisLogging.error("Failed to completely release the previous Iris data runtime.");
IrisLogging.reportError(previousDataFailure);
previousDataFailure.printStackTrace();
}
engine.getPrefetchSaveStarted().set(false);
engine.getEngineData().getStatistics().hotloaded();
if (engine.getWorld().hasPlatformWorld()) {
if (!engine.backgroundTasks.scheduleTrackedTask(() -> {
engine.getPlatformHooks().refreshWorkspace(engine);
engine.getPlatformHooks().reloadDatapacks(engine);
})) {
throw new IllegalStateException("Iris background task admission closed before workspace refresh.");
}
}
broadcastStudioHotload(false, "");
} catch (Throwable e) {
if (!published) {
if (replacementData != null) {
replacementData.unregisterEngine(engine);
Throwable replacementDataFailure = runCleanup(null, replacementData::close);
if (replacementDataFailure != null) {
e.addSuppressed(replacementDataFailure);
}
}
IrisStructureLocator.invalidate(engine);
StructureReachability.invalidate(engine);
if (engine.lifecycleState != LifecycleState.FAILED) {
Throwable rollbackFailure = runCleanup(null, engine.getMantle()::hotload);
if (rollbackFailure == null) {
engine.runtimeBuilder.restoreRuntimeAfterFailedTransition(previousRuntime);
} else {
engine.runtime = previousRuntime;
engine.lifecycleState = LifecycleState.FAILED;
e.addSuppressed(rollbackFailure);
}
}
}
broadcastStudioHotload(true, e.getClass().getSimpleName() + ": " + e.getMessage());
if (e instanceof Error error) {
throw error;
}
if (e instanceof RuntimeException runtimeException) {
throw runtimeException;
}
throw new IllegalStateException("Iris hotload failed.", e);
}
}
}
private void broadcastStudioHotload(boolean failed, String message) {
IrisProtocolServer protocolServer = IrisServices.getOrNull(IrisProtocolServer.class);
if (protocolServer == null) {
return;
}
IrisDimension dimension = engine.getDimension();
String packKey = dimension == null ? "" : dimension.getLoadKey();
protocolServer.broadcastStudioHotload(packKey, 0, failed, message);
protocolServer.broadcastToast(
failed ? IrisMessage.Toast.KIND_ERROR : IrisMessage.Toast.KIND_SUCCESS,
IrisLanguage.plain(ClientUiMessages.TOAST_STUDIO_HOTLOAD),
failed ? IrisLanguage.plain(ClientUiMessages.TOAST_PACK_FAILED, MessageArgument.untrusted("pack", packKey)) : packKey);
}
}
@@ -0,0 +1,112 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine;
import art.arcane.iris.core.localization.BukkitRuntimeMessages;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.volmlib.util.math.M;
/**
* Derives human readable timing reports and the rolling generation rate for an {@link IrisEngine}.
* Purely a reader of the engine's metric accumulators; it never mutates generation state.
*/
final class EngineMetricsReport {
private final IrisEngine engine;
EngineMetricsReport(IrisEngine engine) {
this.engine = engine;
}
void printMetrics(VolmitSender sender) {
KMap<String, Double> totals = new KMap<>();
KMap<String, Double> weights = new KMap<>();
double masterWallClock = engine.getWallClock().getAverage();
KMap<String, Double> timings = engine.getMetrics().pull();
double totalWeight = 0;
double wallClock = engine.getMetrics().getTotal().getAverage();
for (double j : timings.values()) {
totalWeight += j;
}
for (String j : timings.k()) {
weights.put(engine.getName() + "." + j, (wallClock / totalWeight) * timings.get(j));
}
totals.put(engine.getName(), wallClock);
double mtotals = 0;
for (double i : totals.values()) {
mtotals += i;
}
for (String i : totals.k()) {
totals.put(i, (masterWallClock / mtotals) * totals.get(i));
}
double v = 0;
for (double i : weights.values()) {
v += i;
}
for (String i : weights.k()) {
weights.put(i, weights.get(i) / v);
}
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_ENGINE_TOTAL, MessageArgument.untrusted("value", String.valueOf(Form.duration(masterWallClock, 0)))));
for (String i : totals.k()) {
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_ENGINE_ENGINE, MessageArgument.untrusted("i", String.valueOf(i)), MessageArgument.untrusted("value", String.valueOf(Form.duration(totals.get(i), 0)))));
}
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_ENGINE_DETAILS));
for (String i : weights.sortKNumber().reverse()) {
String befb = C.UNDERLINE + "" + C.GREEN + "" + i.split("\\Q[\\E")[0] + C.RESET + C.GRAY + "[";
String num = C.GOLD + i.split("\\Q[\\E")[1].split("]")[0] + C.RESET + C.GRAY + "].";
String afb = C.ITALIC + "" + C.AQUA + i.split("\\Q]\\E")[1].substring(1) + C.RESET + C.GRAY;
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_ENGINE_MESSAGE, MessageArgument.untrusted("befb", String.valueOf(befb)), MessageArgument.untrusted("num", String.valueOf(num)), MessageArgument.untrusted("afb", String.valueOf(afb)), MessageArgument.untrusted("value", String.valueOf(Form.pc(weights.get(i), 0)))));
}
}
double getGeneratedPerSecond() {
if (engine.getPerSecondLatch().flip()) {
double g = engine.getGenerated() - engine.getGeneratedLast().get();
engine.getGeneratedLast().set(engine.getGenerated());
if (g == 0) {
return 0;
}
long dur = M.ms() - engine.getLastGPS().get();
engine.getLastGPS().set(M.ms());
engine.getPerSecond().set(g / ((double) (dur) / 1000D));
}
return engine.getPerSecond().get();
}
}
@@ -0,0 +1,63 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine;
import art.arcane.iris.engine.framework.EngineEffects;
import art.arcane.iris.engine.framework.EngineMode;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.EngineWorldManager;
import java.util.concurrent.CompletableFuture;
/**
* Immutable snapshot of everything an {@link IrisEngine} publishes as a single unit.
* Instances are built by {@link EngineRuntimeBuilder} and retired by {@link EngineShutdownSequence}.
*/
record EngineRuntime(
int cacheId,
EngineTarget target,
IrisComplex complex,
UpperDimensionContext upperContext,
EngineEffects effects,
EngineMode mode,
EngineWorldManager worldManager,
CompletableFuture<Long> hash32,
BiomeMaxes biomeMaxes
) {
EngineRuntime withComplex(
int nextCacheId,
IrisComplex nextComplex,
UpperDimensionContext nextUpperContext,
BiomeMaxes nextBiomeMaxes
) {
return new EngineRuntime(
nextCacheId,
target,
nextComplex,
nextUpperContext,
effects,
mode,
worldManager,
hash32,
nextBiomeMaxes);
}
record BiomeMaxes(double objectDensity, double layerDensity, double decoratorDensity) {
}
}
@@ -0,0 +1,312 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.EngineRuntime.BiomeMaxes;
import art.arcane.iris.engine.IrisEngine.LifecycleState;
import art.arcane.iris.engine.framework.EngineEffects;
import art.arcane.iris.engine.framework.EngineEffectsProvider;
import art.arcane.iris.engine.framework.EngineMode;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.EngineWorldManager;
import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomePaletteLayer;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionMode;
import art.arcane.iris.engine.object.IrisDimensionModeType;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.RNG;
import java.io.File;
import java.util.concurrent.CompletableFuture;
import static art.arcane.iris.engine.EngineShutdownSequence.propagate;
/**
* Assembles and publishes {@link EngineRuntime} snapshots for an {@link IrisEngine}.
* A runtime is built into a thread-local {@link RuntimeAssembly} so partially constructed state is
* visible only to the building thread, then frozen and published to the engine's volatile runtime
* field as a single atomic swap.
*/
final class EngineRuntimeBuilder {
private final IrisEngine engine;
EngineRuntimeBuilder(IrisEngine engine) {
this.engine = engine;
}
EngineRuntime buildRuntime() {
return buildRuntime(engine.getTarget());
}
EngineRuntime buildRuntime(EngineTarget runtimeTarget) {
RuntimeAssembly assembly = new RuntimeAssembly(RNG.r.nextInt(), runtimeTarget);
engine.runtimeAssembly.set(assembly);
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
IrisLogging.debug("Setup Engine " + assembly.cacheId);
long started = M.ms();
assembly.complex = new IrisComplex(engine);
IrisLogging.debug("[IrisEngine timing] complex=" + (M.ms() - started) + "ms");
started = M.ms();
assembly.upperContext = buildUpperContext();
IrisLogging.debug("[IrisEngine timing] buildUpperContext=" + (M.ms() - started) + "ms");
started = M.ms();
assembly.effects = IrisServices.get(EngineEffectsProvider.class).create(engine);
if (assembly.effects == null) {
throw new IllegalStateException("Engine effects provider returned null");
}
IrisLogging.debug("[IrisEngine timing] EngineEffects=" + (M.ms() - started) + "ms");
assembly.hash32 = new CompletableFuture<>();
started = M.ms();
engine.getMantle().hotload();
IrisLogging.debug("[IrisEngine timing] mantle.hotload=" + (M.ms() - started) + "ms");
started = M.ms();
assembly.mode = createMode();
IrisLogging.debug("[IrisEngine timing] setupMode=" + (M.ms() - started) + "ms");
started = M.ms();
assembly.worldManager = IrisServices.get(EngineWorldManagerProvider.class).create(engine);
if (assembly.worldManager == null) {
throw new IllegalStateException("Engine world manager provider returned null");
}
IrisLogging.debug("[IrisEngine timing] IrisWorldManager=" + (M.ms() - started) + "ms");
BiomeMaxes biomeMaxes = computeBiomeMaxes();
return assembly.freeze(biomeMaxes);
} catch (Throwable e) {
Throwable cleanupFailure = engine.shutdownSequence.closeAssembly(assembly, e);
if (cleanupFailure != e) {
e.addSuppressed(cleanupFailure);
}
throw new IllegalStateException("Failed to build a complete Iris engine runtime.", e);
} finally {
engine.runtimeAssembly.remove();
}
}
void publishRuntime(EngineRuntime next, EngineRuntime previous) {
Throwable retirementFailure = engine.shutdownSequence.closeRuntime(previous, null);
if (retirementFailure != null) {
retirementFailure = engine.shutdownSequence.closeRuntime(next, retirementFailure);
engine.lifecycleState = LifecycleState.FAILED;
throw new IllegalStateException("Failed to retire the previous Iris engine runtime.", retirementFailure);
}
if (engine.runtime == previous) {
engine.runtime = null;
}
try {
next.worldManager().start();
} catch (Throwable e) {
Throwable cleanupFailure = engine.shutdownSequence.closeRuntime(next, e);
if (cleanupFailure != e) {
e.addSuppressed(cleanupFailure);
}
engine.lifecycleState = LifecycleState.FAILED;
throw new IllegalStateException("Failed to start the Iris world manager.", e);
}
engine.runtime = next;
engine.publishedTarget = next.target();
engine.getGenerationSessions().activateNextSession();
engine.lifecycleState = LifecycleState.RUNNING;
engine.getClosing().set(false);
engine.backgroundTasks.openBackgroundTaskAdmission();
scheduleRuntimeTasks(next);
IrisLogging.debug("Engine Setup Complete " + next.cacheId());
}
private void scheduleRuntimeTasks(EngineRuntime engineRuntime) {
try {
if (!engine.backgroundTasks.scheduleTrackedTask(() -> {
try {
File[] roots = engine.getData().getLoaders()
.values()
.stream()
.map(ResourceLoader::getFolderName)
.map(name -> new File(engine.getData().getDataFolder(), name))
.filter(File::exists)
.filter(File::isDirectory)
.toArray(File[]::new);
engineRuntime.hash32().complete(IO.hashRecursiveMeta(roots));
} catch (Throwable e) {
engineRuntime.hash32().completeExceptionally(e);
throw propagate(e);
}
})) {
throw new IllegalStateException("Iris background task admission closed before pack hashing.");
}
} catch (Throwable e) {
engineRuntime.hash32().completeExceptionally(e);
IrisLogging.reportError(e);
e.printStackTrace();
}
try {
if (!engine.backgroundTasks.scheduleTrackedTask(() -> engine.getPlatformHooks().refreshDatapackWorkspace(engine))) {
throw new IllegalStateException("Iris background task admission closed before datapack workspace refresh.");
}
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
}
}
UpperDimensionContext buildUpperContext() {
IrisDimension dim = engine.getDimension();
if (!dim.hasUpperDimension()) {
return null;
}
String upperKey = dim.getUpperDimension();
IrisDimension upperDim = upperKey.equals(dim.getLoadKey())
? dim
: IrisData.loadAnyDimension(upperKey, engine.getData());
if (upperDim != null) {
UpperDimensionContext ctx = UpperDimensionContext.create(engine, upperDim);
IrisLogging.info("Upper dimension enabled: " + upperKey
+ (ctx.isSelfReferencing() ? " (self-referencing)" : " (cross-referencing)"));
return ctx;
}
IrisLogging.warn("Upper dimension '" + upperKey + "' could not be resolved, skipping upper terrain.");
return null;
}
private EngineMode createMode() {
Throwable configuredFailure = null;
try {
IrisDimensionMode configuredMode = engine.getDimension().getMode();
if (configuredMode == null) {
configuredMode = new IrisDimensionMode();
engine.getDimension().setMode(configuredMode);
}
EngineMode configured = configuredMode.create(engine);
if (configured == null) {
throw new IllegalStateException("Dimension mode factory returned null");
}
return configured;
} catch (Throwable e) {
configuredFailure = e;
IrisLogging.reportError(e);
e.printStackTrace();
if (engine.getModeFallbackLogged().compareAndSet(false, true)) {
IrisLogging.warn("Failed to initialize configured dimension mode for " + engine.getDimension().getLoadKey() + ", falling back to OVERWORLD mode.");
}
}
try {
EngineMode fallback = IrisDimensionModeType.OVERWORLD.create(engine);
if (fallback == null) {
throw new IllegalStateException("OVERWORLD mode factory returned null");
}
return fallback;
} catch (Throwable fallbackFailure) {
fallbackFailure.addSuppressed(configuredFailure);
throw new IllegalStateException("Both configured and fallback Iris engine modes failed.", fallbackFailure);
}
}
BiomeMaxes computeBiomeMaxes() {
double objectDensity = 0D;
double layerDensity = 0D;
double decoratorDensity = 0D;
for (IrisBiome i : engine.getDimension().getReachableBiomes(engine)) {
double density = 0;
for (IrisObjectPlacement j : i.getObjects()) {
density += j.getDensity() * j.getChance();
}
objectDensity = Math.max(objectDensity, density);
density = 0;
for (IrisDecorator j : i.getDecorators()) {
density += Math.max(j.getStackMax(), 1) * j.getChance();
}
decoratorDensity = Math.max(decoratorDensity, density);
density = 0;
for (IrisBiomePaletteLayer j : i.getLayers()) {
density++;
}
layerDensity = Math.max(layerDensity, density);
}
return new BiomeMaxes(objectDensity, layerDensity, decoratorDensity);
}
void restoreRuntimeAfterFailedTransition(EngineRuntime previous) {
engine.runtime = previous;
if (engine.getGenerationSessions().activeLeases() == 0) {
engine.getGenerationSessions().activateNextSession();
engine.lifecycleState = LifecycleState.RUNNING;
engine.getClosing().set(false);
engine.backgroundTasks.openBackgroundTaskAdmission();
return;
}
engine.lifecycleState = LifecycleState.FAILED;
}
EngineRuntime requireRuntime(String operation) {
EngineRuntime current = engine.runtime;
if (current == null) {
throw new IllegalStateException("Cannot " + operation + " without an active Iris runtime for "
+ engine.getWorld().name() + ".");
}
return current;
}
static final class RuntimeAssembly {
final int cacheId;
final EngineTarget target;
IrisComplex complex;
UpperDimensionContext upperContext;
EngineEffects effects;
EngineMode mode;
EngineWorldManager worldManager;
CompletableFuture<Long> hash32;
RuntimeAssembly(int cacheId, EngineTarget target) {
this.cacheId = cacheId;
this.target = target;
}
EngineRuntime freeze(BiomeMaxes biomeMaxes) {
if (complex == null || effects == null || mode == null || worldManager == null || hash32 == null) {
throw new IllegalStateException("Cannot publish an incomplete Iris engine runtime.");
}
return new EngineRuntime(
cacheId,
target,
complex,
upperContext,
effects,
mode,
worldManager,
hash32,
biomeMaxes);
}
}
}
@@ -0,0 +1,291 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine;
import art.arcane.iris.engine.EngineBackgroundTasks.BackgroundTaskDrain;
import art.arcane.iris.engine.EngineRuntimeBuilder.RuntimeAssembly;
import art.arcane.iris.engine.IrisEngine.LifecycleState;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
/**
* Owns the ordered teardown of a single {@link IrisEngine}, both for a normal close and for a
* construction that failed part way through. Every resource release is gated on the previous one
* having succeeded so a partially released engine never reports itself as closed.
*/
final class EngineShutdownSequence {
private final IrisEngine engine;
private boolean runtimeReleased;
private boolean targetReleased;
private boolean mantleReleased;
private boolean engineDataReleased;
private boolean preservationReleased;
EngineShutdownSequence(IrisEngine engine) {
this.engine = engine;
}
void close() {
Throwable failure;
synchronized (engine.lifecycleLock) {
if (engine.closed) {
return;
}
engine.lifecycleState = LifecycleState.CLOSING;
engine.getClosing().set(true);
engine.backgroundTasks.closeBackgroundTaskAdmission();
EngineTickRegistry.unregisterTicking(engine);
engine.getPlatformHooks().shutdownPregenerator(engine);
try {
engine.getGenerationSessions().sealAndAwait("close", IrisEngine.SESSION_DRAIN_TIMEOUT_MILLIS, true);
} catch (GenerationSessionException e) {
throw new IllegalStateException("Failed to drain Iris generation for close.", e);
}
BackgroundTaskDrain backgroundDrain = engine.backgroundTasks.drainBackgroundTasks("close");
failure = backgroundDrain.failure();
if (!backgroundDrain.allowsResourceRelease() && failure == null) {
failure = new IllegalStateException("Iris background tasks remain active during close.");
}
if (backgroundDrain.allowsResourceRelease()) {
Throwable prefetchFailure = runCleanup(null, engine::savePrefetchOnce);
Throwable engineDataFailure = runCleanup(null, engine::saveEngineData);
failure = appendFailure(failure, prefetchFailure);
failure = appendFailure(failure, engineDataFailure);
failure = releaseRuntime(failure);
if (runtimeReleased) {
failure = releaseTarget(failure);
}
if (targetReleased) {
failure = releaseMantle(failure);
}
if (prefetchFailure == null
&& engineDataFailure == null
&& runtimeReleased
&& targetReleased
&& mantleReleased) {
failure = releaseEngineDataForShutdown(failure);
}
if (engineDataReleased) {
failure = releasePreservation(failure);
}
}
if (failure == null
&& runtimeReleased
&& targetReleased
&& mantleReleased
&& engineDataReleased
&& preservationReleased) {
engine.closed = true;
engine.lifecycleState = LifecycleState.CLOSED;
IrisLogging.debug("Engine Fully Shutdown!");
}
}
if (failure != null) {
IrisLogging.error("Iris engine shutdown remains incomplete after cleanup failures for " + engine.getWorld().name() + ".");
IrisLogging.reportError(failure);
failure.printStackTrace();
throw new IllegalStateException("Iris engine shutdown remains incomplete after cleanup failures.", failure);
}
}
void cleanupFailedConstruction(Throwable original) {
engine.getClosing().set(true);
engine.backgroundTasks.closeBackgroundTaskAdmission();
EngineTickRegistry.unregisterTicking(engine);
engine.lifecycleState = LifecycleState.FAILED;
Throwable cleanupFailure = null;
try {
engine.getGenerationSessions().sealAndAwait("failed initialization", 0L, true);
} catch (Throwable e) {
cleanupFailure = appendFailure(cleanupFailure, e);
}
engine.backgroundTasks.cancelBackgroundTasks("failed initialization");
BackgroundTaskDrain backgroundDrain = engine.backgroundTasks.drainBackgroundTasks("failed initialization");
cleanupFailure = appendFailure(cleanupFailure, backgroundDrain.failure());
if (!backgroundDrain.allowsResourceRelease()) {
cleanupFailure = appendFailure(cleanupFailure,
new IllegalStateException("Iris background tasks remain active after failed initialization."));
if (cleanupFailure != original) {
original.addSuppressed(cleanupFailure);
}
return;
}
cleanupFailure = closeRuntime(engine.runtime, cleanupFailure);
engine.runtime = null;
cleanupFailure = runCleanup(cleanupFailure, engine.getTarget()::close);
cleanupFailure = runCleanup(cleanupFailure, engine.getMantle()::close);
cleanupFailure = runCleanup(cleanupFailure, engine.engineDataStore::releaseEngineData);
engine.closed = true;
cleanupFailure = runCleanup(cleanupFailure, () -> {
PreservationRegistry registry = IrisServices.getOrNull(PreservationRegistry.class);
if (registry != null) {
registry.dereference();
}
});
if (cleanupFailure != null && cleanupFailure != original) {
original.addSuppressed(cleanupFailure);
}
}
Throwable closeAssembly(RuntimeAssembly assembly, Throwable failure) {
if (assembly == null) {
return failure;
}
failure = runCleanup(failure, () -> {
if (assembly.worldManager != null) {
assembly.worldManager.close();
}
});
failure = runCleanup(failure, () -> {
if (assembly.effects != null) {
assembly.effects.close();
}
});
failure = runCleanup(failure, () -> {
if (assembly.mode != null) {
assembly.mode.close();
}
});
failure = runCleanup(failure, () -> {
if (assembly.complex != null) {
assembly.complex.close();
}
});
failure = runCleanup(failure, () -> {
if (assembly.hash32 != null) {
assembly.hash32.cancel(true);
}
});
return failure;
}
Throwable closeRuntime(EngineRuntime engineRuntime, Throwable failure) {
if (engineRuntime == null) {
return failure;
}
failure = runCleanup(failure, engineRuntime.worldManager()::close);
failure = runCleanup(failure, engineRuntime.effects()::close);
failure = runCleanup(failure, engineRuntime.mode()::close);
failure = runCleanup(failure, engineRuntime.complex()::close);
failure = runCleanup(failure, () -> engineRuntime.hash32().cancel(true));
return failure;
}
private Throwable releaseRuntime(Throwable failure) {
if (runtimeReleased) {
return failure;
}
Throwable runtimeFailure = closeRuntime(engine.runtime, null);
if (runtimeFailure != null) {
return appendFailure(failure, runtimeFailure);
}
engine.runtime = null;
runtimeReleased = true;
return failure;
}
private Throwable releaseTarget(Throwable failure) {
if (targetReleased) {
return failure;
}
Throwable targetFailure = runCleanup(null, engine.getTarget()::close);
if (targetFailure != null) {
return appendFailure(failure, targetFailure);
}
targetReleased = true;
return failure;
}
private Throwable releaseMantle(Throwable failure) {
if (mantleReleased) {
return failure;
}
Throwable mantleFailure = runCleanup(null, engine.getMantle()::close);
if (mantleFailure != null) {
return appendFailure(failure, mantleFailure);
}
mantleReleased = true;
return failure;
}
private Throwable releaseEngineDataForShutdown(Throwable failure) {
if (engineDataReleased) {
return failure;
}
Throwable dataFailure = runCleanup(null, engine.engineDataStore::releaseEngineData);
if (dataFailure != null) {
return appendFailure(failure, dataFailure);
}
engineDataReleased = true;
return failure;
}
private Throwable releasePreservation(Throwable failure) {
if (preservationReleased) {
return failure;
}
Throwable preservationFailure = runCleanup(null, () -> {
PreservationRegistry registry = IrisServices.getOrNull(PreservationRegistry.class);
if (registry != null) {
registry.dereference();
}
});
if (preservationFailure != null) {
return appendFailure(failure, preservationFailure);
}
preservationReleased = true;
return failure;
}
static Throwable runCleanup(Throwable failure, Runnable cleanup) {
try {
cleanup.run();
} catch (Throwable e) {
return appendFailure(failure, e);
}
return failure;
}
static Throwable appendFailure(Throwable failure, Throwable additional) {
if (additional == null) {
return failure;
}
if (failure == null) {
return additional;
}
if (failure != additional) {
failure.addSuppressed(additional);
}
return failure;
}
static RuntimeException propagate(Throwable throwable) {
if (throwable instanceof RuntimeException runtimeException) {
return runtimeException;
}
if (throwable instanceof Error error) {
throw error;
}
return new IllegalStateException(throwable);
}
}
@@ -0,0 +1,75 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.scheduling.J;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Set;
/**
* Process-wide registry of live {@link IrisEngine} instances driven by one shared repeating task.
* The task is created on the first registration and cancelled once the last engine unregisters, so
* no ticking work runs when no engine is loaded.
*/
final class EngineTickRegistry {
private static final Object TICK_LOCK = new Object();
private static final Set<IrisEngine> TICK_ENGINES = Collections.newSetFromMap(new IdentityHashMap<>());
private static int sharedTickTask = -1;
private EngineTickRegistry() {
}
static void registerTicking(IrisEngine engine) {
synchronized (TICK_LOCK) {
TICK_ENGINES.add(engine);
if (sharedTickTask == -1) {
sharedTickTask = J.ar(EngineTickRegistry::tickEngines, 1);
}
}
}
static void unregisterTicking(IrisEngine engine) {
synchronized (TICK_LOCK) {
TICK_ENGINES.remove(engine);
if (TICK_ENGINES.isEmpty() && sharedTickTask != -1) {
J.car(sharedTickTask);
sharedTickTask = -1;
}
}
}
private static void tickEngines() {
List<IrisEngine> engines;
synchronized (TICK_LOCK) {
engines = List.copyOf(TICK_ENGINES);
}
for (IrisEngine engine : engines) {
try {
engine.tickRandomPlayer();
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
}
}
}
}
@@ -38,7 +38,8 @@ import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.data.DataProvider;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.project.interpolation.IrisInterpolation.NoiseBounds;
import art.arcane.iris.util.project.interpolation.NoiseBounds;
import art.arcane.iris.util.project.interpolation.NoiseBoundsProvider;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.iris.util.project.stream.interpolation.Interpolated;
@@ -49,6 +50,7 @@ import lombok.Getter;
import lombok.ToString;
import java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -56,16 +58,29 @@ import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
@Data
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache"})
@ToString(exclude = {"data", "gridBoundsCache"})
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators"})
@ToString(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators"})
public class IrisComplex implements DataProvider {
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L);
private static final int GRID_BOUNDS_CACHE_SIZE = 8192;
private static final int HEIGHT_BOUNDS_GRID = 4;
@Getter(AccessLevel.NONE)
private final transient ThreadLocal<GridBoundsCache> gridBoundsCache = ThreadLocal.withInitial(GridBoundsCache::new);
/**
* Immutable snapshot of {@link #generators} taken once at the end of construction, in the exact
* iteration order the map produces. The per-column height paths walk these arrays instead of
* allocating map/set iterators, and the frozen order keeps the floating point accumulation order
* identical to the map iteration it replaces. Mutating {@link #generators} after construction is
* not reflected here.
*/
@Getter(AccessLevel.NONE)
private final transient IrisInterpolator[] frozenInterpolators;
@Getter(AccessLevel.NONE)
private final transient IrisGenerator[][] frozenGenerators;
private RNG rng;
private double fluidHeight;
private IrisData data;
@@ -142,6 +157,15 @@ public class IrisComplex implements DataProvider {
region.getAllBiomes(this).forEach(this::registerGenerators);
});
}
int interpolatorCount = generators.size();
frozenInterpolators = new IrisInterpolator[interpolatorCount];
frozenGenerators = new IrisGenerator[interpolatorCount][];
int frozenIndex = 0;
for (Map.Entry<IrisInterpolator, Set<IrisGenerator>> entry : generators.entrySet()) {
frozenInterpolators[frozenIndex] = entry.getKey();
frozenGenerators[frozenIndex] = entry.getValue().toArray(new IrisGenerator[0]);
frozenIndex++;
}
generatorBounds = buildGeneratorBounds(engine);
KList<IrisShapedGeneratorStyle> overlayNoise = engine.getDimension().getOverlayNoise();
overlayStream = overlayNoise.isEmpty()
@@ -364,8 +388,8 @@ public class IrisComplex implements DataProvider {
return biome;
}
private double interpolateGenerators(Engine engine, IrisInterpolator interpolator, int interpolatorIndex, Set<IrisGenerator> generators, double x, double z, long seed) {
if (generators.isEmpty()) {
private double interpolateGenerators(Engine engine, IrisInterpolator interpolator, int interpolatorIndex, IrisGenerator[] generators, double x, double z, long seed) {
if (generators.length == 0) {
return 0;
}
@@ -379,13 +403,14 @@ public class IrisComplex implements DataProvider {
d += M.lerp(lo, hi, i.getHeight(x, z, seed + 239945));
}
return d / generators.size();
return d / generators.length;
}
private NoiseBounds gridSampleBounds(Engine engine, IrisInterpolator interpolator, int interpolatorIndex, Set<IrisGenerator> generators, double x, double z) {
private NoiseBounds gridSampleBounds(Engine engine, IrisInterpolator interpolator, int interpolatorIndex, IrisGenerator[] generators, double x, double z) {
int grid = HEIGHT_BOUNDS_GRID;
GridBoundsCache cache = gridBoundsCache.get();
if (grid <= 1) {
return sampleBoundsRaw(engine, interpolator, generators, x, z);
return sampleBoundsRaw(cache, engine, interpolator, generators, x, z);
}
int xi = (int) Math.floor(x);
@@ -396,7 +421,6 @@ public class IrisComplex implements DataProvider {
double fx = (x - gx) / grid;
double fz = (z - gz) / grid;
GridBoundsCache cache = gridBoundsCache.get();
long b00 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx, gz);
long b10 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx + grid, gz);
long b01 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx, gz + grid);
@@ -407,13 +431,13 @@ public class IrisComplex implements DataProvider {
return new NoiseBounds(lo, hi);
}
private long cornerBounds(GridBoundsCache cache, Engine engine, IrisInterpolator interpolator, int interpolatorIndex, Set<IrisGenerator> generators, int gx, int gz) {
private long cornerBounds(GridBoundsCache cache, Engine engine, IrisInterpolator interpolator, int interpolatorIndex, IrisGenerator[] generators, int gx, int gz) {
int slot = cache.slot(gx, gz, interpolatorIndex);
if (cache.valid[slot] && cache.gx[slot] == gx && cache.gz[slot] == gz && cache.idx[slot] == interpolatorIndex) {
return cache.packed[slot];
}
NoiseBounds bounds = sampleBoundsRaw(engine, interpolator, generators, gx, gz);
NoiseBounds bounds = sampleBoundsRaw(cache, engine, interpolator, generators, gx, gz);
long packed = (((long) Float.floatToRawIntBits((float) bounds.min())) << 32) | (Float.floatToRawIntBits((float) bounds.max()) & 0xFFFFFFFFL);
cache.gx[slot] = gx;
cache.gz[slot] = gz;
@@ -437,37 +461,23 @@ public class IrisComplex implements DataProvider {
return a + ((b - a) * fz);
}
private NoiseBounds sampleBoundsRaw(Engine engine, IrisInterpolator interpolator, Set<IrisGenerator> generators, double x, double z) {
CoordinateBiomeCache sampleCache = new CoordinateBiomeCache(64);
private NoiseBounds sampleBoundsRaw(GridBoundsCache cache, Engine engine, IrisInterpolator interpolator, IrisGenerator[] generators, double x, double z) {
IdentityHashMap<IrisBiome, GeneratorBounds> cachedBounds = generatorBounds.get(interpolator);
IdentityHashMap<IrisBiome, GeneratorBounds> localBounds = new IdentityHashMap<>(8);
return interpolator.interpolateBounds(x, z, (xx, zz) -> {
try {
IrisBiome bx = sampleCache.get(xx, zz);
if (bx == null) {
bx = baseBiomeStream.get(xx, zz);
sampleCache.put(xx, zz, bx);
}
BoundsSampler sampler = cache.sampler.isInUse() ? new BoundsSampler() : cache.sampler;
sampler.bind(this, engine, generators, cachedBounds);
GeneratorBounds bounds = resolveGeneratorBounds(engine, generators, bx, cachedBounds, localBounds);
return bounds.noiseBounds;
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
IrisLogging.error("Failed to sample interpolated biome bounds at " + xx + " " + zz + "...");
}
return ZERO_NOISE_BOUNDS;
});
try {
return interpolator.interpolateBounds(x, z, sampler);
} finally {
sampler.release();
}
}
private double getInterpolatedHeight(Engine engine, double x, double z, long seed) {
double h = 0;
int interpolatorIndex = 0;
for (Map.Entry<IrisInterpolator, Set<IrisGenerator>> entry : generators.entrySet()) {
h += interpolateGenerators(engine, entry.getKey(), interpolatorIndex, entry.getValue(), x, z, seed);
interpolatorIndex++;
for (int interpolatorIndex = 0; interpolatorIndex < frozenInterpolators.length; interpolatorIndex++) {
h += interpolateGenerators(engine, frozenInterpolators[interpolatorIndex], interpolatorIndex, frozenGenerators[interpolatorIndex], x, z, seed);
}
return h;
@@ -509,18 +519,18 @@ public class IrisComplex implements DataProvider {
allBiomes.add(focusBiome);
}
for (Map.Entry<IrisInterpolator, Set<IrisGenerator>> entry : generators.entrySet()) {
for (int i = 0; i < frozenInterpolators.length; i++) {
IdentityHashMap<IrisBiome, GeneratorBounds> interpolatorBounds = new IdentityHashMap<>(Math.max(allBiomes.size(), 16));
for (IrisBiome biome : allBiomes) {
interpolatorBounds.put(biome, computeGeneratorBounds(engine, entry.getValue(), biome));
interpolatorBounds.put(biome, computeGeneratorBounds(engine, frozenGenerators[i], biome));
}
bounds.put(entry.getKey(), interpolatorBounds);
bounds.put(frozenInterpolators[i], interpolatorBounds);
}
return bounds;
}
private GeneratorBounds computeGeneratorBounds(Engine engine, Set<IrisGenerator> generators, IrisBiome biome) {
private GeneratorBounds computeGeneratorBounds(Engine engine, IrisGenerator[] generators, IrisBiome biome) {
double min = 0D;
double max = 0D;
@@ -539,7 +549,7 @@ public class IrisComplex implements DataProvider {
private GeneratorBounds resolveGeneratorBounds(
Engine engine,
Set<IrisGenerator> generators,
IrisGenerator[] generators,
IrisBiome biome,
IdentityHashMap<IrisBiome, GeneratorBounds> cachedBounds,
IdentityHashMap<IrisBiome, GeneratorBounds> localBounds
@@ -627,6 +637,7 @@ public class IrisComplex implements DataProvider {
private final int[] idx = new int[GRID_BOUNDS_CACHE_SIZE];
private final long[] packed = new long[GRID_BOUNDS_CACHE_SIZE];
private final boolean[] valid = new boolean[GRID_BOUNDS_CACHE_SIZE];
private final BoundsSampler sampler = new BoundsSampler();
private int slot(int cornerX, int cornerZ, int interpolatorIndex) {
long h = (cornerX * 0x9E3779B97F4A7C15L) ^ (cornerZ * 0xC2B2AE3D27D4EB4FL) ^ (interpolatorIndex * 0x165667B19E3779F9L);
@@ -635,53 +646,193 @@ public class IrisComplex implements DataProvider {
}
}
private static class CoordinateBiomeCache {
/**
* Reusable per-thread bounds provider. Holds the biome memo and the lazily computed generator
* bounds for one sampleBoundsRaw pass so the pass allocates nothing; {@link #bind}
* resets both, giving each pass the same empty-scratch semantics a fresh allocation had.
* <p>
* Single threaded and non reentrant by contract, matching the thread local sample caches in
* IrisInterpolation that this provider is invoked through. If a nested pass ever does appear,
* {@link #isInUse()} makes the caller fall back to a freshly allocated sampler.
*/
private static final class BoundsSampler implements NoiseBoundsProvider {
private final CoordinateBiomeCache sampleCache = new CoordinateBiomeCache(64);
private final IdentityHashMap<IrisBiome, GeneratorBounds> localBounds = new IdentityHashMap<>(8);
private IrisComplex complex;
private Engine engine;
private IrisGenerator[] generators;
private IdentityHashMap<IrisBiome, GeneratorBounds> cachedBounds;
private boolean inUse;
private boolean isInUse() {
return inUse;
}
private void bind(IrisComplex complex, Engine engine, IrisGenerator[] generators, IdentityHashMap<IrisBiome, GeneratorBounds> cachedBounds) {
this.complex = complex;
this.engine = engine;
this.generators = generators;
this.cachedBounds = cachedBounds;
this.inUse = true;
sampleCache.clear();
if (!localBounds.isEmpty()) {
localBounds.clear();
}
}
private void release() {
complex = null;
engine = null;
generators = null;
cachedBounds = null;
inUse = false;
}
@Override
public NoiseBounds noise(double xx, double zz) {
try {
IrisBiome bx = sampleCache.get(xx, zz);
if (bx == null) {
bx = complex.baseBiomeStream.get(xx, zz);
sampleCache.put(xx, zz, bx);
}
GeneratorBounds bounds = complex.resolveGeneratorBounds(engine, generators, bx, cachedBounds, localBounds);
return bounds.noiseBounds;
} catch (Throwable e) {
long now = System.currentTimeMillis();
long last = lastBoundsFailureLog.get();
if (now - last >= 5000L && lastBoundsFailureLog.compareAndSet(last, now)) {
IrisLogging.reportError(e);
IrisLogging.warn("Failed to sample interpolated biome bounds at " + xx + " " + zz + ", flattening height to zero: " + e.getClass().getSimpleName() + ": " + e.getMessage());
}
}
return ZERO_NOISE_BOUNDS;
}
}
/**
* Open addressed biome memo keyed on the packed coordinate bits, replacing a linear scan that was
* quadratic in the number of columns a wide starcast touches. Single threaded by contract.
*/
private static final class CoordinateBiomeCache {
private long[] xBits;
private long[] zBits;
private IrisBiome[] values;
private byte[] states;
private int mask;
private int resizeThreshold;
private int size;
private CoordinateBiomeCache(int initialSize) {
xBits = new long[initialSize];
zBits = new long[initialSize];
values = new IrisBiome[initialSize];
private CoordinateBiomeCache(int initialCapacity) {
int minimumCapacity = Math.max(8, initialCapacity);
int tableSize = tableSizeFor((minimumCapacity << 1) + minimumCapacity);
xBits = new long[tableSize];
zBits = new long[tableSize];
values = new IrisBiome[tableSize];
states = new byte[tableSize];
mask = tableSize - 1;
resizeThreshold = Math.max(1, (tableSize * 3) >> 2);
size = 0;
}
private void clear() {
if (size == 0) {
return;
}
Arrays.fill(states, (byte) 0);
size = 0;
}
private IrisBiome get(double x, double z) {
long xb = Double.doubleToLongBits(x);
long zb = Double.doubleToLongBits(z);
for (int i = 0; i < size; i++) {
if (xBits[i] == xb && zBits[i] == zb) {
return values[i];
}
}
return null;
int slot = findSlot(Double.doubleToLongBits(x), Double.doubleToLongBits(z));
return states[slot] == 0 ? null : values[slot];
}
private void put(double x, double z, IrisBiome biome) {
if (size >= xBits.length) {
grow();
long xb = Double.doubleToLongBits(x);
long zb = Double.doubleToLongBits(z);
int slot = findSlot(xb, zb);
boolean occupied = states[slot] != 0;
xBits[slot] = xb;
zBits[slot] = zb;
values[slot] = biome;
states[slot] = 1;
if (occupied) {
return;
}
xBits[size] = Double.doubleToLongBits(x);
zBits[size] = Double.doubleToLongBits(z);
values[size] = biome;
size++;
if (size >= resizeThreshold) {
grow();
}
}
private int findSlot(long xb, long zb) {
int slot = mix(xb, zb) & mask;
while (states[slot] != 0) {
if (xBits[slot] == xb && zBits[slot] == zb) {
break;
}
slot = (slot + 1) & mask;
}
return slot;
}
private int mix(long xb, long zb) {
long hash = xb * 0x9E3779B97F4A7C15L;
hash ^= Long.rotateLeft(zb * 0xC2B2AE3D27D4EB4FL, 32);
hash ^= (hash >>> 33);
hash *= 0xff51afd7ed558ccdL;
hash ^= (hash >>> 33);
return (int) hash;
}
private void grow() {
int nextSize = xBits.length << 1;
long[] nx = new long[nextSize];
long[] nz = new long[nextSize];
IrisBiome[] nv = new IrisBiome[nextSize];
System.arraycopy(xBits, 0, nx, 0, size);
System.arraycopy(zBits, 0, nz, 0, size);
System.arraycopy(values, 0, nv, 0, size);
xBits = nx;
zBits = nz;
values = nv;
long[] previousXBits = xBits;
long[] previousZBits = zBits;
IrisBiome[] previousValues = values;
byte[] previousStates = states;
int nextLength = previousXBits.length << 1;
xBits = new long[nextLength];
zBits = new long[nextLength];
values = new IrisBiome[nextLength];
states = new byte[nextLength];
mask = nextLength - 1;
resizeThreshold = Math.max(1, (nextLength * 3) >> 2);
size = 0;
for (int i = 0; i < previousStates.length; i++) {
if (previousStates[i] == 0) {
continue;
}
int slot = findSlot(previousXBits[i], previousZBits[i]);
xBits[slot] = previousXBits[i];
zBits[slot] = previousZBits[i];
values[slot] = previousValues[i];
states[slot] = 1;
size++;
}
}
private int tableSizeFor(int value) {
int n = value - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
int tableSize = n + 1;
if (tableSize < 8) {
return 8;
}
return tableSize;
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,224 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.object.IrisMarker;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisSpawner;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.plugin.Chunks;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.volmlib.util.matter.MatterMarker;
import org.bukkit.Chunk;
import org.bukkit.World;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
/**
* Resolves the mantle spawn markers of a chunk into spawners. On Folia the mantle read runs
* asynchronously and only the obstruction check and the consumer callback hop back onto the region
* thread that owns the chunk; obstructed markers are removed off-thread.
*/
final class MarkerSpawnScanner {
private final IrisWorldManager manager;
private final Set<Long> markerScanQueue = ConcurrentHashMap.newKeySet();
MarkerSpawnScanner(IrisWorldManager manager) {
this.manager = manager;
}
Map<IrisPosition, KSet<IrisSpawner>> getSpawnersFromMarkers(Chunk c) {
Map<IrisPosition, KSet<IrisSpawner>> p = new KMap<>();
Set<IrisPosition> b = new KSet<>();
if (J.isFolia()) {
if (!manager.getMantle().isChunkLoaded(c.getX(), c.getZ())) {
manager.chunkMaintenance.warmupMantleChunkAsync(c.getX(), c.getZ());
}
return p;
}
manager.getMantle().iterateChunk(c.getX(), c.getZ(), MatterMarker.class, (x, y, z, t) -> {
if (t.getTag().equals("cave_floor") || t.getTag().equals("cave_ceiling")) {
return;
}
IrisMarker mark = manager.getData().getMarkerLoader().load(t.getTag());
if (mark == null) {
return;
}
IrisPosition pos = new IrisPosition((c.getX() << 4) + x, y, (c.getZ() << 4) + z);
if (isMarkerObstructed(c, pos, mark.isEmptyAbove())) {
b.add(pos);
return;
}
for (String i : mark.getSpawners()) {
IrisSpawner m = manager.getData().getSpawnerLoader().load(i);
if (m == null) {
IrisLogging.error("Cannot load spawner: " + i + " for marker on " + manager.getName());
continue;
}
m.setReferenceMarker(mark);
// This is so fucking incorrect its a joke
//noinspection ConstantConditions
if (m != null) {
p.computeIfAbsent(pos, (k) -> new KSet<>()).add(m);
}
}
});
for (IrisPosition i : b) {
manager.getEngine().getMantle().getMantle().remove(i.getX(), i.getY(), i.getZ(), MatterMarker.class);
}
return p;
}
void forEachMarkerSpawner(Chunk c, BiConsumer<IrisPosition, KSet<IrisSpawner>> consumer) {
if (c == null || consumer == null) {
return;
}
if (!J.isFolia()) {
int minY = manager.getEngine().getWorld().minHeight();
getSpawnersFromMarkers(c).forEach((relative, spawners) -> {
if (spawners.isEmpty()) {
return;
}
consumer.accept(new IrisPosition(relative.getX(), relative.getY() + minY, relative.getZ()), spawners);
});
return;
}
int chunkX = c.getX();
int chunkZ = c.getZ();
World world = c.getWorld();
long key = Cache.key(chunkX, chunkZ);
if (!markerScanQueue.add(key)) {
return;
}
J.a(manager.managedTask("bukkit_world_manager_marker_scan", () -> {
try {
Map<IrisPosition, MarkerSpawnData> markerData = collectMarkerSpawnData(chunkX, chunkZ);
if (markerData.isEmpty()) {
return;
}
J.runRegion(world, chunkX, chunkZ, manager.managedTask("bukkit_world_manager_marker_scan_region", () -> {
if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
return;
}
Chunk chunk = world.getChunkAt(chunkX, chunkZ);
int minY = manager.getEngine().getWorld().minHeight();
markerData.forEach((relative, data) -> {
if (data.spawners.isEmpty()) {
return;
}
if (isMarkerObstructed(chunk, relative, data.requiresEmptyAbove)) {
removeMarkerAsync(relative);
return;
}
consumer.accept(new IrisPosition(relative.getX(), relative.getY() + minY, relative.getZ()), data.spawners);
});
}));
} catch (Throwable e) {
IrisLogging.reportError(e);
} finally {
markerScanQueue.remove(key);
}
}, () -> markerScanQueue.remove(key)));
}
private Map<IrisPosition, MarkerSpawnData> collectMarkerSpawnData(int chunkX, int chunkZ) {
Map<IrisPosition, MarkerSpawnData> markerData = new KMap<>();
manager.getMantle().iterateChunk(chunkX, chunkZ, MatterMarker.class, (x, y, z, t) -> {
if (t.getTag().equals("cave_floor") || t.getTag().equals("cave_ceiling")) {
return;
}
IrisMarker mark = manager.getData().getMarkerLoader().load(t.getTag());
if (mark == null) {
return;
}
IrisPosition position = new IrisPosition((chunkX << 4) + x, y, (chunkZ << 4) + z);
MarkerSpawnData data = markerData.computeIfAbsent(position, k -> new MarkerSpawnData());
data.requiresEmptyAbove = data.requiresEmptyAbove || mark.isEmptyAbove();
for (String i : mark.getSpawners()) {
IrisSpawner spawner = manager.getData().getSpawnerLoader().load(i);
if (spawner == null) {
IrisLogging.error("Cannot load spawner: " + i + " for marker on " + manager.getName());
continue;
}
spawner.setReferenceMarker(mark);
data.spawners.add(spawner);
}
});
return markerData;
}
private boolean isMarkerObstructed(Chunk chunk, IrisPosition relative, boolean requiresEmptyAbove) {
if (!requiresEmptyAbove) {
return false;
}
int minY = manager.getEngine().getWorld().minHeight();
int markerY = WorldBlockDropRouter.toWorldY(relative.getY(), minY);
if (markerY + 2 >= chunk.getWorld().getMaxHeight()) {
return true;
}
int localX = relative.getX() & 15;
int localZ = relative.getZ() & 15;
return chunk.getBlock(localX, markerY + 1, localZ).getBlockData().getMaterial().isSolid()
|| chunk.getBlock(localX, markerY + 2, localZ).getBlockData().getMaterial().isSolid();
}
private void removeMarkerAsync(IrisPosition marker) {
J.a(manager.managedTask("bukkit_world_manager_remove_marker", () -> {
try {
manager.getMantle().remove(marker.getX(), marker.getY(), marker.getZ(), MatterMarker.class);
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}));
}
private static final class MarkerSpawnData {
private final KSet<IrisSpawner> spawners = new KSet<>();
private boolean requiresEmptyAbove;
}
}
@@ -14,7 +14,7 @@ import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.data.DataProvider;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.project.interpolation.IrisInterpolation.NoiseBounds;
import art.arcane.iris.util.project.interpolation.NoiseBounds;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.iris.util.project.stream.interpolation.Interpolated;
@@ -0,0 +1,134 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.service.tree.BlockDropRouter;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBlockDrops;
import art.arcane.iris.engine.object.IrisMarker;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.matter.MatterMarker;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.inventory.ItemStack;
import java.util.List;
import java.util.function.Consumer;
/**
* Biome, region and dimension block drops for a Bukkit Iris world. The drop list is resolved on the
* event thread, the marker cleanup runs asynchronously and the drops themselves are handed to the
* event's own router when it offers one so a deferred break keeps its inventory destination.
*/
final class WorldBlockDropRouter {
private final IrisWorldManager manager;
WorldBlockDropRouter(IrisWorldManager manager) {
this.manager = manager;
}
void onBlockBreak(BlockBreakEvent e) {
if (e.getBlock().getWorld().equals(BukkitWorldBinding.world(manager.getTarget().getWorld()))) {
int blockX = e.getBlock().getX();
int mantleY = toMantleY(e.getBlock().getY(), manager.getEngine().getWorld().minHeight());
int blockZ = e.getBlock().getZ();
KList<ItemStack> d = new KList<>();
IrisBiome b = EngineBukkitOps.getBiome(manager.getEngine(), e.getBlock().getLocation());
List<IrisBlockDrops> dropProviders = filterDrops(b.getBlockDrops(), e, manager.getData());
if (dropProviders.stream().noneMatch(IrisBlockDrops::isSkipParents)) {
IrisRegion r = EngineBukkitOps.getRegion(manager.getEngine(), e.getBlock().getLocation());
dropProviders.addAll(filterDrops(r.getBlockDrops(), e, manager.getData()));
dropProviders.addAll(filterDrops(manager.getEngine().getDimension().getBlockDrops(), e, manager.getData()));
}
dropProviders.forEach(provider -> provider.fillDrops(false, d));
if (dropProviders.stream().anyMatch(IrisBlockDrops::isReplaceVanillaDrops)) {
e.setDropItems(false);
}
World w = e.getBlock().getWorld();
Location blockLocation = e.getBlock().getLocation();
Location dropLocation = blockLocation.clone().add(.5, .5, .5);
BlockDropRouter dropRouter = e instanceof BlockDropRouter router ? router : null;
Runnable finalizedBreak = manager.managedTask("bukkit_world_manager_block_break_finalize", () -> {
if (e.isCancelled()) {
return;
}
J.a(manager.managedTask("bukkit_world_manager_block_break_marker", () -> {
MatterMarker marker = manager.getMantle().get(blockX, mantleY, blockZ, MatterMarker.class);
if (marker == null || marker.getTag().equals("cave_floor") || marker.getTag().equals("cave_ceiling")) {
return;
}
IrisMarker mark = manager.getData().getMarkerLoader().load(marker.getTag());
if (mark == null || mark.isRemoveOnChange()) {
manager.getMantle().remove(blockX, mantleY, blockZ, MatterMarker.class);
}
}));
routeDrops(d, dropRouter, item -> w.dropItemNaturally(dropLocation, item));
});
if (!J.runAt(blockLocation, finalizedBreak, 1) && !J.isFolia()) {
J.s(finalizedBreak, 1);
}
}
}
void onBlockPlace(BlockPlaceEvent e) {
}
static int toMantleY(int worldY, int minHeight) {
return worldY - minHeight;
}
static <T> void routeDrops(Iterable<T> drops, BlockDropRouter router, Consumer<T> fallback) {
for (T drop : drops) {
boolean routed = false;
if (router != null) {
try {
routed = router.routeDrop(drop);
} catch (Throwable error) {
IrisLogging.reportError("Failed to route a deferred Iris block drop.", error);
}
}
if (!routed) {
fallback.accept(drop);
}
}
}
static int toWorldY(int mantleY, int minHeight) {
return mantleY + minHeight;
}
private List<IrisBlockDrops> filterDrops(KList<IrisBlockDrops> drops, BlockBreakEvent e, IrisData data) {
return new KList<>(drops.stream().filter(d -> d.shouldDropFor(e.getBlock().getBlockData(), data)).toList());
}
}
@@ -0,0 +1,405 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.object.IrisSpawner;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.plugin.Chunks;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.math.PowerOfTwoCoordinates;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.Matter;
import org.bukkit.Chunk;
import org.bukkit.World;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Chunk discovery, post-load block updates and mantle warmup for a Bukkit Iris world. Every scan is
* scheduled from the global thread, fans out per player onto the owning entity thread and touches a
* chunk only from its own region thread; the scan-scheduled flags and per-chunk key sets keep a
* single pass in flight per chunk and are always cleared on the rejection path.
*/
final class WorldChunkMaintenance {
private static final int MAX_FORCED_CHUNK_UPDATES = 128;
private final IrisWorldManager manager;
private final Set<Long> mantleWarmupQueue = ConcurrentHashMap.newKeySet();
private final Set<Long> markerFlagQueue = ConcurrentHashMap.newKeySet();
private final Set<Long> discoveredFlagQueue = ConcurrentHashMap.newKeySet();
private final Set<Long> chunkUpdateQueue = ConcurrentHashMap.newKeySet();
private final AtomicBoolean chunkUpdateScanScheduled = new AtomicBoolean();
private final AtomicBoolean chunkDiscoveryScanScheduled = new AtomicBoolean();
private int forcedChunkUpdateCursor = 0;
WorldChunkMaintenance(IrisWorldManager manager) {
this.manager = manager;
}
void discoverChunks() {
World world = BukkitWorldBinding.world(manager.getEngine().getWorld());
if (world == null) {
return;
}
if (manager.entitySpawner.isPregenActiveForThisWorld()) {
return;
}
if (!chunkDiscoveryScanScheduled.compareAndSet(false, true)) {
return;
}
boolean scheduled = J.runGlobal(manager.managedTask("bukkit_world_manager_discover_chunks", () -> {
try {
if (manager.getEngine().isClosed() || !world.equals(BukkitWorldBinding.world(manager.getEngine().getWorld()))) {
return;
}
for (Player player : world.getPlayers()) {
if (player == null) {
continue;
}
J.runEntity(player, manager.managedTask("bukkit_world_manager_discover_player", () -> {
if (!player.isOnline() || !world.equals(player.getWorld())) {
return;
}
int centerX = PowerOfTwoCoordinates.blockToChunkFloor(player.getLocation().getBlockX());
int centerZ = PowerOfTwoCoordinates.blockToChunkFloor(player.getLocation().getBlockZ());
int radius = 1;
for (int x = -radius; x <= radius; x++) {
for (int z = -radius; z <= radius; z++) {
int chunkX = centerX + x;
int chunkZ = centerZ + z;
raiseDiscoveredChunkFlag(world, chunkX, chunkZ);
}
}
}));
}
} catch (Throwable e) {
IrisLogging.reportError(e);
} finally {
chunkDiscoveryScanScheduled.set(false);
}
}, () -> chunkDiscoveryScanScheduled.set(false)));
if (!scheduled) {
chunkDiscoveryScanScheduled.set(false);
}
}
private void raiseDiscoveredChunkFlag(World world, int chunkX, int chunkZ) {
if (world == null) {
return;
}
if (!J.isFolia()) {
manager.getMantle().getChunk(chunkX, chunkZ).flag(MantleFlag.DISCOVERED, true);
return;
}
long key = Cache.key(chunkX, chunkZ);
if (!discoveredFlagQueue.add(key)) {
return;
}
J.a(manager.managedTask("bukkit_world_manager_discovered_flag", () -> {
try {
Mantle<Matter> mantle = manager.getMantle();
if (!mantle.hasFlag(chunkX, chunkZ, MantleFlag.DISCOVERED)) {
mantle.flag(chunkX, chunkZ, MantleFlag.DISCOVERED, true);
}
} catch (Throwable e) {
IrisLogging.reportError(e);
} finally {
discoveredFlagQueue.remove(key);
}
}, () -> discoveredFlagQueue.remove(key)));
}
void updateChunks() {
World world = BukkitWorldBinding.world(manager.getEngine().getWorld());
if (world == null) {
return;
}
if (manager.entitySpawner.isPregenActiveForThisWorld()) {
return;
}
if (!chunkUpdateScanScheduled.compareAndSet(false, true)) {
return;
}
boolean scheduled = J.runGlobal(manager.managedTask(
"bukkit_world_manager_update_chunks",
() -> updateChunksOnGlobal(world),
() -> chunkUpdateScanScheduled.set(false)));
if (!scheduled) {
chunkUpdateScanScheduled.set(false);
}
}
private void updateChunksOnGlobal(World world) {
try {
if (manager.getEngine().isClosed() || !world.equals(BukkitWorldBinding.world(manager.getEngine().getWorld()))) {
return;
}
List<Player> players = new ArrayList<>(world.getPlayers());
manager.playersPresent = !players.isEmpty();
manager.loadedChunkCount = world.getLoadedChunks().length;
for (Player player : players) {
if (player == null) {
continue;
}
J.runEntity(player, manager.managedTask(
"bukkit_world_manager_player_chunk_updates",
() -> schedulePlayerChunkUpdates(world, player)));
}
scheduleForcedChunkUpdates(world);
} catch (Throwable e) {
IrisLogging.reportError(e);
} finally {
chunkUpdateScanScheduled.set(false);
}
}
private void schedulePlayerChunkUpdates(World world, Player player) {
if (!player.isOnline() || !world.equals(player.getWorld())) {
return;
}
int centerX = PowerOfTwoCoordinates.blockToChunkFloor(player.getLocation().getBlockX());
int centerZ = PowerOfTwoCoordinates.blockToChunkFloor(player.getLocation().getBlockZ());
int radius = 1;
for (int x = -radius; x <= radius; x++) {
for (int z = -radius; z <= radius; z++) {
scheduleChunkUpdate(world, centerX + x, centerZ + z);
}
}
}
private void scheduleForcedChunkUpdates(World world) {
List<Position2> forcedChunks = new ArrayList<>();
for (Chunk chunk : world.getForceLoadedChunks()) {
forcedChunks.add(new Position2(chunk.getX(), chunk.getZ()));
}
forcedChunks.sort(Comparator.comparingInt(Position2::getX).thenComparingInt(Position2::getZ));
int forcedChunkCount = forcedChunks.size();
if (forcedChunkCount == 0) {
forcedChunkUpdateCursor = 0;
return;
}
int updateCount = Math.min(forcedChunkCount, MAX_FORCED_CHUNK_UPDATES);
int start = Math.floorMod(forcedChunkUpdateCursor, forcedChunkCount);
for (int i = 0; i < updateCount; i++) {
Position2 chunk = forcedChunks.get((start + i) % forcedChunkCount);
scheduleChunkUpdate(world, chunk.getX(), chunk.getZ());
}
forcedChunkUpdateCursor = (start + updateCount) % forcedChunkCount;
}
private void scheduleChunkUpdate(World world, int chunkX, int chunkZ) {
long key = Cache.key(chunkX, chunkZ);
if (!chunkUpdateQueue.add(key)) {
return;
}
try {
boolean scheduled = J.runRegion(world, chunkX, chunkZ, manager.managedTask("bukkit_world_manager_chunk_update", () -> {
try {
updateChunkRegion(world, chunkX, chunkZ);
} finally {
chunkUpdateQueue.remove(key);
}
}, () -> chunkUpdateQueue.remove(key)));
if (!scheduled) {
chunkUpdateQueue.remove(key);
}
} catch (Throwable e) {
chunkUpdateQueue.remove(key);
IrisLogging.reportError(e);
}
}
private void updateChunkRegion(World world, int chunkX, int chunkZ) {
if (world == null || !world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
return;
}
Chunk chunk = world.getChunkAt(chunkX, chunkZ);
if (IrisSettings.get().getWorld().isPostLoadBlockUpdates()) {
if (!manager.getMantle().isChunkLoaded(chunkX, chunkZ)) {
warmupMantleChunkAsync(chunkX, chunkZ);
return;
}
EngineBukkitOps.updateChunk(manager.getEngine(), chunk);
}
if (!manager.entitySpawner.isEntitySpawningEnabledForCurrentWorld()) {
return;
}
if (!IrisSettings.get().getWorld().isMarkerEntitySpawningSystem()) {
return;
}
if (!J.isFolia() && !manager.getMantle().isChunkLoaded(chunkX, chunkZ)) {
warmupMantleChunkAsync(chunkX, chunkZ);
return;
}
raiseInitialSpawnMarkerFlag(world, chunkX, chunkZ, () -> {
int delay = RNG.r.i(5, 200);
J.runRegion(world, chunkX, chunkZ, manager.managedTask("bukkit_world_manager_initial_spawn_followup", () -> {
if (!world.isChunkLoaded(chunkX, chunkZ)) {
return;
}
manager.entitySpawner.spawnIn(world.getChunkAt(chunkX, chunkZ), true);
}), delay);
Chunk markerChunk = world.getChunkAt(chunkX, chunkZ);
manager.markerScanner.forEachMarkerSpawner(markerChunk, (block, spawners) -> {
IrisSpawner s = new KList<>(spawners).getRandom();
if (s == null) {
return;
}
manager.entitySpawner.spawn(block, s, true);
});
});
}
void raiseInitialSpawnMarkerFlag(World world, int chunkX, int chunkZ, Runnable onFirstRaise) {
if (world == null || onFirstRaise == null) {
return;
}
if (!J.isFolia()) {
manager.getMantle().raiseFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED_MARKER, onFirstRaise);
return;
}
long key = Cache.key(chunkX, chunkZ);
if (!markerFlagQueue.add(key)) {
return;
}
J.a(manager.managedTask("bukkit_world_manager_spawn_marker_flag", () -> {
boolean raised = false;
try {
Mantle<Matter> mantle = manager.getMantle();
if (!mantle.hasFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED_MARKER)) {
mantle.flag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED_MARKER, true);
raised = true;
}
} catch (Throwable e) {
IrisLogging.reportError(e);
} finally {
markerFlagQueue.remove(key);
}
if (!raised) {
return;
}
J.runRegion(world, chunkX, chunkZ, manager.managedTask("bukkit_world_manager_spawn_marker_callback", () -> {
if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
return;
}
onFirstRaise.run();
}));
}, () -> markerFlagQueue.remove(key)));
}
void warmupMantleChunkAsync(int chunkX, int chunkZ) {
long key = Cache.key(chunkX, chunkZ);
if (!mantleWarmupQueue.add(key)) {
return;
}
J.a(manager.managedTask("bukkit_world_manager_mantle_warmup", () -> {
try {
manager.getMantle().getChunk(chunkX, chunkZ);
} catch (Throwable e) {
IrisLogging.reportError(e);
} finally {
mantleWarmupQueue.remove(key);
}
}, () -> mantleWarmupQueue.remove(key)));
}
Position2[] getLoadedChunkPositionsSnapshot(World world) {
if (world == null) {
return new Position2[0];
}
CompletableFuture<Position2[]> future = new CompletableFuture<>();
boolean scheduled = J.runGlobal(() -> {
try {
Chunk[] chunks = world.getLoadedChunks();
Position2[] positions = new Position2[chunks.length];
for (int i = 0; i < chunks.length; i++) {
positions[i] = new Position2(chunks[i].getX(), chunks[i].getZ());
}
manager.loadedChunkCount = positions.length;
future.complete(positions);
} catch (Throwable e) {
future.completeExceptionally(e);
}
});
if (!scheduled) {
return new Position2[0];
}
try {
return future.get(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return new Position2[0];
} catch (ExecutionException | TimeoutException e) {
IrisLogging.reportError(e);
return new Position2[0];
}
}
}
@@ -0,0 +1,558 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.object.IRare;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisEntitySpawn;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisSpawner;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.plugin.Chunks;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.math.PowerOfTwoCoordinates;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.math.RNG;
import lombok.Data;
import org.bukkit.Chunk;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Ambient and marker entity spawning for a Bukkit Iris world. Every count and spawn hops onto the
* thread that owns the data it reads (global for whole-world scans, entity for Folia candidate
* scans, region for the chunk being populated) and spawning is paused whenever a count could not
* be completed, so an incomplete saturation reading can never authorize a spawn.
*/
final class WorldEntitySpawner {
private final IrisWorldManager manager;
private final AtomicInteger actuallySpawned = new AtomicInteger();
private final AtomicBoolean entityCountWarningReported = new AtomicBoolean();
private final AtomicBoolean entityCountErrorReported = new AtomicBoolean();
private int cooldown = 0;
WorldEntitySpawner(IrisWorldManager manager) {
this.manager = manager;
}
boolean onAsyncTick() {
if (manager.getEngine().isClosing() || manager.getEngine().isClosed()) {
return false;
}
if (isPregenActiveForThisWorld()) {
J.sleep(500);
return false;
}
actuallySpawned.set(0);
if (!manager.getEngine().getWorld().hasPlatformWorld()) {
IrisLogging.debug("Can't spawn. No real world");
J.sleep(5000);
return false;
}
if (manager.cl.flip()) {
try {
World realWorld = BukkitWorldBinding.world(manager.getEngine().getWorld());
if (realWorld == null) {
manager.entityCount = 0;
manager.entityCountValid = false;
} else if (J.isFolia()) {
Integer count = getFoliaLivingEntityCount(realWorld);
if (count != null) {
manager.entityCount = count;
manager.entityCountValid = true;
resetEntityCountFailures();
} else {
manager.entityCountValid = false;
}
} else {
CompletableFuture<Integer> future = new CompletableFuture<>();
boolean scheduled = J.runGlobal(() -> {
try {
int count = 0;
for (Entity entity : realWorld.getEntities()) {
if (entity instanceof LivingEntity && !entity.isDead()) {
count++;
}
}
future.complete(count);
} catch (Throwable ex) {
future.completeExceptionally(ex);
}
});
if (scheduled) {
manager.entityCount = future.get(2, TimeUnit.SECONDS);
manager.entityCountValid = true;
resetEntityCountFailures();
} else {
reportEntityCountFailure("Unable to schedule the global entity count; pausing Iris entity spawning until a complete count is available.", null);
}
}
} catch (InterruptedException e) {
manager.entityCountValid = false;
Thread.currentThread().interrupt();
return false;
} catch (TimeoutException e) {
reportEntityCountFailure("Timed out while counting entities; pausing Iris entity spawning until a complete count is available.", null);
} catch (ExecutionException e) {
Throwable cause = e.getCause() == null ? e : e.getCause();
reportEntityCountFailure("Failed to count entities; pausing Iris entity spawning until a complete count is available.", cause);
} catch (Throwable e) {
reportEntityCountFailure("Failed to count entities; pausing Iris entity spawning until a complete count is available.", e);
}
}
if (!manager.entityCountValid) {
return false;
}
double epx = manager.getEntitySaturation();
if (epx > IrisSettings.get().getWorld().getTargetSpawnEntitiesPerChunk()) {
IrisLogging.debug("Can't spawn. The entity per chunk ratio is at " + Form.pc(epx, 2) + " > 100% (total entities " + manager.entityCount + ")");
J.sleep(5000);
return false;
}
int spawnBuffer = RNG.r.i(2, 12);
World world = BukkitWorldBinding.world(manager.getEngine().getWorld());
if (world == null) {
return false;
}
Position2[] cc = manager.chunkMaintenance.getLoadedChunkPositionsSnapshot(world);
while (spawnBuffer-- > 0) {
if (manager.getEngine().isClosing() || manager.getEngine().isClosed()) {
return actuallySpawned.get() > 0;
}
if (cc.length == 0) {
IrisLogging.debug("Can't spawn. No chunks!");
return false;
}
Position2 c = cc[RNG.r.nextInt(cc.length)];
if (!spawnChunkSafely(world, c.getX(), c.getZ(), false)) {
return actuallySpawned.get() > 0;
}
}
return actuallySpawned.get() > 0;
}
boolean isPregenActiveForThisWorld() {
World world = BukkitWorldBinding.world(manager.getEngine().getWorld());
if (world == null) {
return false;
}
if (IrisToolbelt.isWorldMaintenanceActive(world)) {
return true;
}
PregeneratorJob job = PregeneratorJob.getInstance();
if (job == null) {
return false;
}
return job.targetsWorldIdentity(WorldIdentity.serialize(world));
}
private Integer getFoliaLivingEntityCount(World world) {
CompletableFuture<List<Player>> playerFuture = new CompletableFuture<>();
boolean scheduled = J.runGlobal(() -> {
try {
playerFuture.complete(new ArrayList<>(world.getPlayers()));
} catch (Throwable e) {
playerFuture.completeExceptionally(e);
}
});
if (!scheduled) {
reportEntityCountFailure("Unable to schedule the Folia player snapshot; pausing Iris entity spawning until a complete count is available.", null);
return null;
}
List<Player> players;
try {
players = playerFuture.get(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
manager.entityCountValid = false;
Thread.currentThread().interrupt();
return null;
} catch (TimeoutException e) {
reportEntityCountFailure("Timed out while reading the Folia player snapshot; pausing Iris entity spawning until a complete count is available.", null);
return null;
} catch (ExecutionException e) {
Throwable cause = e.getCause() == null ? e : e.getCause();
reportEntityCountFailure("Failed to read the Folia player snapshot; pausing Iris entity spawning until a complete count is available.", cause);
return null;
}
Map<String, Entity> candidates = new ConcurrentHashMap<>();
AtomicBoolean incomplete = new AtomicBoolean();
AtomicReference<Throwable> failure = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(players.size());
for (Player player : players) {
if (player == null) {
latch.countDown();
continue;
}
if (!J.runEntity(player, () -> {
try {
if (!player.isOnline() || !world.equals(player.getWorld())) {
return;
}
candidates.put(player.getUniqueId().toString(), player);
for (Entity nearby : player.getNearbyEntities(64, 64, 64)) {
if (nearby != null) {
candidates.put(nearby.getUniqueId().toString(), nearby);
}
}
} catch (Throwable e) {
incomplete.set(true);
failure.compareAndSet(null, e);
} finally {
latch.countDown();
}
})) {
incomplete.set(true);
latch.countDown();
}
}
if (!awaitEntityTasks(latch, 2, TimeUnit.SECONDS) || incomplete.get()) {
if (!Thread.currentThread().isInterrupted()) {
reportEntityCountFailure("The Folia entity candidate scan was incomplete; pausing Iris entity spawning until a complete count is available.", failure.get());
}
return null;
}
AtomicInteger count = new AtomicInteger();
incomplete.set(false);
failure.set(null);
CountDownLatch entityLatch = new CountDownLatch(candidates.size());
for (Entity entity : candidates.values()) {
if (!J.runEntity(entity, () -> {
try {
if (entity instanceof LivingEntity && world.equals(entity.getWorld()) && !entity.isDead()) {
count.incrementAndGet();
}
} catch (Throwable e) {
incomplete.set(true);
failure.compareAndSet(null, e);
} finally {
entityLatch.countDown();
}
})) {
incomplete.set(true);
entityLatch.countDown();
}
}
if (!awaitEntityTasks(entityLatch, 2, TimeUnit.SECONDS) || incomplete.get()) {
if (!Thread.currentThread().isInterrupted()) {
reportEntityCountFailure("The Folia entity validation scan was incomplete; pausing Iris entity spawning until a complete count is available.", failure.get());
}
return null;
}
return count.get();
}
static boolean awaitEntityTasks(CountDownLatch latch, long timeout, TimeUnit unit) {
try {
return latch.await(timeout, unit);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
private boolean spawnChunkSafely(World world, int chunkX, int chunkZ, boolean initial) {
if (world == null) {
return false;
}
CompletableFuture<Void> future = new CompletableFuture<>();
AtomicBoolean failureReported = new AtomicBoolean();
future.whenComplete((ignored, failure) -> {
if (failure != null) {
reportSpawnFailure(chunkX, chunkZ, failure, failureReported);
}
});
boolean scheduled;
try {
scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
try {
if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
future.complete(null);
return;
}
spawnIn(world.getChunkAt(chunkX, chunkZ), initial);
future.complete(null);
} catch (Throwable e) {
future.completeExceptionally(e);
}
});
} catch (Throwable e) {
IrisLogging.reportError("Failed to schedule an Iris entity spawn for chunk " + chunkX + "," + chunkZ + ".", e);
return false;
}
if (!scheduled) {
IrisLogging.debug("Skipped Iris entity spawning because the region task was not accepted for chunk " + chunkX + "," + chunkZ + ".");
return false;
}
try {
future.get(5, TimeUnit.SECONDS);
return true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} catch (TimeoutException e) {
IrisLogging.warn("Timed out waiting for Iris entity spawning in chunk %d,%d; deferring the remaining spawn buffer.", chunkX, chunkZ);
return false;
} catch (ExecutionException e) {
Throwable cause = e.getCause() == null ? e : e.getCause();
reportSpawnFailure(chunkX, chunkZ, cause, failureReported);
return false;
}
}
private void reportEntityCountFailure(String message, Throwable error) {
manager.entityCountValid = false;
if (error != null) {
if (entityCountErrorReported.compareAndSet(false, true)) {
IrisLogging.reportError(message, error);
}
return;
}
if (entityCountWarningReported.compareAndSet(false, true)) {
IrisLogging.warn(message);
}
}
private void resetEntityCountFailures() {
entityCountWarningReported.set(false);
entityCountErrorReported.set(false);
}
private void reportSpawnFailure(int chunkX, int chunkZ, Throwable failure, AtomicBoolean failureReported) {
if (!failureReported.compareAndSet(false, true)) {
return;
}
Throwable cause = failure.getCause() == null ? failure : failure.getCause();
IrisLogging.reportError("Failed to spawn Iris entities in chunk " + chunkX + "," + chunkZ + ".", cause);
}
void spawnIn(Chunk c, boolean initial) {
if (manager.getEngine().isClosed()) {
return;
}
if (!isEntitySpawningEnabledForCurrentWorld()) {
return;
}
IrisComplex complex = manager.getEngine().getComplex();
if (complex == null) {
return;
}
if (IrisSettings.get().getWorld().isMarkerEntitySpawningSystem()) {
manager.markerScanner.forEachMarkerSpawner(c, (block, spawners) -> {
IrisSpawner s = new KList<>(spawners).getRandom();
if (s == null) {
return;
}
spawn(block, s, false);
J.runRegion(c.getWorld(), c.getX(), c.getZ(), manager.managedTask(
"bukkit_world_manager_marker_spawn_followup",
() -> manager.chunkMaintenance.raiseInitialSpawnMarkerFlag(c.getWorld(), c.getX(), c.getZ(),
() -> spawn(block, s, true))));
});
}
if (!IrisSettings.get().getWorld().isAmbientEntitySpawningSystem()) {
return;
}
//@builder
Predicate<IrisSpawner> filter = i -> i.canSpawn(manager.getEngine(), c.getX(), c.getZ());
ChunkCounter counter = new ChunkCounter(c.getEntities());
IrisBiome biome = EngineBukkitOps.getSurfaceBiome(manager.getEngine(), c);
IrisEntitySpawn v = spawnRandomly(Stream.concat(manager.getData().getSpawnerLoader()
.loadAll(manager.getDimension().getEntitySpawners())
.shuffleCopy(RNG.r)
.stream()
.filter(filter)
.filter((i) -> i.isValid(biome)),
Stream.concat(manager.getData()
.getSpawnerLoader()
.loadAll(manager.getEngine().getRegion(PowerOfTwoCoordinates.chunkToBlock(c.getX()), PowerOfTwoCoordinates.chunkToBlock(c.getZ())).getEntitySpawners())
.shuffleCopy(RNG.r)
.stream()
.filter(filter),
manager.getData().getSpawnerLoader()
.loadAll(manager.getEngine().getSurfaceBiome(PowerOfTwoCoordinates.chunkToBlock(c.getX()), PowerOfTwoCoordinates.chunkToBlock(c.getZ())).getEntitySpawners())
.shuffleCopy(RNG.r)
.stream()
.filter(filter)))
.filter(counter)
.flatMap((i) -> stream(i, initial))
.collect(Collectors.toList()))
.getRandom();
//@done
if (v == null || v.getReferenceSpawner() == null)
return;
spawn(c, v);
}
private void spawn(Chunk c, IrisEntitySpawn i) {
IrisSpawner ref = i.getReferenceSpawner();
int s = i.spawn(manager.getEngine(), c, RNG.r);
actuallySpawned.addAndGet(s);
if (s > 0) {
ref.spawn(manager.getEngine(), c.getX(), c.getZ());
}
}
private void spawn(IrisPosition pos, IrisEntitySpawn i) {
IrisSpawner ref = i.getReferenceSpawner();
if (!ref.canSpawn(manager.getEngine(), PowerOfTwoCoordinates.blockToChunkFloor(pos.getX()), PowerOfTwoCoordinates.blockToChunkFloor(pos.getZ())))
return;
int s = i.spawn(manager.getEngine(), pos, RNG.r);
actuallySpawned.addAndGet(s);
if (s > 0) {
ref.spawn(manager.getEngine(), PowerOfTwoCoordinates.blockToChunkFloor(pos.getX()), PowerOfTwoCoordinates.blockToChunkFloor(pos.getZ()));
}
}
void spawn(IrisPosition block, IrisSpawner spawner, boolean initial) {
if (manager.getEngine().isClosed()) {
return;
}
if (spawner == null) {
return;
}
KList<IrisEntitySpawn> s = initial ? spawner.getInitialSpawns() : spawner.getSpawns();
if (s.isEmpty()) {
return;
}
IrisEntitySpawn ss = spawnRandomly(s).getRandom();
ss.setReferenceSpawner(spawner);
ss.setReferenceMarker(spawner.getReferenceMarker());
spawn(block, ss);
}
private Stream<IrisEntitySpawn> stream(IrisSpawner s, boolean initial) {
for (IrisEntitySpawn i : initial ? s.getInitialSpawns() : s.getSpawns()) {
i.setReferenceSpawner(s);
i.setReferenceMarker(s.getReferenceMarker());
}
return (initial ? s.getInitialSpawns() : s.getSpawns()).stream();
}
boolean isEntitySpawningEnabledForCurrentWorld() {
if (!manager.getEngine().isStudio()) {
return true;
}
return IrisSettings.get().getStudio().isEntitySpawning();
}
private KList<IrisEntitySpawn> spawnRandomly(List<IrisEntitySpawn> types) {
KList<IrisEntitySpawn> rarityTypes = new KList<>();
int totalRarity = 0;
for (IrisEntitySpawn i : types) {
totalRarity += IRare.get(i);
}
for (IrisEntitySpawn i : types) {
rarityTypes.addMultiple(i, totalRarity / IRare.get(i));
}
return rarityTypes;
}
@Data
private static class ChunkCounter implements Predicate<IrisSpawner> {
private final Entity[] entities;
private transient int index = 0;
private transient int count = 0;
@Override
public boolean test(IrisSpawner spawner) {
int max = spawner.getMaxEntitiesPerChunk();
if (max <= count)
return false;
while (index < entities.length) {
if (entities[index++] instanceof LivingEntity) {
if (++count >= max)
return false;
}
}
return true;
}
}
}
@@ -0,0 +1,104 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.RuntimeUiMessages;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.util.common.scheduling.jobs.QueueJob;
import art.arcane.volmlib.util.collection.KList;
import io.papermc.lib.PaperLib;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerTeleportEvent;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
/**
* Cancels a teleport, loads the destination chunks off the main thread and then replays the teleport
* on the thread that owns the player. The replay sets the manager's ignore flag so the reissued
* teleport is not intercepted again.
*/
final class WorldTeleportWarmup {
private final IrisWorldManager manager;
WorldTeleportWarmup(IrisWorldManager manager) {
this.manager = manager;
}
void teleportAsync(PlayerTeleportEvent e) {
e.setCancelled(true);
warmupAreaAsync(e.getPlayer(), e.getTo(), () -> J.runEntity(e.getPlayer(), manager.managedTask(
"bukkit_world_manager_teleport",
() -> {
manager.ignoreTeleport().set(true);
e.getPlayer().teleport(e.getTo(), e.getCause());
manager.ignoreTeleport().set(false);
})));
}
private void warmupAreaAsync(Player player, Location to, Runnable r) {
J.a(manager.managedTask("bukkit_world_manager_teleport_warmup", () -> {
int viewDistance = 2;
KList<Future<Chunk>> futures = new KList<>();
for (int i = -viewDistance; i <= viewDistance; i++) {
for (int j = -viewDistance; j <= viewDistance; j++) {
int finalJ = j;
int finalI = i;
if (to.getWorld().isChunkLoaded((to.getBlockX() >> 4) + i, (to.getBlockZ() >> 4) + j)) {
futures.add(CompletableFuture.completedFuture(null));
continue;
}
futures.add(MultiBurst.burst.completeValue(()
-> PaperLib.getChunkAtAsync(to.getWorld(),
(to.getBlockX() >> 4) + finalI,
(to.getBlockZ() >> 4) + finalJ,
true, false).get()));
}
}
new QueueJob<Future<Chunk>>() {
@Override
public void execute(Future<Chunk> chunkFuture) {
try {
chunkFuture.get();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
IrisLogging.debug("Chunk warmup interrupted while loading async teleport chunk.");
} catch (ExecutionException ex) {
IrisLogging.reportError(ex);
}
}
@Override
public String getName() {
return IrisLanguage.text(RuntimeUiMessages.JOB_LOADING_CHUNKS);
}
}.queue(futures).execute(new VolmitSender(player), true, r);
}));
}
}
@@ -23,19 +23,25 @@ import art.arcane.iris.engine.framework.EngineAssignedActuator;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.context.ChunkedDataCache;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterBiomeInject;
import art.arcane.volmlib.util.matter.slices.BiomeInjectMatter;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome;
public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
private final RNG rng;
private final ChronoLatch cl = new ChronoLatch(5000);
private final KMap<String, ResolvedBiome> resolvedBiomes = new KMap<>();
public IrisBiomeActuator(Engine engine) {
super(engine, "Biome");
@@ -47,36 +53,65 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
public void onActuate(int x, int z, Hunk<PlatformBiome> h, boolean multicore, ChunkContext context) {
try {
PrecisionStopwatch p = PrecisionStopwatch.start();
for (int xf = 0; xf < h.getWidth(); xf++) {
int width = h.getWidth();
int depth = h.getDepth();
int height = h.getHeight();
Engine engine = getEngine();
Mantle<Matter> mantle = engine.getMantle().getMantle();
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
for (int xf = 0; xf < width; xf++) {
IrisBiome ib;
for (int zf = 0; zf < h.getDepth(); zf++) {
ib = context.getBiome().get(xf, zf);
MatterBiomeInject matter;
PlatformBiome biome;
for (int zf = 0; zf < depth; zf++) {
ib = biomeCache.get(xf, zf);
String key;
if (ib.isCustom()) {
IrisBiomeCustom custom = ib.getCustomBiome(rng, getEngine(), x + xf, 0, z + zf);
String key = getDimension().getLoadKey() + ":" + custom.getId();
biome = IrisPlatforms.get().registries().biome(key);
matter = BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(key));
IrisBiomeCustom custom = ib.getCustomBiome(rng, engine, x + xf, 0, z + zf);
key = getDimension().getLoadKey() + ":" + custom.getId();
} else {
String skyKey = ib.getSkyBiomeKey(rng, getEngine(), x + xf, 0, z + zf);
biome = IrisPlatforms.get().registries().biome(skyKey);
matter = BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(skyKey));
key = ib.getSkyBiomeKey(rng, engine, x + xf, 0, z + zf);
}
ResolvedBiome resolved = resolve(key);
PlatformBiome biome = resolved.biome();
if (biome != null) {
for (int yf = 0; yf < h.getHeight(); yf++) {
h.set(xf, yf, zf, biome);
}
h.set(xf, 0, zf, xf, height - 1, zf, biome);
}
getEngine().getMantle().getMantle().set(x + xf, 0, z + zf, matter);
mantle.set(x + xf, 0, z + zf, resolved.matter());
}
}
getEngine().getMetrics().getBiome().put(p.getMilliseconds());
engine.getMetrics().getBiome().put(p.getMilliseconds());
} catch (Throwable e) {
e.printStackTrace();
}
}
/**
* The registry lookup and the biome id only depend on the scatter resolved key, so cache the pair
* instead of paying two string keyed lookups per column. Unresolved biomes are never cached, so a
* biome registered later still resolves.
*/
private ResolvedBiome resolve(String key) {
ResolvedBiome cached = key == null ? null : resolvedBiomes.get(key);
if (cached != null) {
return cached;
}
IrisPlatform platform = IrisPlatforms.get();
PlatformBiome biome = platform.registries().biome(key);
ResolvedBiome resolved = new ResolvedBiome(biome, BiomeInjectMatter.get(platform.biomeWriter().biomeIdFor(key)));
if (key != null && biome != null) {
resolvedBiomes.put(key, resolved);
}
return resolved;
}
private record ResolvedBiome(PlatformBiome biome, MatterBiomeInject matter) {
}
}
@@ -84,128 +84,6 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
terrainSliverOptimized(x, z, xf, h, context);
}
@BlockCoordinates
private void terrainSliverLegacy(int x, int z, int xf, Hunk<PlatformBlockState> h, ChunkContext context) {
int zf, realX, realZ, hf, he;
IrisBiome biome;
IrisRegion region;
int clampedFluidHeight = Math.min(h.getHeight(), getDimension().getFluidHeight());
for (zf = 0; zf < h.getDepth(); zf++) {
realX = xf + x;
realZ = zf + z;
biome = context.getBiome().get(xf, zf);
region = context.getRegion().get(xf, zf);
he = Math.min(h.getHeight(), context.getRoundedHeight(xf, zf));
hf = Math.max(clampedFluidHeight, he);
if (hf < 0) {
continue;
}
KList<PlatformBlockState> blocks = null;
KList<PlatformBlockState> fblocks = null;
int depth, fdepth;
for (int i = hf; i >= 0; i--) {
if (i >= h.getHeight()) {
continue;
}
if (i == 0) {
if (getDimension().isBedrock()) {
h.setRaw(xf, i, zf, BEDROCK);
lastBedrock = i;
continue;
}
}
PlatformBlockState ore = biome.generateOres(realX, i, realZ, rng, getData(), true);
ore = ore == null ? region.generateOres(realX, i, realZ, rng, getData(), true) : ore;
ore = ore == null ? getDimension().generateOres(realX, i, realZ, rng, getData(), true) : ore;
if (ore != null) {
h.setRaw(xf, i, zf, ore);
continue;
}
if (i > he && i <= hf) {
fdepth = hf - i;
if (fblocks == null) {
fblocks = biome.generateSeaLayers(realX, realZ, rng, hf - he, getData());
}
if (fblocks.hasIndex(fdepth)) {
h.setRaw(xf, i, zf, fblocks.get(fdepth));
continue;
}
h.setRaw(xf, i, zf, context.getFluid().get(xf, zf));
continue;
}
if (i <= he) {
depth = he - i;
if (blocks == null) {
blocks = biome.generateLayers(getDimension(), realX, realZ, rng,
he,
he,
getData(),
getComplex());
}
if (blocks.hasIndex(depth)) {
h.setRaw(xf, i, zf, blocks.get(depth));
continue;
}
ore = biome.generateOres(realX, i, realZ, rng, getData(), false);
ore = ore == null ? region.generateOres(realX, i, realZ, rng, getData(), false) : ore;
ore = ore == null ? getDimension().generateOres(realX, i, realZ, rng, getData(), false) : ore;
if (ore != null) {
h.setRaw(xf, i, zf, ore);
} else {
h.setRaw(xf, i, zf, context.getRock().get(xf, zf));
}
}
}
UpperDimensionContext upperContext = getEngine().getUpperContext();
if (upperContext != null) {
int chunkHeight = h.getHeight();
boolean bedrockEnabled = getDimension().isBedrock();
int rawUpperSurface = upperContext.getUpperSurfaceY(realX, realZ);
int upperGap = getDimension().getUpperDimensionGap();
int upperSurfaceY = Math.max(rawUpperSurface, he + upperGap);
if (upperSurfaceY < chunkHeight - 1) {
IrisBiome upperBiome = upperContext.getUpperBiome(realX, realZ);
PlatformBlockState upperRock = upperContext.getRockBlock(realX, realZ);
int upperThickness = chunkHeight - 1 - upperSurfaceY;
KList<PlatformBlockState> upperBlocks = upperBiome != null
? upperBiome.generateLayers(upperContext.getDimension(),
realX, realZ, rng, upperThickness, upperThickness,
upperContext.getData(), getComplex())
: null;
for (int y = chunkHeight - 1; y >= upperSurfaceY; y--) {
if (y == chunkHeight - 1 && bedrockEnabled) {
h.setRaw(xf, y, zf, BEDROCK);
continue;
}
int depthFromFace = y - upperSurfaceY;
if (upperBlocks != null && upperBlocks.hasIndex(depthFromFace)) {
h.setRaw(xf, y, zf, upperBlocks.get(depthFromFace));
} else {
h.setRaw(xf, y, zf, upperRock);
}
}
}
}
}
}
@BlockCoordinates
private void terrainSliverOptimized(int x, int z, int xf, Hunk<PlatformBlockState> h, ChunkContext context) {
int chunkHeight = h.getHeight();
@@ -21,36 +21,32 @@ package art.arcane.iris.engine.data.cache;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.function.NastySupplier;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;
public class AtomicCache<T> {
private transient final AtomicReference<T> t;
private transient final AtomicBoolean set;
private transient final ReentrantLock lock;
private static final Object NULL_VALUE = new Object();
private transient final Object initLock = new Object();
private transient final boolean nullSupport;
private transient volatile Object value;
public AtomicCache() {
this(false);
}
public AtomicCache(boolean nullSupport) {
set = nullSupport ? new AtomicBoolean() : null;
t = new AtomicReference<>();
lock = new ReentrantLock();
this.nullSupport = nullSupport;
}
public void reset() {
t.set(null);
if (nullSupport) {
set.set(false);
synchronized (initLock) {
value = null;
}
}
public T getIfPresent() {
return unwrap(value);
}
public T aquireNasty(NastySupplier<T> t) {
return aquire(() -> {
try {
@@ -73,35 +69,41 @@ public class AtomicCache<T> {
}
public T aquire(Supplier<T> t) {
if (this.t.get() != null) {
return this.t.get();
} else if (nullSupport && set.get()) {
return null;
Object v = value;
if (v != null) {
return unwrap(v);
}
lock.lock();
synchronized (initLock) {
v = value;
if (this.t.get() != null) {
lock.unlock();
return this.t.get();
} else if (nullSupport && set.get()) {
lock.unlock();
return null;
}
try {
this.t.set(t.get());
if (nullSupport) {
set.set(true);
if (v != null) {
return unwrap(v);
}
} catch (Throwable e) {
IrisLogging.error("Atomic cache failure!");
e.printStackTrace();
try {
T computed = t.get();
if (computed != null) {
value = computed;
return computed;
}
if (nullSupport) {
value = NULL_VALUE;
}
} catch (Throwable e) {
IrisLogging.error("Atomic cache failure!");
e.printStackTrace();
}
return null;
}
}
lock.unlock();
return this.t.get();
@SuppressWarnings("unchecked")
private T unwrap(Object v) {
return v == null || v == NULL_VALUE ? null : (T) v;
}
}
@@ -1,26 +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.engine.data.cache;
public interface Multicache {
<V> Cache<V> getCache(int id);
<V> Cache<V> createCache();
}
@@ -61,6 +61,21 @@ public class LinkedTerrainChunk implements TerrainChunk {
biomes[biomeIndex(x, y, z)] = (Biome) bio.nativeHandle();
}
/**
* Writes the whole vertical column in one pass, striding the flat biome array directly. Equivalent to
* calling setBiome for every y in [minHeight, maxHeight).
*/
public void fillBiomeColumn(int x, int z, PlatformBiome bio) {
Biome handle = (Biome) bio.nativeHandle();
int stride = CHUNK_SIZE * CHUNK_SIZE;
int index = (z & (CHUNK_SIZE - 1)) * CHUNK_SIZE + (x & (CHUNK_SIZE - 1));
for (int y = 0; y < biomeHeight; y++) {
biomes[index] = handle;
index += stride;
}
}
@Override
public int getMinHeight() {
return minHeight;
@@ -1,60 +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.engine.data.io;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
public interface Deserializer<T> {
T fromStream(InputStream stream) throws IOException;
default T fromFile(File file) throws IOException {
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) {
return fromStream(bis);
}
}
default T fromBytes(byte[] data) throws IOException {
ByteArrayInputStream stream = new ByteArrayInputStream(data);
return fromStream(stream);
}
default T fromResource(Class<?> clazz, String path) throws IOException {
try (InputStream stream = clazz.getClassLoader().getResourceAsStream(path)) {
if (stream == null) {
throw new IOException("resource \"" + path + "\" not found");
}
return fromStream(stream);
}
}
default T fromURL(URL url) throws IOException {
try (InputStream stream = url.openStream()) {
return fromStream(stream);
}
}
}
@@ -1,25 +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.engine.data.io;
@FunctionalInterface
public interface ExceptionBiFunction<T, U, R, E extends Exception> {
R accept(T t, U u) throws E;
}
@@ -1,25 +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.engine.data.io;
@FunctionalInterface
public interface ExceptionTriConsumer<T, U, V, E extends Exception> {
void accept(T t, U u, V v) throws E;
}
@@ -1,31 +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.engine.data.io;
public interface MaxDepthIO {
default int decrementMaxDepth(int maxDepth) {
if (maxDepth < 0) {
throw new IllegalArgumentException("negative maximum depth is not allowed");
} else if (maxDepth == 0) {
throw new MaxDepthReachedException("reached maximum depth of NBT structure");
}
return --maxDepth;
}
}
@@ -1,30 +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.engine.data.io;
/**
* Exception indicating that the maximum (de-)serialization depth has been reached.
*/
@SuppressWarnings("serial")
public class MaxDepthReachedException extends RuntimeException {
public MaxDepthReachedException(String msg) {
super(msg);
}
}
@@ -1,44 +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.engine.data.io;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public interface Serializer<T> {
void toStream(T object, OutputStream out) throws IOException;
default void toFile(T object, File file) throws IOException {
try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file))) {
toStream(object, bos);
}
}
default byte[] toBytes(T object) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
toStream(object, bos);
bos.close();
return bos.toByteArray();
}
}
@@ -1,55 +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.engine.data.io;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
public interface StringDeserializer<T> extends Deserializer<T> {
T fromReader(Reader reader) throws IOException;
default T fromString(String s) throws IOException {
return fromReader(new StringReader(s));
}
@Override
default T fromStream(InputStream stream) throws IOException {
try (Reader reader = new InputStreamReader(stream)) {
return fromReader(reader);
}
}
@Override
default T fromFile(File file) throws IOException {
try (Reader reader = new FileReader(file)) {
return fromReader(reader);
}
}
@Override
default T fromBytes(byte[] data) throws IOException {
return fromReader(new StringReader(new String(data)));
}
}
@@ -1,53 +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.engine.data.io;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.io.Writer;
public interface StringSerializer<T> extends Serializer<T> {
void toWriter(T object, Writer writer) throws IOException;
default String toString(T object) throws IOException {
Writer writer = new StringWriter();
toWriter(object, writer);
writer.flush();
return writer.toString();
}
@Override
default void toStream(T object, OutputStream stream) throws IOException {
Writer writer = new OutputStreamWriter(stream);
toWriter(object, writer);
writer.flush();
}
@Override
default void toFile(T object, File file) throws IOException {
try (Writer writer = new FileWriter(file)) {
toWriter(object, writer);
}
}
}
@@ -166,12 +166,10 @@ final class DecoratorCore {
}
if (!decorator.isForcePlace()) {
if (decorator.getWhitelist() != null
&& decorator.getWhitelist().stream().noneMatch(d -> d.getBlockData(irisData).equals(bdx))) {
if (decorator.getWhitelist() != null && !matchesPalette(decorator.getWhitelistArray(irisData), bdx)) {
return;
}
if (decorator.getBlacklist() != null
&& decorator.getBlacklist().stream().anyMatch(d -> d.getBlockData(irisData).equals(bdx))) {
if (decorator.getBlacklist() != null && matchesPalette(decorator.getBlacklistArray(irisData), bdx)) {
return;
}
}
@@ -203,6 +201,16 @@ final class DecoratorCore {
}
}
private static boolean matchesPalette(PlatformBlockState[] palette, PlatformBlockState surface) {
for (int i = 0; i < palette.length; i++) {
if (palette[i].equals(surface)) {
return true;
}
}
return false;
}
static void placeSingleAt(IrisDecorator decorator, int x, int z,
int realX, int height, int realZ, Hunk<PlatformBlockState> data,
RNG rng, IrisData irisData, boolean applyFixFaces, EngineMantle mantle) {
@@ -1,34 +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.engine.framework;
import art.arcane.iris.util.project.hunk.Hunk;
public abstract class EngineAssignedBiModifier<A, B> extends EngineAssignedComponent implements EngineBiModifier<A, B> {
public EngineAssignedBiModifier(Engine engine, String name) {
super(engine, name);
}
public abstract void onModify(int x, int z, Hunk<A> a, Hunk<B> b);
@Override
public void modify(int x, int z, Hunk<A> a, Hunk<B> b) {
onModify(x, z, a, b);
}
}
@@ -37,7 +37,19 @@ public abstract class EngineAssignedModifier<T> extends EngineAssignedComponent
onModify(x, z, output, multicore, context);
} catch (Throwable e) {
IrisLogging.error("Modifier Failure: " + getName());
e.printStackTrace();
IrisLogging.reportError(e);
// A failed modifier leaves a half-written chunk. Never continue as if it succeeded,
// let the chunk generation failure path handle it.
if (e instanceof Error error) {
throw error;
}
if (e instanceof RuntimeException runtime) {
throw runtime;
}
throw new IllegalStateException("Modifier Failure: " + getName(), e);
}
}
}
@@ -1,25 +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.engine.framework;
import art.arcane.iris.util.project.hunk.Hunk;
public interface EngineBiModifier<A, B> extends EngineComponent {
void modify(int x, int z, Hunk<A> a, Hunk<B> b);
}
@@ -1,58 +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.engine.framework;
import art.arcane.iris.spi.IrisLogging;
import com.google.gson.Gson;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.volmlib.util.io.IO;
import lombok.Data;
import java.io.File;
import java.io.IOException;
import java.util.List;
@Data
public class EngineData {
private String dimension;
private String lastVersion;
private List<IrisPosition> strongholdPositions;
public static EngineData load(File f) {
try {
f.getParentFile().mkdirs();
return new Gson().fromJson(IO.readAll(f), EngineData.class);
} catch (Throwable e) {
IrisLogging.reportError(e);
}
return new EngineData();
}
public void save(File f) {
try {
f.getParentFile().mkdirs();
IO.writeAll(f, new Gson().toJson(this));
} catch (IOException e) {
IrisLogging.reportError(e);
e.printStackTrace();
}
}
}
@@ -1,64 +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.engine.framework;
import art.arcane.iris.engine.data.chunk.TerrainChunk;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.project.hunk.Hunk;
import lombok.Data;
import java.util.concurrent.atomic.AtomicBoolean;
@Data
public class PregeneratedData {
private final Hunk<PlatformBlockState> blocks;
private final Hunk<PlatformBlockState> post;
private final Hunk<PlatformBiome> biomes;
private final AtomicBoolean postMod;
public PregeneratedData(int height) {
postMod = new AtomicBoolean(false);
blocks = Hunk.newAtomicHunk(16, height, 16);
biomes = Hunk.newAtomicHunk(16, height, 16);
Hunk<PlatformBlockState> p = Hunk.newMappedHunkSynced(16, height, 16);
post = p.trackWrite(postMod);
}
public Runnable inject(TerrainChunk tc) {
blocks.iterateSync((x, y, z, b) -> {
if (b != null) {
tc.setBlock(x, y, z, b);
}
PlatformBiome bf = biomes.get(x, y, z);
if (bf != null) {
tc.setBiome(x, y, z, bf);
}
});
if (postMod.get()) {
return () -> Hunk.view(tc).insertSoftly(0, 0, 0, post, (b) -> b == null || B.isAirOrFluid(b));
}
return () -> {
};
}
}
@@ -1,112 +0,0 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.math.Spiraler;
import art.arcane.iris.util.common.parallel.BurstExecutor;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import org.apache.commons.lang3.function.TriFunction;
import java.util.Collection;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
@FunctionalInterface
public interface ResultLocator<T> {
static void cancelSearch() {
if (LocatorCanceller.cancel != null) {
LocatorCanceller.cancel.run();
LocatorCanceller.cancel = null;
}
}
static ResultLocator<IrisObject> locateObject(Collection<String> keys) {
return (e, pos) -> {
Set<String> objects = e.getObjectsAt(pos.getX(), pos.getZ());
for (String object : objects) {
if (!keys.contains(object)) continue;
return e.getData().getObjectLoader().load(object);
}
return null;
};
}
T find(Engine e, Position2 chunkPos);
default <R> ResultLocator<R> then(TriFunction<Engine, Position2, T, R> filter) {
return (e, pos) -> {
var t = find(e, pos);
return t != null ? filter.apply(e, pos, t) : null;
};
}
default Future<Result<T>> find(Engine engine, Position2 pos, long timeout, Consumer<Integer> checks, boolean cancelable) throws WrongEngineBroException {
if (engine.isClosed()) {
throw new WrongEngineBroException();
}
cancelSearch();
return MultiBurst.burst.completeValue(() -> {
int tc = IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism()) * 17;
MultiBurst burst = MultiBurst.burst;
AtomicBoolean found = new AtomicBoolean(false);
AtomicReference<Result<T>> foundObj = new AtomicReference<>();
Position2 cursor = pos;
AtomicInteger searched = new AtomicInteger();
AtomicBoolean stop = new AtomicBoolean(false);
PrecisionStopwatch px = PrecisionStopwatch.start();
if (cancelable) LocatorCanceller.cancel = () -> stop.set(true);
AtomicReference<Position2> next = new AtomicReference<>(cursor);
Spiraler s = new Spiraler(100000, 100000, (x, z) -> next.set(new Position2(x, z)));
s.setOffset(cursor.getX(), cursor.getZ());
s.next();
while (!found.get() && !stop.get() && px.getMilliseconds() < timeout) {
BurstExecutor e = burst.burst(tc);
for (int i = 0; i < tc; i++) {
Position2 p = next.get();
s.next();
e.queue(() -> {
var o = find(engine, p);
if (o != null) {
if (foundObj.get() == null) {
foundObj.set(new Result<>(o, p));
}
found.set(true);
}
searched.incrementAndGet();
});
}
e.complete();
checks.accept(searched.get());
}
LocatorCanceller.cancel = null;
if (found.get() && foundObj.get() != null) {
return foundObj.get();
}
return null;
});
}
record Result<T>(T obj, Position2 pos) {
public int getBlockX() {
return (pos.getX() << 4) + 8;
}
public int getBlockZ() {
return (pos.getZ() << 4) + 8;
}
}
}
@@ -229,23 +229,6 @@ public interface EngineMantle extends MatterGenerator {
getMantle().set(x, y, z, UpdateMatter.ON);
}
@BlockCoordinates
default void dropCavernBlock(int x, int y, int z) {
Matter matter = getMantle().getChunk(x & 15, z & 15).get(y & 15);
if (matter != null) {
matter.slice(MatterCavern.class).set(x & 15, y & 15, z & 15, null);
}
}
default boolean queueRegenerate(int x, int z) {
return false; // TODO:
}
default boolean dequeueRegenerate(int x, int z) {
return false;// TODO:
}
default int getLoadedRegionCount() {
return getMantle().getLoadedRegionCount();
}
@@ -26,13 +26,11 @@ import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.link.Identifier;
import art.arcane.iris.core.tools.WorldMaintenance;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.IrisGeneratorStyle;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.TileData;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.function.Function3;
@@ -44,17 +42,15 @@ import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.matter.MatterSlice;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.common.scheduling.J;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import lombok.Data;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.B;
import org.bukkit.util.Vector;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReferenceArray;
import static art.arcane.iris.engine.mantle.EngineMantle.AIR;
@@ -62,19 +58,22 @@ import static art.arcane.iris.engine.mantle.EngineMantle.AIR;
public class MantleWriter implements IObjectPlacer, AutoCloseable {
private final EngineMantle engineMantle;
private final Mantle<Matter> mantle;
private final Map<Long, MantleChunk<Matter>> cachedChunks;
private final int radius;
private final int x;
private final int z;
private final int windowSide;
private final AtomicReferenceArray<MantleChunk<Matter>> window;
public MantleWriter(EngineMantle engineMantle, Mantle<Matter> mantle, int x, int z, int radius, boolean multicore) {
this.engineMantle = engineMantle;
this.mantle = mantle;
this.radius = radius * 2;
final int d = this.radius + 1;
this.cachedChunks = multicore ? new KMap<>(d * d, 0.75f, Math.max(32, Runtime.getRuntime().availableProcessors() * 4)) : new Long2ObjectOpenHashMap<>(d * d);
this.x = x;
this.z = z;
// Every coordinate acquireChunk accepts lives in this window, so a flat array replaces the
// boxed per-block map lookup on the placement and carve hot paths.
this.windowSide = (this.radius * 2) + 1;
this.window = new AtomicReferenceArray<>(windowSide * windowSide);
final boolean foliaMaintenance = J.isFolia()
&& WorldMaintenance.isWorldMaintenanceActive(engineMantle.getEngine().getWorld().identity());
@@ -82,18 +81,14 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
if (foliaMaintenance && IrisSettings.get().getGeneral().isDebug()) {
IrisLogging.info("MantleWriter using sequential chunk prefetch for maintenance regen at " + x + "," + z + ".");
}
Map<Long, MantleChunk<Matter>> map = multicore
? cachedChunks
: new KMap<Long, MantleChunk<Matter>>(d * d, 1f, parallelism);
mantle.getChunks(
x - radius,
x + radius,
z - radius,
z + radius,
parallelism,
(i, j, c) -> map.put(Cache.key(i, j), c.use())
this::storePrefetchedChunk
);
if (!multicore) cachedChunks.putAll(map);
}
private static Set<IrisPosition> getBallooned(Set<IrisPosition> vset, double radius) {
@@ -337,19 +332,48 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
@ChunkCoordinates
public MantleChunk<Matter> acquireChunk(int cx, int cz) {
if (cx < this.x - radius || cx > this.x + radius
|| cz < this.z - radius || cz > this.z + radius) {
int index = windowIndex(cx, cz);
if (index < 0) {
IrisLogging.error("Mantle Writer Accessed chunk out of bounds" + cx + "," + cz);
return null;
}
final Long key = Cache.key(cx, cz);
MantleChunk<Matter> chunk = cachedChunks.get(key);
if (chunk == null) {
chunk = mantle.getChunk(cx, cz).use();
MantleChunk<Matter> old = cachedChunks.put(key, chunk);
if (old != null) old.release();
while (true) {
MantleChunk<Matter> chunk = window.get(index);
if (chunk != null) {
return chunk;
}
// Losing this race must release our own use, never the winner's: the winner is already
// writing through the chunk it published.
MantleChunk<Matter> acquired = mantle.getChunk(cx, cz).use();
if (window.compareAndSet(index, null, acquired)) {
return acquired;
}
acquired.release();
}
}
@ChunkCoordinates
private int windowIndex(int cx, int cz) {
int localX = cx - x + radius;
int localZ = cz - z + radius;
if (localX < 0 || localX >= windowSide || localZ < 0 || localZ >= windowSide) {
return -1;
}
return (localX * windowSide) + localZ;
}
@ChunkCoordinates
private void storePrefetchedChunk(int cx, int cz, MantleChunk<Matter> chunk) {
int index = windowIndex(cx, cz);
if (index < 0) {
return;
}
if (!window.compareAndSet(index, null, chunk.use())) {
chunk.release();
}
return chunk;
}
@Override
@@ -934,10 +958,11 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
@Override
public void close() {
Iterator<MantleChunk<Matter>> iterator = cachedChunks.values().iterator();
while (iterator.hasNext()) {
iterator.next().release();
iterator.remove();
for (int index = 0; index < window.length(); index++) {
MantleChunk<Matter> chunk = window.getAndSet(index, null);
if (chunk != null) {
chunk.release();
}
}
}
}
@@ -1,6 +1,7 @@
package art.arcane.iris.engine.mantle;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
@@ -13,11 +14,18 @@ import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public interface MatterGenerator {
MultiBurst DISPATCHER = MultiBurst.burst;
ConcurrentHashMap<MatterTaskKey, CompletableFuture<Void>> IN_FLIGHT_COMPONENTS = new ConcurrentHashMap<>();
long COMPONENT_TASK_POLL_MS = 1000L;
long COMPONENT_TASK_TIMEOUT_MS = Long.getLong("iris.mantle.componentTimeout", 120000L);
Engine getEngine();
@@ -39,137 +47,146 @@ public interface MatterGenerator {
LongOpenHashSet partialChunks = new LongOpenHashSet();
try (MantleWriter writer = new MantleWriter(getEngine().getMantle(), getMantle(), x, z, writeRadius, multicore)) {
for (MantlePass pass : getComponents()) {
int passRadius = pass.passChunkRadius();
List<MantleComponent> passComponents = pass.components();
MantleComponent[] enabledComponents = new MantleComponent[passComponents.size()];
int[] componentPassRadii = new int[passComponents.size()];
int enabledComponentCount = 0;
for (MantleComponent component : passComponents) {
if (component.isEnabled()) {
// A component must cover its own reach plus every later pass' reach, or a
// later pass reads this component's data from chunks it never wrote.
int componentReach = component.getRadius() + pass.downstreamBlockRadius();
componentPassRadii[enabledComponentCount] = componentReach > 0 ? Math.ceilDiv(componentReach, 16) : 0;
enabledComponents[enabledComponentCount++] = component;
// Every task launched below writes through this writer. They must all be finished before
// close() releases the cached chunks, even when a pass throws, or detached pool threads
// write into released chunks.
List<MatterComponentTask> outstandingTasks = null;
try {
for (MantlePass pass : getComponents()) {
int passRadius = pass.passChunkRadius();
List<MantleComponent> passComponents = pass.components();
MantleComponent[] enabledComponents = new MantleComponent[passComponents.size()];
int[] componentPassRadii = new int[passComponents.size()];
int enabledComponentCount = 0;
for (MantleComponent component : passComponents) {
if (component.isEnabled()) {
// A component must cover its own reach plus every later pass' reach, or a
// later pass reads this component's data from chunks it never wrote.
int componentReach = component.getRadius() + pass.downstreamBlockRadius();
componentPassRadii[enabledComponentCount] = componentReach > 0 ? Math.ceilDiv(componentReach, 16) : 0;
enabledComponents[enabledComponentCount++] = component;
}
}
}
if (enabledComponentCount == 0) {
continue;
}
if (enabledComponentCount == 0) {
continue;
}
boolean inlineComponents = multicore && DISPATCHER.ownsCurrentThread();
List<CompletableFuture<Void>> launchedTasks = multicore && !inlineComponents ? new ArrayList<>() : null;
MantleComponent[] eligibleComponents = new MantleComponent[enabledComponentCount];
// A dispatcher thread runs its components inline: dispatching from inside the pool
// only trades a worker for a blocked worker.
boolean asyncComponents = multicore && !DISPATCHER.ownsCurrentThread();
if (asyncComponents && outstandingTasks == null) {
outstandingTasks = new ArrayList<>();
}
MantleComponent[] eligibleComponents = new MantleComponent[enabledComponentCount];
for (int i = -passRadius; i <= passRadius; i++) {
int absI = Math.abs(i);
for (int j = -passRadius; j <= passRadius; j++) {
int absJ = Math.abs(j);
int passX = x + i;
int passZ = z + j;
long passKey = chunkKey(passX, passZ);
boolean partial = false;
boolean anyComponentInRadius = false;
for (int i = -passRadius; i <= passRadius; i++) {
int absI = Math.abs(i);
for (int j = -passRadius; j <= passRadius; j++) {
int absJ = Math.abs(j);
int passX = x + i;
int passZ = z + j;
long passKey = chunkKey(passX, passZ);
boolean partial = false;
boolean anyComponentInRadius = false;
for (int componentIndex = 0; componentIndex < enabledComponentCount; componentIndex++) {
int componentPassRadius = componentPassRadii[componentIndex];
if (absI > componentPassRadius || absJ > componentPassRadius) {
partial = true;
} else {
anyComponentInRadius = true;
}
}
if (!anyComponentInRadius) {
partialChunks.add(passKey);
continue;
}
if (partial) {
partialChunks.add(passKey);
}
MantleChunk<Matter> chunk = writer.acquireChunk(passX, passZ);
if (chunk.isFlagged(MantleFlag.PLANNED)) {
continue;
}
int eligibleComponentCount = 0;
for (int componentIndex = 0; componentIndex < enabledComponentCount; componentIndex++) {
MantleComponent component = enabledComponents[componentIndex];
int componentPassRadius = componentPassRadii[componentIndex];
if (absI > componentPassRadius || absJ > componentPassRadius) {
continue;
}
if (chunk.isFlagged(component.getFlag())) {
continue;
}
MantleFlag[] prerequisites = component.getPrerequisiteFlags();
if (prerequisites.length > 0) {
boolean prerequisitesMet = true;
for (MantleFlag prereq : prerequisites) {
if (!chunk.isFlagged(prereq)) {
prerequisitesMet = false;
break;
}
for (int componentIndex = 0; componentIndex < enabledComponentCount; componentIndex++) {
int componentPassRadius = componentPassRadii[componentIndex];
if (absI > componentPassRadius || absJ > componentPassRadius) {
partial = true;
} else {
anyComponentInRadius = true;
}
if (!prerequisitesMet) {
partialChunks.add(passKey);
}
if (!anyComponentInRadius) {
partialChunks.add(passKey);
continue;
}
if (partial) {
partialChunks.add(passKey);
}
MantleChunk<Matter> chunk = writer.acquireChunk(passX, passZ);
if (chunk == null) {
throw new IllegalStateException("Mantle pass chunk " + passX + "," + passZ
+ " is outside the writer prepared at " + x + "," + z + " with radius " + writeRadius);
}
if (chunk.isFlagged(MantleFlag.PLANNED)) {
continue;
}
int eligibleComponentCount = 0;
for (int componentIndex = 0; componentIndex < enabledComponentCount; componentIndex++) {
MantleComponent component = enabledComponents[componentIndex];
int componentPassRadius = componentPassRadii[componentIndex];
if (absI > componentPassRadius || absJ > componentPassRadius) {
continue;
}
if (chunk.isFlagged(component.getFlag())) {
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;
}
}
eligibleComponents[eligibleComponentCount++] = component;
}
eligibleComponents[eligibleComponentCount++] = component;
}
if (eligibleComponentCount == 0) {
continue;
}
if (eligibleComponentCount == 0) {
continue;
}
int finalPassX = passX;
int finalPassZ = passZ;
if (multicore) {
if (inlineComponents) {
int finalPassX = passX;
int finalPassZ = passZ;
if (asyncComponents) {
for (int componentIndex = 0; componentIndex < eligibleComponentCount; componentIndex++) {
MantleComponent component = eligibleComponents[componentIndex];
outstandingTasks.add(runComponentAsync(chunk, component, writer, finalPassX, finalPassZ, context));
}
} else {
for (int componentIndex = 0; componentIndex < eligibleComponentCount; componentIndex++) {
MantleComponent component = eligibleComponents[componentIndex];
runComponentInline(chunk, component, writer, finalPassX, finalPassZ, context);
}
} else {
for (int componentIndex = 0; componentIndex < eligibleComponentCount; componentIndex++) {
MantleComponent component = eligibleComponents[componentIndex];
launchedTasks.add(runComponentAsync(chunk, component, writer, finalPassX, finalPassZ, context));
}
}
} else {
for (int componentIndex = 0; componentIndex < eligibleComponentCount; componentIndex++) {
MantleComponent component = eligibleComponents[componentIndex];
runComponentInline(chunk, component, writer, finalPassX, finalPassZ, context);
}
}
}
}
if (launchedTasks != null) {
for (CompletableFuture<Void> launchedTask : launchedTasks) {
launchedTask.join();
if (asyncComponents) {
awaitComponentTasks(outstandingTasks);
}
}
}
for (int i = -getRealRadius(); i <= getRealRadius(); i++) {
for (int j = -getRealRadius(); j <= getRealRadius(); j++) {
int realX = x + i;
int realZ = z + j;
long realKey = chunkKey(realX, realZ);
if (partialChunks.contains(realKey)) {
continue;
for (int i = -getRealRadius(); i <= getRealRadius(); i++) {
for (int j = -getRealRadius(); j <= getRealRadius(); j++) {
int realX = x + i;
int realZ = z + j;
long realKey = chunkKey(realX, realZ);
if (partialChunks.contains(realKey)) {
continue;
}
writer.acquireChunk(realX, realZ).flag(MantleFlag.PLANNED, true);
}
writer.acquireChunk(realX, realZ).flag(MantleFlag.PLANNED, true);
}
} finally {
abandonComponentTasks(outstandingTasks);
}
}
}
@@ -178,7 +195,7 @@ public interface MatterGenerator {
return (((long) x) << 32) ^ (z & 0xffffffffL);
}
private CompletableFuture<Void> runComponentAsync(
private MatterComponentTask runComponentAsync(
MantleChunk<Matter> chunk,
MantleComponent component,
MantleWriter writer,
@@ -187,30 +204,121 @@ public interface MatterGenerator {
ChunkContext context
) {
MantleFlag flag = component.getFlag();
MatterTaskKey key = new MatterTaskKey(getMantle(), chunkX, chunkZ, flag);
if (chunk.isFlagged(flag)) {
return CompletableFuture.completedFuture(null);
return new MatterComponentTask(key, CompletableFuture.completedFuture(null), null);
}
MatterTaskKey key = new MatterTaskKey(getMantle(), chunkX, chunkZ, flag.ordinal());
CompletableFuture<Void> future = new CompletableFuture<>();
CompletableFuture<Void> existing = IN_FLIGHT_COMPONENTS.putIfAbsent(key, future);
if (existing != null) {
return existing;
return new MatterComponentTask(key, existing, null);
}
try {
if (DISPATCHER.ownsCurrentThread()) {
completeComponentTask(future, key, chunk, component, writer, chunkX, chunkZ, context);
} else {
CompletableFuture.runAsync(() -> completeComponentTask(future, key, chunk, component, writer, chunkX, chunkZ, context), DISPATCHER);
return new MatterComponentTask(key, future, null);
}
Future<?> submission = DISPATCHER.submit(() -> completeComponentTask(future, key, chunk, component, writer, chunkX, chunkZ, context));
return new MatterComponentTask(key, future, submission);
} catch (Throwable throwable) {
IN_FLIGHT_COMPONENTS.remove(key, future);
future.completeExceptionally(throwable);
throw throwable;
}
}
return future;
/**
* Pass barrier. Waits for every launched task, even if one of them failed, so that no task is
* still writing through the writer once this returns. The first failure is rethrown.
*/
private static void awaitComponentTasks(List<MatterComponentTask> tasks) {
if (tasks.isEmpty()) {
return;
}
Throwable failure = null;
for (MatterComponentTask task : tasks) {
try {
awaitComponentTask(task);
} catch (Throwable throwable) {
if (failure == null) {
failure = throwable;
} else if (failure != throwable) {
failure.addSuppressed(throwable);
}
}
}
tasks.clear();
if (failure != null) {
throw failure instanceof RuntimeException runtime ? runtime : new CompletionException(failure);
}
}
/**
* Drains tasks that never reached their pass barrier because the pass threw. The generation
* attempt is already failing, so the wait itself is the point and failures are dropped rather
* than masking the original throwable.
*/
private static void abandonComponentTasks(List<MatterComponentTask> tasks) {
if (tasks == null || tasks.isEmpty()) {
return;
}
try {
awaitComponentTasks(tasks);
} catch (Throwable ignored) {
}
}
private static void awaitComponentTask(MatterComponentTask task) {
CompletableFuture<Void> future = task.future();
long start = System.currentTimeMillis();
boolean interrupted = false;
try {
while (true) {
try {
future.get(COMPONENT_TASK_POLL_MS, TimeUnit.MILLISECONDS);
return;
} catch (InterruptedException interruption) {
// The writer cannot close while this task still writes through it, so the wait is
// uninterruptible and the flag is restored on the way out.
interrupted = true;
} catch (TimeoutException timeout) {
Future<?> submission = task.submission();
boolean dropped = submission != null && submission.isDone();
if (future.isDone()) {
continue;
}
long waited = System.currentTimeMillis() - start;
if (!dropped && waited < COMPONENT_TASK_TIMEOUT_MS) {
continue;
}
// The task can no longer complete (the dispatcher dropped it) or is wedged. Drop
// the dedup entry so later generations do not inherit a future that never
// completes, and release anything else already waiting on it.
IllegalStateException failure = new IllegalStateException("Mantle component " + task.key()
+ (dropped ? " was dropped by the dispatcher" : " did not complete in " + waited + "ms"));
IN_FLIGHT_COMPONENTS.remove(task.key(), future);
future.completeExceptionally(failure);
IrisLogging.error(failure.getMessage());
throw new CompletionException(failure);
} catch (ExecutionException executionFailure) {
Throwable cause = executionFailure.getCause();
throw new CompletionException(cause != null ? cause : executionFailure);
}
}
} finally {
if (interrupted) {
Thread.currentThread().interrupt();
}
}
}
private void completeComponentTask(
@@ -245,17 +353,24 @@ public interface MatterGenerator {
chunk.raiseFlagSuspend(component.getFlag(), () -> component.generateLayer(writer, chunkX, chunkZ, context));
}
/**
* A launched component task. {@code submission} is the dispatcher handle when this generation
* owns the task, and null when the future belongs to another generation or already completed.
*/
record MatterComponentTask(MatterTaskKey key, CompletableFuture<Void> future, Future<?> submission) {
}
final class MatterTaskKey {
private final Mantle<Matter> mantle;
private final int chunkX;
private final int chunkZ;
private final int flagOrdinal;
private final MantleFlag flag;
MatterTaskKey(Mantle<Matter> mantle, int chunkX, int chunkZ, int flagOrdinal) {
MatterTaskKey(Mantle<Matter> mantle, int chunkX, int chunkZ, MantleFlag flag) {
this.mantle = mantle;
this.chunkX = chunkX;
this.chunkZ = chunkZ;
this.flagOrdinal = flagOrdinal;
this.flag = flag;
}
@Override
@@ -271,7 +386,7 @@ public interface MatterGenerator {
return mantle == other.mantle
&& chunkX == other.chunkX
&& chunkZ == other.chunkZ
&& flagOrdinal == other.flagOrdinal;
&& flag.ordinal() == other.flag.ordinal();
}
@Override
@@ -279,8 +394,13 @@ public interface MatterGenerator {
int result = System.identityHashCode(mantle);
result = 31 * result + chunkX;
result = 31 * result + chunkZ;
result = 31 * result + flagOrdinal;
result = 31 * result + flag.ordinal();
return result;
}
@Override
public String toString() {
return flag.name() + " at " + chunkX + "," + chunkZ;
}
}
}
@@ -0,0 +1,71 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.mantle.components;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.matter.MatterSlice;
final class CaveCarveScratch {
final int[] columnMaxY = new int[256];
final int[] waterMaxY = new int[256];
final int[] surfaceBreakFloorY = new int[256];
final boolean[] surfaceBreakColumn = new boolean[256];
final double[] columnThreshold = new double[256];
final double[] passThreshold = new double[256];
final double[] fullWeights = new double[256];
final double[] clampedColumnWeights = new double[256];
final int[] activeColumnIndices = new int[256];
final int[] activeColumnTopY = new int[256];
final int[] planeColumnIndices = new int[256];
final double[] planeThresholdLimit = new double[256];
final boolean[] planeCarve = new boolean[256];
final double[] adaptivePlaneDensity = new double[81];
final double[] adaptivePlanePrediction = new double[256];
final double[] adaptivePlaneAmbiguity = new double[256];
final int[] adaptivePlaneSampleBounds = new int[4];
final int[] adaptiveCellX = new int[256];
final int[] adaptiveCellZ = new int[256];
final int[] adaptiveRow0 = new int[256];
final int[] adaptiveRow1 = new int[256];
final double[] adaptiveTx = new double[256];
final double[] adaptiveTz = new double[256];
final boolean[] warpCacheSet = new boolean[256];
final int[] warpCacheX = new int[256];
final int[] warpCacheY = new int[256];
final int[] warpCacheZ = new int[256];
final double[] warpCacheA = new double[256];
final double[] warpCacheB = new double[256];
final int[] tileIndices = new int[4];
final int[] tileLocalX = new int[4];
final int[] tileLocalZ = new int[4];
final int[] tileTopY = new int[4];
CaveFieldModuleState[] activeModules = new CaveFieldModuleState[0];
double[] activeModuleRemainingMin = new double[0];
double[] activeModuleRemainingMax = new double[0];
int activeModulesY = Integer.MIN_VALUE;
int activeModuleCount;
double[] verticalEdgeFade = new double[0];
MatterCavern[] matterByY = new MatterCavern[0];
Matter[] sectionMatter = new Matter[0];
MatterSlice<?>[] sectionSlices = new MatterSlice<?>[0];
int adaptiveGeometryStep = -1;
int adaptiveGeometryAxisCells = -1;
boolean fullWeightsInitialized;
}
@@ -0,0 +1,58 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.engine.object.IrisCaveFieldModule;
import art.arcane.iris.engine.object.IrisRange;
import art.arcane.iris.util.project.noise.CNG;
final class CaveFieldModuleState {
final CNG density;
final int minY;
final int maxY;
final double weight;
final double threshold;
final boolean invert;
final double minContribution;
final double maxContribution;
CaveFieldModuleState(IrisCaveFieldModule module, CNG density) {
IrisRange range = module.getVerticalRange();
this.density = density;
this.minY = (int) Math.floor(range.getMin());
this.maxY = (int) Math.ceil(range.getMax());
this.weight = module.getWeight();
this.threshold = module.getThreshold();
this.invert = module.isInvert();
double rawMin = invert ? threshold - 1D : -1D - threshold;
double rawMax = invert ? threshold + 1D : 1D - threshold;
this.minContribution = rawMin * weight;
this.maxContribution = rawMax * weight;
}
double sample(double x, int y, double z) {
double sampled = density.noiseFastSigned3D(x, y, z);
return invert ? (threshold - sampled) * weight : (sampled - threshold) * weight;
}
double sample(double x, double y, double z) {
double sampled = density.noiseFastSigned3D(x, y, z);
return invert ? (threshold - sampled) * weight : (sampled - threshold) * weight;
}
}
@@ -0,0 +1,120 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.mantle.components;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.math.PowerOfTwoCoordinates;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.matter.MatterSlice;
import java.util.BitSet;
import java.util.IdentityHashMap;
import java.util.Map;
final class CaveWaterSupportPlan {
private final IdentityHashMap<MatterCavern, WaterCandidateGroup> groups = new IdentityHashMap<>();
void add(int localX, int y, int localZ, MatterCavern water, MatterCavern air) {
WaterCandidateGroup group = groups.computeIfAbsent(water, key -> new WaterCandidateGroup(water, air));
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
group.positions.set((y << 8) | columnIndex);
}
void resolve(MantleChunk<Matter> chunk) {
if (chunk == null) {
groups.clear();
return;
}
for (Map.Entry<MatterCavern, WaterCandidateGroup> entry : groups.entrySet()) {
WaterCandidateGroup group = entry.getValue();
for (int position = group.positions.nextSetBit(0); position >= 0; position = group.positions.nextSetBit(position + 1)) {
int y = position >>> 8;
int columnIndex = position & 255;
int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
int localZ = columnIndex & 15;
MatterCavern current = getCavern(chunk, localX, y, localZ);
if (current != group.water || hasCupSupport(chunk, localX, y, localZ)) {
continue;
}
Matter section = chunk.get(y >> 4);
MatterSlice<MatterCavern> cavernSlice = section.getSlice(MatterCavern.class);
cavernSlice.set(localX, y & 15, localZ, group.air);
}
}
groups.clear();
}
private static boolean hasCupSupport(MantleChunk<Matter> chunk, int localX, int y, int localZ) {
if (localX <= 0 || localX >= 15 || localZ <= 0 || localZ >= 15
|| y <= 1 || !isSolid(chunk, localX, y - 1, localZ)
|| !isSolid(chunk, localX, y - 2, localZ)) {
return false;
}
int support = 0;
if (isSolid(chunk, localX + 1, y, localZ)) {
support++;
}
if (isSolid(chunk, localX - 1, y, localZ)) {
support++;
}
if (isSolid(chunk, localX, y, localZ + 1)) {
support++;
}
if (isSolid(chunk, localX, y, localZ - 1)) {
support++;
}
if (isSolid(chunk, localX, y + 1, localZ)) {
support++;
}
return support >= 4;
}
private static boolean isSolid(MantleChunk<Matter> chunk, int localX, int y, int localZ) {
if (localX < 0 || localX >= 16 || localZ < 0 || localZ >= 16) {
return false;
}
MatterCavern cavern = getCavern(chunk, localX, y, localZ);
return cavern == null || !cavern.isCavern();
}
private static MatterCavern getCavern(MantleChunk<Matter> chunk, int localX, int y, int localZ) {
Matter section = chunk.get(y >> 4);
if (section == null) {
return null;
}
MatterSlice<MatterCavern> cavernSlice = section.getSlice(MatterCavern.class);
return cavernSlice == null ? null : cavernSlice.get(localX, y & 15, localZ);
}
private static final class WaterCandidateGroup {
private final MatterCavern water;
private final MatterCavern air;
private final BitSet positions = new BitSet();
private WaterCandidateGroup(MatterCavern water, MatterCavern air) {
this.water = water;
this.air = air;
}
}
}
@@ -0,0 +1,159 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState;
final class GoldenDebugObjectPlacer implements IObjectPlacer {
private static final int[] GOLDEN_DEBUG_TARGET = parseGoldenDebugTarget(resolveGoldenDebugSpec());
private static final boolean GOLDEN_DEBUG = GOLDEN_DEBUG_TARGET != null;
private final IObjectPlacer delegate;
private final String tag;
GoldenDebugObjectPlacer(IObjectPlacer delegate, String tag) {
this.delegate = delegate;
this.tag = tag;
}
private static String resolveGoldenDebugSpec() {
String property = System.getProperty("iris.goldendebug");
if (property != null && !property.isBlank()) {
return property;
}
try {
java.io.File marker = new java.io.File("plugins/Iris/goldendebug.txt");
if (marker.isFile()) {
return java.nio.file.Files.readString(marker.toPath()).trim();
}
} catch (Throwable ignored) {
}
return null;
}
private static int[] parseGoldenDebugTarget(String raw) {
if (raw == null || raw.isBlank()) {
return null;
}
String[] parts = raw.split(",");
if (parts.length != 2 && parts.length != 3) {
return null;
}
try {
int radius = parts.length == 3 ? Integer.parseInt(parts[2].trim()) : 0;
return new int[]{Integer.parseInt(parts[0].trim()), Integer.parseInt(parts[1].trim()), radius};
} catch (NumberFormatException e) {
return null;
}
}
static boolean isGoldenDebugChunk(int x, int z) {
return GOLDEN_DEBUG
&& Math.abs(GOLDEN_DEBUG_TARGET[0] - x) <= GOLDEN_DEBUG_TARGET[2]
&& Math.abs(GOLDEN_DEBUG_TARGET[1] - z) <= GOLDEN_DEBUG_TARGET[2];
}
@Override
public int getHighest(int x, int z, IrisData data) {
int result = delegate.getHighest(x, z, data);
IrisLogging.info("Goldendebug query: tag=" + tag + " getHighest(" + x + "," + z + ")=" + result);
return result;
}
@Override
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
int result = delegate.getHighest(x, z, data, ignoreFluid);
IrisLogging.info("Goldendebug query: tag=" + tag + " getHighest(" + x + "," + z + ",ignoreFluid=" + ignoreFluid + ")=" + result);
return result;
}
@Override
public void set(int x, int y, int z, PlatformBlockState d) {
delegate.set(x, y, z, d);
}
@Override
public PlatformBlockState get(int x, int y, int z) {
return delegate.get(x, y, z);
}
@Override
public boolean isPreventingDecay() {
return delegate.isPreventingDecay();
}
@Override
public boolean isCarved(int x, int y, int z) {
boolean result = delegate.isCarved(x, y, z);
IrisLogging.info("Goldendebug query: tag=" + tag + " isCarved(" + x + "," + y + "," + z + ")=" + result);
return result;
}
@Override
public boolean isSurfaceSolid(int x, int y, int z) {
return delegate.isSurfaceSolid(x, y, z);
}
@Override
public boolean isSolid(int x, int y, int z) {
boolean result = delegate.isSolid(x, y, z);
IrisLogging.info("Goldendebug query: tag=" + tag + " isSolid(" + x + "," + y + "," + z + ")=" + result);
return result;
}
@Override
public boolean isUnderwater(int x, int z) {
return delegate.isUnderwater(x, z);
}
@Override
public int getFluidHeight() {
return delegate.getFluidHeight();
}
@Override
public boolean isDebugSmartBore() {
return delegate.isDebugSmartBore();
}
@Override
public <T> void setData(int xx, int yy, int zz, T data) {
delegate.setData(xx, yy, zz, data);
}
@Override
public <T> T getData(int xx, int yy, int zz, Class<T> t) {
return delegate.getData(xx, yy, zz, t);
}
@Override
public void setTile(int xx, int yy, int zz, TileData tile) {
delegate.setTile(xx, yy, zz, tile);
}
@Override
public Engine getEngine() {
return delegate.getEngine();
}
}
File diff suppressed because it is too large. Load diff
@@ -62,7 +62,8 @@ public class MantleCarvingComponent extends IrisMantleComponent {
private static final double[] KERNEL_WEIGHT = new double[KERNEL_SIZE];
private static final ThreadLocal<BlendScratch> BLEND_SCRATCH = ThreadLocal.withInitial(BlendScratch::new);
private final Map<IrisCaveProfile, IrisCaveCarver3D> profileCarvers = new IdentityHashMap<>();
private final Object profileCarverLock = new Object();
private volatile Map<IrisCaveProfile, IrisCaveCarver3D> profileCarvers = new IdentityHashMap<>();
static {
int kernelIndex = 0;
@@ -90,7 +91,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start();
List<WeightedProfile> weightedProfiles = resolveWeightedProfiles(x, z, complex, resolverState);
getEngineMantle().getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds());
IrisCaveCarver3D.WaterSupportPlan waterSupportPlan = new IrisCaveCarver3D.WaterSupportPlan();
CaveWaterSupportPlan waterSupportPlan = new CaveWaterSupportPlan();
for (WeightedProfile weightedProfile : weightedProfiles) {
carveProfile(weightedProfile, writer, x, z, chunkSurfaceHeights, waterSupportPlan);
}
@@ -104,7 +105,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
@ChunkCoordinates
private void carveProfile(WeightedProfile weightedProfile, MantleWriter writer, int cx, int cz,
int[] chunkSurfaceHeights, IrisCaveCarver3D.WaterSupportPlan waterSupportPlan) {
int[] chunkSurfaceHeights, CaveWaterSupportPlan waterSupportPlan) {
IrisCaveCarver3D carver = getCarver(weightedProfile.profile);
carver.carve(writer, cx, cz, weightedProfile.columnWeights, MIN_WEIGHT, THRESHOLD_PENALTY,
weightedProfile.worldYRange, chunkSurfaceHeights, null, waterSupportPlan);
@@ -112,7 +113,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
private void carveUpperTerrain(UpperDimensionContext upperCtx, List<WeightedProfile> normalProfiles,
MantleWriter writer, int cx, int cz, int[] lowerSurfaceHeights,
IrisCaveCarver3D.WaterSupportPlan waterSupportPlan) {
CaveWaterSupportPlan waterSupportPlan) {
int chunkHeight = getEngineMantle().getEngine().getHeight();
int worldMinHeight = getEngineMantle().getEngine().getWorld().minHeight();
int gap = getDimension().getUpperDimensionGap();
@@ -453,14 +454,21 @@ public class MantleCarvingComponent extends IrisMantleComponent {
}
private IrisCaveCarver3D getCarver(IrisCaveProfile profile) {
synchronized (profileCarvers) {
IrisCaveCarver3D carver = profileCarvers.get(profile);
if (carver != null) {
return carver;
IrisCaveCarver3D carver = profileCarvers.get(profile);
if (carver != null) {
return carver;
}
IrisCaveCarver3D createdCarver = new IrisCaveCarver3D(getEngineMantle().getEngine(), profile);
synchronized (profileCarverLock) {
IrisCaveCarver3D published = profileCarvers.get(profile);
if (published != null) {
return published;
}
IrisCaveCarver3D createdCarver = new IrisCaveCarver3D(getEngineMantle().getEngine(), profile);
profileCarvers.put(profile, createdCarver);
Map<IrisCaveProfile, IrisCaveCarver3D> updated = new IdentityHashMap<>(profileCarvers);
updated.put(profile, createdCarver);
profileCarvers = updated;
return createdCarver;
}
}
@@ -28,7 +28,6 @@ import art.arcane.iris.engine.mantle.ComponentFlag;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.mantle.IrisMantleComponent;
import art.arcane.iris.engine.mantle.MantleWriter;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.TreeBlockMaterial;
import art.arcane.iris.engine.object.CarvingMode;
@@ -49,7 +48,6 @@ import art.arcane.iris.engine.object.IrisProceduralPlacement;
import art.arcane.iris.engine.object.IrisProceduralTree;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.ObjectPlaceMode;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
@@ -81,45 +79,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
private static final int BEDROCK_CLEARANCE = 6;
private static final Map<String, CaveRejectLogState> CAVE_REJECT_LOG_STATE = new ConcurrentHashMap<>();
private static final Set<String> MISSING_LOAD_KEY_WARNED = ConcurrentHashMap.newKeySet();
private static final int[] GOLDEN_DEBUG_TARGET = parseGoldenDebugTarget(resolveGoldenDebugSpec());
private static final boolean GOLDEN_DEBUG = GOLDEN_DEBUG_TARGET != null;
private static String resolveGoldenDebugSpec() {
String property = System.getProperty("iris.goldendebug");
if (property != null && !property.isBlank()) {
return property;
}
try {
java.io.File marker = new java.io.File("plugins/Iris/goldendebug.txt");
if (marker.isFile()) {
return java.nio.file.Files.readString(marker.toPath()).trim();
}
} catch (Throwable ignored) {
}
return null;
}
private static int[] parseGoldenDebugTarget(String raw) {
if (raw == null || raw.isBlank()) {
return null;
}
String[] parts = raw.split(",");
if (parts.length != 2 && parts.length != 3) {
return null;
}
try {
int radius = parts.length == 3 ? Integer.parseInt(parts[2].trim()) : 0;
return new int[]{Integer.parseInt(parts[0].trim()), Integer.parseInt(parts[1].trim()), radius};
} catch (NumberFormatException e) {
return null;
}
}
private static boolean isGoldenDebugChunk(int x, int z) {
return GOLDEN_DEBUG
&& Math.abs(GOLDEN_DEBUG_TARGET[0] - x) <= GOLDEN_DEBUG_TARGET[2]
&& Math.abs(GOLDEN_DEBUG_TARGET[1] - z) <= GOLDEN_DEBUG_TARGET[2];
}
public MantleObjectComponent(EngineMantle engineMantle) {
super(engineMantle, ReservedFlag.OBJECT, 1);
@@ -449,7 +408,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
int blockX = x << 4;
int blockZ = z << 4;
boolean golden = isGoldenDebugChunk(x, z);
boolean golden = GoldenDebugObjectPlacer.isGoldenDebugChunk(x, z);
CaveAnchorCache caveAnchorCache = new CaveAnchorCache();
for (IrisProceduralPlacement p : proceduralObjects.getAllPlacements()) {
boolean treePlacement = p instanceof IrisProceduralTree;
@@ -474,7 +433,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
int minDepthBelowSurface = resolveObjectMinDepthBelowSurface(caveProfile);
int anchorSearchAttempts = resolveAnchorSearchAttempts(caveProfile);
IObjectPlacer basePlacer = p.isPlausible() ? new DecayControlPlacer(writer) : writer;
IObjectPlacer placer = golden ? new GoldenDebugPlacer(basePlacer, scope + "/" + p.getName()) : basePlacer;
IObjectPlacer placer = golden ? new GoldenDebugObjectPlacer(basePlacer, scope + "/" + p.getName()) : basePlacer;
int density = Math.max(1, p.getDensity());
for (int i = 0; i < density; i++) {
IrisObject variant = p.getVariantObject(getData(), rng);
@@ -554,8 +513,8 @@ public class MantleObjectComponent extends IrisMantleComponent {
placeResult = contained.resultY();
commitResult = contained.commitResult();
} else {
String marker = placementMarker(variant, id, "procedural");
placeResult = variant.place(xx, -1, zz, placer, placement, rng, (b, data) -> {
String marker = placementMarker(variant, id, "procedural");
if (marker != null) {
placer.setData(b.getX(), b.getY(), b.getZ(), marker);
}
@@ -671,7 +630,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
int nullObjects = 0;
int errors = 0;
int density = objectPlacement.getDensity(rng, x, z, getData());
boolean golden = isGoldenDebugChunk(chunkX, chunkZ);
boolean golden = GoldenDebugObjectPlacer.isGoldenDebugChunk(chunkX, chunkZ);
for (int i = 0; i < density; i++) {
attempts++;
@@ -692,7 +651,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
IrisObjectPlacement effectivePlacement = resolveEffectivePlacement(objectPlacement, v);
boolean treePlacement = isTreePlacement(v, effectivePlacement);
int id = rng.i(0, Integer.MAX_VALUE);
IObjectPlacer placePlacer = golden ? new GoldenDebugPlacer(writer, scope + "/" + v.getLoadKey()) : writer;
IObjectPlacer placePlacer = golden ? new GoldenDebugObjectPlacer(writer, scope + "/" + v.getLoadKey()) : writer;
if (golden) {
IrisLogging.info("Goldendebug object attempt: chunk=" + chunkX + "," + chunkZ
+ " scope=" + scope
@@ -703,8 +662,8 @@ public class MantleObjectComponent extends IrisMantleComponent {
+ " mode=" + effectivePlacement.getMode());
}
try {
String marker = placementMarker(v, id, "surface");
int result = v.place(xx, -1, zz, placePlacer, effectivePlacement, rng, (b, data) -> {
String marker = placementMarker(v, id, "surface");
if (marker != null) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
}
@@ -1073,9 +1032,9 @@ public class MantleObjectComponent extends IrisMantleComponent {
placement.setForcePlace(true);
}
boolean treePlacement = isTreePlacement(v, objectPlacement);
String marker = placementMarker(v, id, "upper");
int result = v.place(xx, anchorY, zz, writer, placement, rng, (b, data) -> {
String marker = placementMarker(v, id, "upper");
if (marker != null) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
}
@@ -1247,99 +1206,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
return maxAnchorY + 1;
}
private static final class GoldenDebugPlacer implements IObjectPlacer {
private final IObjectPlacer delegate;
private final String tag;
private GoldenDebugPlacer(IObjectPlacer delegate, String tag) {
this.delegate = delegate;
this.tag = tag;
}
@Override
public int getHighest(int x, int z, IrisData data) {
int result = delegate.getHighest(x, z, data);
IrisLogging.info("Goldendebug query: tag=" + tag + " getHighest(" + x + "," + z + ")=" + result);
return result;
}
@Override
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
int result = delegate.getHighest(x, z, data, ignoreFluid);
IrisLogging.info("Goldendebug query: tag=" + tag + " getHighest(" + x + "," + z + ",ignoreFluid=" + ignoreFluid + ")=" + result);
return result;
}
@Override
public void set(int x, int y, int z, PlatformBlockState d) {
delegate.set(x, y, z, d);
}
@Override
public PlatformBlockState get(int x, int y, int z) {
return delegate.get(x, y, z);
}
@Override
public boolean isPreventingDecay() {
return delegate.isPreventingDecay();
}
@Override
public boolean isCarved(int x, int y, int z) {
boolean result = delegate.isCarved(x, y, z);
IrisLogging.info("Goldendebug query: tag=" + tag + " isCarved(" + x + "," + y + "," + z + ")=" + result);
return result;
}
@Override
public boolean isSurfaceSolid(int x, int y, int z) {
return delegate.isSurfaceSolid(x, y, z);
}
@Override
public boolean isSolid(int x, int y, int z) {
boolean result = delegate.isSolid(x, y, z);
IrisLogging.info("Goldendebug query: tag=" + tag + " isSolid(" + x + "," + y + "," + z + ")=" + result);
return result;
}
@Override
public boolean isUnderwater(int x, int z) {
return delegate.isUnderwater(x, z);
}
@Override
public int getFluidHeight() {
return delegate.getFluidHeight();
}
@Override
public boolean isDebugSmartBore() {
return delegate.isDebugSmartBore();
}
@Override
public <T> void setData(int xx, int yy, int zz, T data) {
delegate.setData(xx, yy, zz, data);
}
@Override
public <T> T getData(int xx, int yy, int zz, Class<T> t) {
return delegate.getData(xx, yy, zz, t);
}
@Override
public void setTile(int xx, int yy, int zz, TileData tile) {
delegate.setTile(xx, yy, zz, tile);
}
@Override
public Engine getEngine() {
return delegate.getEngine();
}
}
private int findCaveAnchorY(MantleWriter writer, RNG rng, int x, int z, IrisCaveAnchorMode anchorMode, int anchorScanStep, int objectMinDepthBelowSurface, CaveAnchorCache anchorCache) {
KList<Integer> anchors = anchorCache.get(writer, anchorMode, anchorScanStep, objectMinDepthBelowSurface, x, z);
if (anchors.isEmpty()) {
@@ -104,12 +104,14 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
MantleChunk<Matter> mc = mantle.getChunk(x, z).use();
try {
PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start();
final int worldHeightSpan = getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight();
final int caveLavaHeight = getEngine().getDimension().getCaveLavaHeight();
mc.iterate(MatterCavern.class, (xx, yy, zz, c) -> {
if (c == null) {
return;
}
if (yy >= getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight() || yy <= 0) {
if (yy >= worldHeightSpan || yy <= 0) {
return;
}
@@ -138,10 +140,12 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
return;
}
PlatformBlockState explicitState = resolveExplicitCarveState(c, context.getFluid().get(rx, rz), LAVA, AIR);
if (explicitCarveIntent) {
output.setRaw(rx, yy, rz, explicitState);
} else if (usesDefaultLava(getEngine().getDimension().getCaveLavaHeight(), yy)) {
// Only a water cavern consumes the fluid sample, and on the maintenance path that
// sample is a full procedural stream evaluation, so never take it per voxel.
PlatformBlockState fluid = c.isWater() ? context.getFluid().get(rx, rz) : null;
output.setRaw(rx, yy, rz, resolveExplicitCarveState(c, fluid, LAVA, AIR));
} else if (usesDefaultLava(caveLavaHeight, yy)) {
output.setRaw(rx, yy, rz, LAVA);
} else {
output.setRaw(rx, yy, rz, AIR);
Loaded 100 of 374 files, more files were not shown because too many files have changed in this diff. Show more