mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 21:40:05 +00:00
🧹
This commit is contained in:
1 parent
0afbfcb7a2
commit
324cf9e095
374 files changed
+22703
-25648
No files matched your search
@@ -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();
|
||||
|
||||
+4
-3
@@ -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);
|
||||
}
|
||||
|
||||
+156
-122
@@ -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;
|
||||
}
|
||||
}
|
||||
+65
-38
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
-315
@@ -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);
|
||||
}
|
||||
}
|
||||
-401
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-74
@@ -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;
|
||||
}
|
||||
}
|
||||
-140
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-68
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-66
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
-113
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-52
@@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
-233
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
-54
@@ -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;
|
||||
}
|
||||
}
|
||||
-43
@@ -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);
|
||||
}
|
||||
}
|
||||
-42
@@ -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;
|
||||
}
|
||||
}
|
||||
+120
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+159
@@ -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();
|
||||
}
|
||||
}
|
||||
+164
-291
File diff suppressed because it is too large.
Load diff
+18
-10
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+7
-141
@@ -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
Reference in new issue
Block a user