This commit is contained in:
Brian Neumann-Fopiano
2026-07-16 12:26:01 -04:00
parent 55b7460b9a
commit 699247b1de
327 changed files with 30138 additions and 3934 deletions
@@ -142,7 +142,7 @@ public final class DatapackIngestService {
if (report.changed()) {
message(sender, C.YELLOW + "New datapack structures were installed. A server restart is required for them to register and generate.");
message(sender, C.GRAY + "After the restart their jigsaw pools, pieces & objects are imported automatically (set general.autoImportDatapackStructures=false to disable), or run /iris structure import <dimension> to import everything on demand. Reference an imported key from a 'structures' placement to position it manually.");
message(sender, C.GRAY + "Datapacks replace matching vanilla structure keys by default. Set 'importedStructures.datapackOverrides' to false to keep minecraft-namespaced structure definitions untouched; control non-minecraft datapack and mod structures with importedStructures.enabled/disabled.");
message(sender, C.GRAY + "Datapacks replace matching vanilla structure keys by default. Set 'importedStructures.datapackOverrides' to false to keep minecraft-namespaced structure definitions untouched; deny non-minecraft datapack and mod structures explicitly with importedStructures.disabled.");
if (restart) {
ServerConfigurator.restart();
} else {
@@ -401,28 +401,46 @@ public final class DatapackIngestService {
}
IrisLogging.info("Importing datapack structures (jigsaw pools, pieces & objects) into packs that declare datapackImports...");
AtomicInteger packs = new AtomicInteger();
AtomicInteger attemptedPacks = new AtomicInteger();
AtomicInteger completedPacks = new AtomicInteger();
try (Stream<IrisData> stream = ServerConfigurator.allPacks()) {
stream.forEach(data -> {
if (data == null || !hasImports(data)) {
return;
}
attemptedPacks.incrementAndGet();
try {
BulkStructureImporter.importDatapackStructures(data, StructureImporter.Mode.ADD_ONLY, BukkitPlatform.console());
packs.incrementAndGet();
} catch (Throwable e) {
IrisLogging.reportError(e);
BulkStructureImporter.Report report = BulkStructureImporter.importDatapackStructures(
data, StructureImporter.Mode.ADD_ONLY, BukkitPlatform.console());
if (report.failed() > 0) {
IrisLogging.error("Datapack structure import for pack '%s' reported %d failure(s); the manifest remains pending for retry.",
data.getDataFolder().getPath(), report.failed());
return;
}
completedPacks.incrementAndGet();
} catch (RuntimeException e) {
IrisLogging.reportError("Datapack structure import failed for pack '"
+ data.getDataFolder().getPath() + "'; the manifest remains pending for retry.", e);
}
});
}
for (Entry entry : manifest.entries) {
entry.structuresImported = true;
if (!markStructuresImportedIfComplete(
manifest.entries, attemptedPacks.get(), completedPacks.get())) {
return;
}
writeManifest(root, manifest);
if (packs.get() > 0) {
IrisLogging.info("Datapack structure import finished for " + packs.get() + " pack(s). Reference the imported keys from a 'structures' placement to position them manually.");
IrisLogging.info("Datapack structure import finished for " + completedPacks.get() + " pack(s). Reference the imported keys from a 'structures' placement to position them manually.");
}
static boolean markStructuresImportedIfComplete(List<Entry> entries, int attemptedPacks, int completedPacks) {
if (attemptedPacks < 1 || completedPacks != attemptedPacks) {
return false;
}
for (Entry entry : entries) {
entry.structuresImported = true;
}
return true;
}
private static void flattenIfWrapped(File dir) throws IOException {
@@ -33,6 +33,10 @@ import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.structure.StructureGraphCatalog;
import art.arcane.iris.core.structure.authoring.StructureRecoveryResult;
import art.arcane.iris.core.structure.authoring.StructureTransactionWriter;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBlockData;
import art.arcane.iris.engine.object.IrisDimension;
@@ -209,10 +213,6 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
return loadAny(IrisGenerator.class, key, nearest);
}
public static IrisStructure loadAnyStructure(String key, @Nullable IrisData nearest) {
return loadAny(IrisStructure.class, key, nearest);
}
public static IrisJigsawPool loadAnyJigsawPool(String key, @Nullable IrisData nearest) {
return loadAny(IrisJigsawPool.class, key, nearest);
}
@@ -330,6 +330,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
}
public synchronized void hotloaded() {
StructureGraphCatalog.invalidate(this);
closed = false;
possibleSnippets = new KMap<>();
builder = new GsonBuilder()
@@ -342,6 +343,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
loaders.clear();
File packs = dataFolder;
packs.mkdirs();
recoverStructureTransactions();
this.lootLoader = registerLoader(IrisLootTable.class);
this.spawnerLoader = registerLoader(IrisSpawner.class);
this.entityLoader = registerLoader(IrisEntity.class);
@@ -380,6 +382,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
.setPrettyPrinting();
loaders.clear();
dataFolder.mkdirs();
recoverStructureTransactions();
biomeLoader = registerLoader(IrisBiome.class);
dimensionLoader = registerLoader(IrisDimension.class);
builder.registerTypeAdapterFactory(KeyedType::createTypeAdapter);
@@ -390,11 +393,46 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
}
public void dump() {
StructureGraphCatalog.invalidate(this);
for (ResourceLoader<?> i : loaders.values()) {
i.clearCache();
}
}
public synchronized void invalidateStructureResources() {
StructureGraphCatalog.invalidate(this);
invalidateLoader(objectLoader);
invalidateLoader(structureLoader);
invalidateLoader(jigsawPoolLoader);
invalidateLoader(jigsawPieceLoader);
if (engine != null) {
IrisStructureLocator.invalidate(engine);
}
}
private void recoverStructureTransactions() {
StructureRecoveryResult recovery = new StructureTransactionWriter(dataFolder.toPath())
.recoverIncompleteTransactions();
if (recovery.successful()) {
if (recovery.recoveredTransactions() > 0) {
IrisLogging.warn("Recovered " + recovery.recoveredTransactions()
+ " interrupted structure authoring transaction(s) in " + dataFolder);
}
return;
}
IllegalStateException failure = new IllegalStateException(
"Unable to recover interrupted structure authoring transactions in " + dataFolder);
for (StructureRecoveryResult.Failure recoveryFailure : recovery.failures()) {
failure.addSuppressed(new IOException(
"Recovery failed for " + recoveryFailure.transactionRoot(),
recoveryFailure.cause()
));
}
IrisLogging.reportError(failure);
failure.printStackTrace();
throw failure;
}
public void clearLists() {
for (ResourceLoader<?> i : loaders.values()) {
i.clearList();
@@ -402,6 +440,14 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
possibleSnippets.clear();
}
private void invalidateLoader(ResourceLoader<?> loader) {
if (loader == null) {
return;
}
loader.clearCache();
loader.clearList();
}
public Set<Class<?>> resolveSnippets() {
var result = new HashSet<Class<?>>();
var processed = new HashSet<Class<?>>();
@@ -23,17 +23,7 @@ import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.nms.v1X.NMSBinding1X;
import org.bukkit.Bukkit;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
public class INMS {
private static final Version CURRENT = new Version(26, 2, 0, "v26_2_R1");
private static final List<Version> REVISION = List.of(
CURRENT
);
//@done
private static final INMSBinding binding = bind();
@@ -46,81 +36,39 @@ public class INMS {
return "BUKKIT";
}
try {
String name = Bukkit.getServer().getClass().getCanonicalName();
if (name.equals("org.bukkit.craftbukkit.CraftServer")) {
return getTag(REVISION, "BUKKIT");
} else {
return name.split("\\Q.\\E")[3];
}
} catch (Throwable e) {
IrisLogging.reportError(e);
IrisLogging.error("Failed to determine server nms version!");
e.printStackTrace();
}
return "BUKKIT";
return NmsBindingSelector.select(requireMinecraftVersion());
}
private static INMSBinding bind() {
String code = getNMSTag();
boolean disableNms = IrisSettings.get().getGeneral().isDisableNMS();
List<String> probeCodes = NmsBindingProbeSupport.getBindingProbeCodes(code, disableNms, getFallbackBindingCodes());
if ("BUKKIT".equals(code) && !disableNms) {
IrisLogging.info("NMS tag resolution fell back to Bukkit; probing supported revision bindings.");
}
for (int i = 0; i < probeCodes.size(); i++) {
INMSBinding resolvedBinding = tryBind(probeCodes.get(i), i == 0);
if (resolvedBinding != null) {
return resolvedBinding;
}
}
if (disableNms) {
IrisLogging.info("Craftbukkit " + code + " <-> " + NMSBinding1X.class.getSimpleName() + " Successfully Bound");
IrisLogging.warn("Note: NMS support is disabled. Iris is running in limited Bukkit fallback mode.");
IrisLogging.info("Craftbukkit BUKKIT <-> " + NMSBinding1X.class.getSimpleName() + " Successfully Bound");
IrisLogging.warn("NMS support is disabled. Iris world creation is unavailable until general.disableNMS=false.");
return new NMSBinding1X();
}
MinecraftVersion detectedVersion = getMinecraftVersion();
String serverVersion = detectedVersion == null ? Bukkit.getServer().getVersion() : detectedVersion.value();
throw new IllegalStateException("Iris requires Minecraft 26.2. Detected server version: " + serverVersion);
return bindExact(getNMSTag());
}
private static String getTag(List<Version> versions, String def) {
MinecraftVersion detectedVersion = getMinecraftVersion();
if (detectedVersion == null) {
return def;
}
for (Version p : versions) {
if (!detectedVersion.isSameRelease(p.major, p.minor, p.patch)) {
continue;
}
return p.tag;
}
return def;
}
private static MinecraftVersion getMinecraftVersion() {
private static MinecraftVersion requireMinecraftVersion() {
try {
return MinecraftVersion.detect(Bukkit.getServer());
MinecraftVersion detected = MinecraftVersion.detect(Bukkit.getServer());
if (detected == null) {
throw new IllegalStateException("Iris could not determine the exact Minecraft server version");
}
return detected;
} catch (Throwable e) {
IrisLogging.reportError(e);
IrisLogging.error("Failed to determine server minecraft version!");
e.printStackTrace();
return null;
if (e instanceof IllegalStateException illegalStateException) {
throw illegalStateException;
}
throw new IllegalStateException("Iris could not determine the exact Minecraft server version", e);
}
}
private static INMSBinding tryBind(String code, boolean announce) {
if (announce) {
IrisLogging.info("Locating NMS Binding for " + code);
} else {
IrisLogging.info("Probing NMS Binding for " + code);
}
private static INMSBinding bindExact(String code) {
IrisLogging.info("Locating exact NMS Binding for " + code);
try {
Class<?> clazz = Class.forName("art.arcane.iris.core.nms." + code + ".NMSBinding");
Object candidate = clazz.getConstructor().newInstance();
@@ -128,25 +76,15 @@ public class INMS {
IrisLogging.info("Craftbukkit " + code + " <-> " + candidate.getClass().getSimpleName() + " Successfully Bound");
return binding;
}
} catch (ClassNotFoundException | NoClassDefFoundError classNotFoundException) {
IrisLogging.warn("Failed to load NMS binding class for " + code + ": " + classNotFoundException.getMessage());
throw new IllegalStateException("Exact NMS binding class for " + code
+ " does not implement " + INMSBinding.class.getName());
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
}
return null;
}
private static Set<String> getFallbackBindingCodes() {
Set<String> codes = new LinkedHashSet<>();
for (Version version : REVISION) {
if (version.tag != null && !version.tag.isBlank()) {
codes.add(version.tag);
if (e instanceof IllegalStateException illegalStateException) {
throw illegalStateException;
}
throw new IllegalStateException("Failed to bind exact NMS revision " + code, e);
}
return codes;
}
private record Version(int major, int minor, int patch, String tag) {}
}
@@ -74,6 +74,8 @@ public interface INMSBinding {
boolean supportsCustomBiomes();
boolean supportsIrisWorldGeneration();
int getTrueBiomeBaseId(Object biomeBase);
Object getTrueBiomeBase(Location location);
@@ -94,25 +96,15 @@ public interface INMSBinding {
KList<Biome> getBiomes();
default KList<String> getStructureKeys() {
return new KList<>();
}
KList<String> getStructureKeys();
default KList<String> getStructureSetKeys() {
return new KList<>();
}
KList<String> getStructureSetKeys();
default KList<String> getReachableStructureKeys(World world) {
return new KList<>();
}
KList<String> getReachableStructureKeys(World world);
default KList<String> getStructureBiomeKeys(String structureKey) {
return new KList<>();
}
KList<String> getStructureBiomeKeys(String structureKey);
default KList<String> getPossibleBiomeKeys(World world) {
return new KList<>();
}
KList<String> getPossibleBiomeKeys(World world);
default KList<String> getObjectFeatureKeys() {
return new KList<>();
@@ -229,8 +221,15 @@ public interface INMSBinding {
KMap<Material, List<BlockProperty>> getBlockProperties();
private void validateDimensionTypes(WorldCreator c) {
if (c.generator() instanceof PlatformChunkGenerator gen
&& missingDimensionTypes(gen.getTarget().getDimension().getDimensionTypeKey())) {
if (!(c.generator() instanceof PlatformChunkGenerator generator)) {
return;
}
if (!supportsIrisWorldGeneration()) {
throw new IllegalStateException("Iris world '" + c.name() + "' cannot be created with limited NMS binding "
+ getClass().getSimpleName()
+ "; set general.disableNMS=false and use the supported Minecraft 26.2 server runtime");
}
if (missingDimensionTypes(generator.getTarget().getDimension().getDimensionTypeKey())) {
throw new IllegalStateException("Missing dimension types to create world");
}
}
@@ -1,29 +0,0 @@
package art.arcane.iris.core.nms;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
final class NmsBindingProbeSupport {
private NmsBindingProbeSupport() {
}
static List<String> getBindingProbeCodes(String code, boolean disableNms, Collection<String> fallbackCodes) {
List<String> probeCodes = new ArrayList<>();
if (code == null || code.isBlank()) {
return probeCodes;
}
if (!"BUKKIT".equals(code)) {
probeCodes.add(code);
return probeCodes;
}
if (disableNms || fallbackCodes == null) {
return probeCodes;
}
probeCodes.addAll(fallbackCodes);
return probeCodes;
}
}
@@ -0,0 +1,20 @@
package art.arcane.iris.core.nms;
final class NmsBindingSelector {
private static final String SUPPORTED_VERSION = "26.2";
private static final String SUPPORTED_TAG = "v26_2_R1";
private NmsBindingSelector() {
}
static String select(MinecraftVersion version) {
if (version == null) {
throw new IllegalStateException("Iris requires an exact Minecraft version before selecting NMS");
}
if (!version.isSameRelease(26, 2, 0)) {
throw new IllegalStateException("Iris requires Minecraft " + SUPPORTED_VERSION
+ ". Detected server version: " + version.value());
}
return SUPPORTED_TAG;
}
}
@@ -98,7 +98,8 @@ public class NMSBinding1X implements INMSBinding {
@Override
public void inject(long seed, Engine engine, World world) throws NoSuchFieldException, IllegalAccessException {
throw new IllegalStateException("Iris world generation requires the supported NMS binding; "
+ "general.disableNMS=true cannot create or initialize an Iris world");
}
public Vector3d getBoundingbox() {
@@ -159,6 +160,11 @@ public class NMSBinding1X implements INMSBinding {
return false;
}
@Override
public boolean supportsIrisWorldGeneration() {
return false;
}
@Override
public int getTrueBiomeBaseId(Object biomeBase) {
return 0;
@@ -212,6 +218,31 @@ public class NMSBinding1X implements INMSBinding {
return biomes;
}
@Override
public KList<String> getStructureKeys() {
throw unsupportedStructureHook("read registered structure keys");
}
@Override
public KList<String> getStructureSetKeys() {
throw unsupportedStructureHook("read registered structure-set keys");
}
@Override
public KList<String> getReachableStructureKeys(World world) {
throw unsupportedStructureHook("resolve reachable structures");
}
@Override
public KList<String> getStructureBiomeKeys(String structureKey) {
throw unsupportedStructureHook("resolve structure biome keys");
}
@Override
public KList<String> getPossibleBiomeKeys(World world) {
throw unsupportedStructureHook("resolve possible biome keys");
}
@Override
public DataVersion getDataVersion() {
return DataVersion.UNSUPPORTED;
@@ -258,4 +289,10 @@ public class NMSBinding1X implements INMSBinding {
IrisLogging.error("Cannot use the global data palette! Iris is incapable of using MCA generation on this version of minecraft!");
return null;
}
private IllegalStateException unsupportedStructureHook(String operation) {
return new IllegalStateException("Iris cannot " + operation + " with limited NMS binding "
+ getClass().getSimpleName()
+ "; set general.disableNMS=false and use the supported Minecraft 26.2 server runtime");
}
}
@@ -19,6 +19,7 @@
package art.arcane.iris.core.pack;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -42,6 +43,21 @@ public final class PackValidationRegistry {
return RESULTS.get(packName);
}
public static PackValidationResult requireLoadable(String packName) {
if (packName == null || packName.isBlank()) {
throw new IllegalArgumentException("Pack name is required for validation");
}
PackValidationResult result = get(packName);
if (result == null) {
throw new BrokenPackException(packName, List.of(
"Required pack validation has not completed. World creation fails closed until validation succeeds."));
}
if (!result.isLoadable()) {
throw new BrokenPackException(packName, result.getBlockingErrors());
}
return result;
}
public static boolean isBroken(String packName) {
PackValidationResult result = get(packName);
return result != null && !result.isLoadable();
@@ -19,6 +19,10 @@
package art.arcane.iris.core.pack;
import art.arcane.iris.engine.object.IrisBiomeCustomSpawnType;
import art.arcane.iris.engine.object.IrisLoot;
import art.arcane.iris.engine.object.IrisLootReference;
import art.arcane.iris.engine.object.IrisLootTable;
import art.arcane.iris.engine.object.ObjectPlaceMode;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformEntityType;
@@ -35,6 +39,7 @@ import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -51,11 +56,13 @@ public final class PackValidator {
private static final String DATAPACKS_FOLDER = "datapacks";
private static final String CACHE_FOLDER = "cache";
private static final String OBJECTS_FOLDER = "objects";
private static final String LOOT_FOLDER = "loot";
private static final String DIMENSIONS_FOLDER = "dimensions";
private static final String STRUCTURES_FOLDER = "structures";
private static final String JIGSAW_POOLS_FOLDER = "jigsaw-pools";
private static final String JIGSAW_PIECES_FOLDER = "jigsaw-pieces";
private static final List<String> STRUCTURE_HOST_FOLDERS = List.of(DIMENSIONS_FOLDER, "regions", "biomes");
private static final List<String> REMOVED_WORLDGEN_FIELDS = List.of("fluidBodies");
private static final List<String> UNSUPPORTED_STRUCTURE_TRANSFORM_FIELDS = List.of("rotation", "translate", "scale");
private static final Pattern RESOURCE_KEY_PATTERN = Pattern.compile("[a-z0-9_.-]+:[a-z0-9/._-]+");
@@ -86,8 +93,19 @@ public final class PackValidator {
}
validateDimensions(packFolder, dimensionFiles, blockingErrors, warnings);
blockingErrors.addAll(validateLootGraph(packFolder));
blockingErrors.addAll(validateRemovedWorldgenFields(packFolder));
blockingErrors.addAll(validateUnsupportedStructureTransforms(packFolder));
blockingErrors.addAll(validateStructureGraph(packFolder));
StructureGraphPackValidator.Validation compiledStructures =
StructureGraphPackValidator.validate(
packFolder.toPath(), collectPlacedStructureKeys(packFolder));
addDistinct(blockingErrors, compiledStructures.errors());
addDistinct(warnings, compiledStructures.warnings());
blockingErrors.addAll(validateNativeStructureReplacements(
packFolder,
compiledStructures.replacementOutputStructures(),
compiledStructures.sampledVerticalEnvelopes()));
blockingErrors.addAll(validateSpawnerEntityReferences(
new File(packFolder, "spawners"), new File(packFolder, "entities")));
blockingErrors.addAll(validateCustomBiomeSpawns(
@@ -98,6 +116,54 @@ public final class PackValidator {
return new PackValidationResult(packName, blockingErrors, warnings, validatedAt);
}
private static void addDistinct(List<String> destination, List<String> additions) {
for (String addition : additions) {
if (!destination.contains(addition)) {
destination.add(addition);
}
}
}
static Set<String> collectPlacedStructureKeys(File packFolder) {
Set<String> structureKeys = new LinkedHashSet<>();
if (packFolder == null || !packFolder.isDirectory()) {
return structureKeys;
}
for (String folderName : STRUCTURE_HOST_FOLDERS) {
File resourceFolder = new File(packFolder, folderName);
if (!resourceFolder.isDirectory()) {
continue;
}
for (File resourceFile : listJsonRecursive(resourceFolder)) {
JSONObject resource = 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()) {
@@ -165,6 +231,535 @@ public final class PackValidator {
return blockingErrors;
}
static List<String> validateLootGraph(File packFolder) {
List<String> blockingErrors = new ArrayList<>();
if (packFolder == null || !packFolder.isDirectory()) {
return blockingErrors;
}
File lootFolder = new File(packFolder, LOOT_FOLDER);
Set<String> lootKeys = deriveRegistrantKeysExact(lootFolder);
if (lootFolder.isDirectory()) {
List<File> lootFiles = listJsonRecursive(lootFolder);
lootFiles.sort(Comparator.comparing(File::getPath));
for (File lootFile : lootFiles) {
String lootKey = deriveKey(lootFolder, lootFile);
JSONObject table = readGraphJson(lootFile, "Loot table", lootKey, blockingErrors);
if (table != null) {
validateLootTable(lootKey, table, blockingErrors);
}
}
}
for (String folderName : STRUCTURE_HOST_FOLDERS) {
File resourceFolder = new File(packFolder, folderName);
if (!resourceFolder.isDirectory()) {
continue;
}
List<File> resourceFiles = listJsonRecursive(resourceFolder);
resourceFiles.sort(Comparator.comparing(File::getPath));
for (File resourceFile : resourceFiles) {
JSONObject resource = readJson(resourceFile);
if (resource == null || !resource.has("loot")) {
continue;
}
String resourceType = structureHostType(folderName);
String resourceKey = deriveKey(resourceFolder, resourceFile);
validateLootReference(resourceType, resourceKey, resource.opt("loot"), lootKeys, blockingErrors);
}
}
return blockingErrors;
}
static List<String> validateRemovedWorldgenFields(File packFolder) {
List<String> blockingErrors = new ArrayList<>();
if (packFolder == null || !packFolder.isDirectory()) {
return blockingErrors;
}
for (String folderName : STRUCTURE_HOST_FOLDERS) {
File resourceFolder = new File(packFolder, folderName);
if (!resourceFolder.isDirectory()) {
continue;
}
List<File> resourceFiles = listJsonRecursive(resourceFolder);
resourceFiles.sort(Comparator.comparing(File::getPath));
String resourceType = structureHostType(folderName);
for (File resourceFile : resourceFiles) {
JSONObject resource = readJson(resourceFile);
if (resource == null) {
continue;
}
String resourceKey = deriveKey(resourceFolder, resourceFile);
for (String field : 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;
}
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 + "'.");
}
}
}
private 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();
}
private 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 + ".");
}
}
private 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 + ".");
}
}
private 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 + ".");
}
}
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, STRUCTURES_FOLDER);
Map<String, JSONObject> structures = new HashMap<>();
for (File structureFile : listJsonRecursive(structuresFolder)) {
JSONObject structure = readJson(structureFile);
if (structure != null) {
structures.put(deriveKey(structuresFolder, structureFile), structure);
}
}
for (String folderName : STRUCTURE_HOST_FOLDERS) {
File resourceFolder = new File(packFolder, folderName);
if (!resourceFolder.isDirectory()) {
continue;
}
List<File> resourceFiles = listJsonRecursive(resourceFolder);
resourceFiles.sort(Comparator.comparing(File::getPath));
String resourceType = structureHostType(folderName);
for (File resourceFile : resourceFiles) {
JSONObject resource = readJson(resourceFile);
if (resource == null) {
continue;
}
JSONArray placements = resource.optJSONArray("structures");
if (placements == null) {
continue;
}
String resourceKey = 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 (!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");
if (references == null || references.length() == 0) {
blockingErrors.add("Dimension '" + resourceKey + "' structures[" + placementIndex
+ "] requests REPLACE_SOURCE without any Iris structure references.");
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 (!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 static void validateStructurePlacements(File packFolder,
Set<String> structureKeys,
List<String> blockingErrors) {
@@ -786,6 +1381,12 @@ public final class PackValidator {
private record ReferencedContentKeys(Set<String> blocks, Set<String> items, Set<String> entities) {
}
private record DimensionVerticalBounds(long minimumY, long maximumY) {
}
private record PlacementVerticalBounds(long minimumY, long maximumY, boolean underground) {
}
record SpawnCategoryResolution(boolean entityKnown, String category) {
static SpawnCategoryResolution unknown() {
return new SpawnCategoryResolution(false, null);
@@ -810,6 +1411,8 @@ public final class PackValidator {
continue;
}
validateImportedStructurePolicy(dimensionKey, dimJson, blockingErrors);
JSONArray regionsArray = dimJson.optJSONArray("regions");
if (regionsArray == null || regionsArray.length() == 0) {
blockingErrors.add("Dimension '" + dimensionKey + "' declares no regions.");
@@ -853,6 +1456,68 @@ public final class PackValidator {
}
}
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);
}
}
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) {
@@ -0,0 +1,351 @@
/*
* 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.framework.PlacedStructurePiece;
import art.arcane.iris.engine.framework.StructureAssembler;
import art.arcane.iris.engine.framework.structure.IrisObjectFrameReader;
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.StructureGraphAssemblySample;
import art.arcane.iris.engine.framework.structure.StructureGraphResolver;
import art.arcane.iris.engine.object.IrisJigsawPiece;
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.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.BufferedInputStream;
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.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
final class StructureGraphPackValidator {
private static final Gson GSON = new GsonBuilder().create();
private static final List<Long> GEOMETRY_SAMPLE_SEEDS = List.of(
0L, 1L, 2L, 3L, 5L, 8L, 13L, 21L, 34L, 55L, 89L, 144L, 233L, 377L, 610L, 987L);
private static final int GEOMETRY_SAMPLE_ORIGIN_Y = 64;
private StructureGraphPackValidator() {
}
static Validation validate(Path packRoot) {
return validate(packRoot, null);
}
static Validation validate(Path packRoot, Set<String> activeStructureKeys) {
Path normalizedRoot = packRoot.toAbsolutePath().normalize();
Path structuresRoot = normalizedRoot.resolve("structures");
Set<String> errors = new LinkedHashSet<>();
Set<String> warnings = new LinkedHashSet<>();
Set<String> replacementOutputStructures = new LinkedHashSet<>();
Map<String, List<SampledVerticalEnvelope>> sampledVerticalEnvelopes = new LinkedHashMap<>();
FileResolver resolver = new FileResolver(normalizedRoot);
for (Path structureFile : listJsonFiles(structuresRoot)) {
String structureKey = resourceKey(structuresRoot, structureFile, ".json");
if (structureKey.equals("structure-index")) {
continue;
}
IrisStructure structure = readJson(structureFile, IrisStructure.class);
if (structure == null) {
continue;
}
structure.setLoadKey(structureKey);
if (activeStructureKeys != null && !activeStructureKeys.contains(structureKey)) {
continue;
}
StructureGraphCompilation compilation;
try {
compilation = StructureGraphCompiler.compile(structure, resolver);
} catch (MalformedObjectResourceException e) {
errors.add(e.getMessage());
continue;
}
for (StructureGraphDiagnostic diagnostic : compilation.getDiagnostics()) {
if (diagnostic.severity() == StructureGraphDiagnostic.Severity.ERROR) {
errors.add(diagnostic.message());
} else {
warnings.add(diagnostic.message());
}
}
if (!compilation.isAssemblyViable()) {
errors.add("Structure '" + structureKey
+ "' does not produce a complete deterministic assembly and will not generate"
+ failedSampleSummary(compilation) + ".");
continue;
}
RuntimeGeometryValidation geometry = validateRuntimeGeometry(compilation);
if (geometry.failures().isEmpty()) {
sampledVerticalEnvelopes.put(structureKey, geometry.sampledVerticalEnvelopes());
if (compilation.guaranteesAssemblyOutput() && geometry.outputForEverySample()) {
replacementOutputStructures.add(structureKey);
}
} else {
errors.add("Structure '" + structureKey
+ "' fails sampled collision- and radius-aware assembly: "
+ String.join("; ", geometry.failures()) + ".");
}
}
return new Validation(
List.copyOf(errors), List.copyOf(warnings), Set.copyOf(replacementOutputStructures),
immutableEnvelopes(sampledVerticalEnvelopes));
}
private static RuntimeGeometryValidation validateRuntimeGeometry(
StructureGraphCompilation compilation
) {
List<String> failures = new ArrayList<>();
List<SampledVerticalEnvelope> sampledVerticalEnvelopes = new ArrayList<>();
boolean outputForEverySample = true;
for (long seed : GEOMETRY_SAMPLE_SEEDS) {
try {
StructureAssembler assembler = StructureAssembler.forCompilation(
compilation, new IrisPosition(0, GEOMETRY_SAMPLE_ORIGIN_Y, 0));
KList<PlacedStructurePiece> pieces = assembler.assemble(new RNG(seed));
if (pieces == null) {
failures.add("seed " + seed + " returned no complete assembly");
} else if (pieces.isEmpty()) {
outputForEverySample = false;
} else {
sampledVerticalEnvelopes.add(sampleVerticalEnvelope(seed, pieces));
}
} catch (RuntimeException e) {
failures.add("seed " + seed + " threw " + e.getClass().getSimpleName()
+ ": " + failureMessage(e));
}
}
return new RuntimeGeometryValidation(
List.copyOf(failures), outputForEverySample, List.copyOf(sampledVerticalEnvelopes));
}
private static SampledVerticalEnvelope sampleVerticalEnvelope(
long seed,
KList<PlacedStructurePiece> pieces
) {
int minimumY = Integer.MAX_VALUE;
int maximumY = Integer.MIN_VALUE;
for (PlacedStructurePiece piece : pieces) {
minimumY = Math.min(minimumY, piece.getMinY());
maximumY = Math.max(maximumY, piece.getMaxY());
}
return new SampledVerticalEnvelope(
seed,
pieces.size(),
Math.subtractExact(minimumY, GEOMETRY_SAMPLE_ORIGIN_Y),
Math.subtractExact(maximumY, GEOMETRY_SAMPLE_ORIGIN_Y));
}
private static Map<String, List<SampledVerticalEnvelope>> immutableEnvelopes(
Map<String, List<SampledVerticalEnvelope>> sampledVerticalEnvelopes
) {
Map<String, List<SampledVerticalEnvelope>> immutable = new LinkedHashMap<>();
for (Map.Entry<String, List<SampledVerticalEnvelope>> entry : sampledVerticalEnvelopes.entrySet()) {
immutable.put(entry.getKey(), List.copyOf(entry.getValue()));
}
return Map.copyOf(immutable);
}
private static String failureMessage(RuntimeException exception) {
return exception.getMessage() == null || exception.getMessage().isBlank()
? "no failure detail" : exception.getMessage();
}
private static String failedSampleSummary(StructureGraphCompilation compilation) {
List<String> failures = new ArrayList<>();
for (StructureGraphAssemblySample sample : compilation.getAssemblySamples()) {
if (sample.outcome().isComplete()) {
continue;
}
failures.add("seed " + sample.seed() + " placed " + sample.outcome().pieceKeys().size()
+ " piece(s), left " + sample.outcome().unresolvedConnectorCount()
+ " unresolved, cap=" + sample.outcome().pieceCapReached());
if (failures.size() == 3) {
break;
}
}
return failures.isEmpty() ? "" : " (" + String.join("; ", failures) + ")";
}
private static List<Path> listJsonFiles(Path root) {
if (!Files.isDirectory(root)) {
return List.of();
}
try (Stream<Path> paths = Files.walk(root)) {
return paths.filter(Files::isRegularFile)
.filter(path -> path.getFileName().toString().endsWith(".json"))
.sorted()
.toList();
} catch (IOException e) {
return List.of();
}
}
private static String resourceKey(Path root, Path file, String extension) {
String relative = root.relativize(file).toString().replace(file.getFileSystem().getSeparator(), "/");
return relative.substring(0, relative.length() - extension.length());
}
private static <T> T readJson(Path file, Class<T> type) {
try {
return GSON.fromJson(Files.readString(file, StandardCharsets.UTF_8), type);
} catch (IOException | RuntimeException e) {
return null;
}
}
record Validation(
List<String> errors,
List<String> warnings,
Set<String> replacementOutputStructures,
Map<String, List<SampledVerticalEnvelope>> sampledVerticalEnvelopes
) {
Validation {
errors = List.copyOf(errors);
warnings = List.copyOf(warnings);
replacementOutputStructures = Set.copyOf(replacementOutputStructures);
sampledVerticalEnvelopes = immutableEnvelopes(sampledVerticalEnvelopes);
}
}
record SampledVerticalEnvelope(long seed, int pieceCount, int minimumYOffset, int maximumYOffset) {
SampledVerticalEnvelope {
if (pieceCount <= 0) {
throw new IllegalArgumentException("Sampled vertical envelope must contain at least one piece");
}
if (maximumYOffset < minimumYOffset) {
throw new IllegalArgumentException("Sampled vertical envelope maximum Y cannot be below minimum Y");
}
}
}
private record RuntimeGeometryValidation(
List<String> failures,
boolean outputForEverySample,
List<SampledVerticalEnvelope> sampledVerticalEnvelopes
) {
}
private static final class FileResolver implements StructureGraphResolver {
private final Path packRoot;
private final Map<String, IrisJigsawPool> pools = new HashMap<>();
private final Map<String, IrisJigsawPiece> pieces = new HashMap<>();
private final Map<String, IrisObject> objects = new HashMap<>();
private final Set<String> missingPools = new LinkedHashSet<>();
private final Set<String> missingPieces = new LinkedHashSet<>();
private final Set<String> missingObjects = new LinkedHashSet<>();
private FileResolver(Path packRoot) {
this.packRoot = packRoot;
}
@Override
public IrisJigsawPool loadPool(String key) {
if (pools.containsKey(key)) {
return pools.get(key);
}
if (missingPools.contains(key)) {
return null;
}
Path file = resolve("jigsaw-pools", key, ".json");
IrisJigsawPool pool = file == null ? null : readJson(file, IrisJigsawPool.class);
if (pool == null) {
missingPools.add(key);
return null;
}
pools.put(key, pool);
return pool;
}
@Override
public IrisJigsawPiece loadPiece(String key) {
if (pieces.containsKey(key)) {
return pieces.get(key);
}
if (missingPieces.contains(key)) {
return null;
}
Path file = resolve("jigsaw-pieces", key, ".json");
IrisJigsawPiece piece = file == null ? null : readJson(file, IrisJigsawPiece.class);
if (piece == null) {
missingPieces.add(key);
return null;
}
pieces.put(key, piece);
return piece;
}
@Override
public IrisObject loadObject(String key) {
if (objects.containsKey(key)) {
return objects.get(key);
}
if (missingObjects.contains(key)) {
return null;
}
Path file = resolve("objects", key, ".iob");
IrisObject object = file == null ? null : readObjectBounds(file);
if (object == null) {
missingObjects.add(key);
return null;
}
objects.put(key, object);
return object;
}
private Path resolve(String folder, String key, String extension) {
if (key == null || key.isBlank() || key.indexOf('\\') >= 0 || key.indexOf(':') >= 0) {
return null;
}
Path root = packRoot.resolve(folder).normalize();
Path file = root.resolve(key + extension).normalize();
if (!file.startsWith(root) || !Files.isRegularFile(file)) {
return null;
}
return file;
}
private IrisObject readObjectBounds(Path file) {
try (BufferedInputStream input = new BufferedInputStream(Files.newInputStream(file))) {
return IrisObjectFrameReader.readBounds(input, file.toString());
} catch (IOException e) {
throw new MalformedObjectResourceException(e.getMessage(), e);
}
}
}
private static final class MalformedObjectResourceException extends RuntimeException {
private MalformedObjectResourceException(String message, IOException cause) {
super(message, cause);
}
}
}
@@ -0,0 +1,518 @@
package art.arcane.iris.core.pack;
import art.arcane.iris.core.structure.authoring.StructureResourceBundle;
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;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
public final class StructurePackageClosure {
private static final String STRUCTURES = "structures";
private static final String POOLS = "jigsaw-pools";
private static final String PIECES = "jigsaw-pieces";
private static final String OBJECTS = "objects";
private static final String LOOT = "loot";
private final Path sourceRoot;
private final Set<String> structures;
private final Set<String> pools;
private final Set<String> pieces;
private final Set<String> objects;
private final Set<String> loot;
private final List<String> errors;
private StructurePackageClosure(Path sourceRoot, MutableClosure closure) {
this.sourceRoot = sourceRoot;
this.structures = Collections.unmodifiableSet(new LinkedHashSet<>(closure.structures));
this.pools = Collections.unmodifiableSet(new LinkedHashSet<>(closure.pools));
this.pieces = Collections.unmodifiableSet(new LinkedHashSet<>(closure.pieces));
this.objects = Collections.unmodifiableSet(new LinkedHashSet<>(closure.objects));
this.loot = Collections.unmodifiableSet(new LinkedHashSet<>(closure.loot));
this.errors = Collections.unmodifiableList(new ArrayList<>(closure.errors));
}
public static StructurePackageClosure collect(File sourceRoot, Collection<String> rootStructures) {
Path normalizedRoot = sourceRoot.toPath().toAbsolutePath().normalize();
MutableClosure closure = new MutableClosure();
if (!Files.isDirectory(normalizedRoot)) {
closure.errors.add("Structure package source is not a directory: " + normalizedRoot);
return new StructurePackageClosure(normalizedRoot, closure);
}
if (rootStructures != null) {
for (String structureKey : rootStructures) {
enqueueKey(closure.structureQueue, structureKey, "structure", closure.errors);
}
}
collectStructures(normalizedRoot, closure);
collectPools(normalizedRoot, closure);
collectPieces(normalizedRoot, closure);
validateBinaryResources(normalizedRoot, closure);
validatePortableCollisions(closure.structures, STRUCTURES, closure.errors);
validatePortableCollisions(closure.pools, POOLS, closure.errors);
validatePortableCollisions(closure.pieces, PIECES, closure.errors);
validatePortableCollisions(closure.objects, OBJECTS, closure.errors);
validatePortableCollisions(closure.loot, LOOT, closure.errors);
return new StructurePackageClosure(normalizedRoot, closure);
}
public boolean isValid() {
return errors.isEmpty();
}
public Set<String> structures() {
return structures;
}
public Set<String> pools() {
return pools;
}
public Set<String> pieces() {
return pieces;
}
public Set<String> objects() {
return objects;
}
public Set<String> loot() {
return loot;
}
public List<String> errors() {
return errors;
}
public String writeTo(File targetRoot, boolean minify) throws IOException {
if (!isValid()) {
throw new IOException("Cannot package an invalid structure graph: " + String.join("; ", errors));
}
Path normalizedTarget = targetRoot.toPath().toAbsolutePath().normalize();
prepareTargetRoot(normalizedTarget);
StringBuilder hashes = new StringBuilder();
copyJsonResources(normalizedTarget, STRUCTURES, structures, minify, hashes);
copyJsonResources(normalizedTarget, POOLS, pools, minify, hashes);
copyJsonResources(normalizedTarget, PIECES, pieces, minify, hashes);
copyJsonResources(normalizedTarget, LOOT, loot, minify, hashes);
copyBinaryResources(normalizedTarget, OBJECTS, objects, hashes);
return IO.hash(hashes.toString());
}
private static void collectStructures(Path sourceRoot, MutableClosure closure) {
while (!closure.structureQueue.isEmpty()) {
String structureKey = closure.structureQueue.removeFirst();
if (!closure.structures.add(structureKey)) {
continue;
}
JSONObject structure = readJson(sourceRoot, STRUCTURES, structureKey, closure.errors);
if (structure == null) {
continue;
}
String startPool = requiredString(structure, "startPool", "Structure '" + structureKey + "'", closure.errors);
if (startPool != null) {
enqueueKey(closure.poolQueue, startPool,
"start pool for structure '" + structureKey + "'", closure.errors);
}
JSONArray loot = optionalArray(structure, "loot", "Structure '" + structureKey + "'", closure.errors);
addArrayKeys(loot, closure.loot, "loot table", closure.errors);
}
}
private static void collectPools(Path sourceRoot, MutableClosure closure) {
while (!closure.poolQueue.isEmpty()) {
String poolKey = closure.poolQueue.removeFirst();
if (!closure.pools.add(poolKey)) {
continue;
}
JSONObject pool = readJson(sourceRoot, POOLS, poolKey, closure.errors);
if (pool == null) {
continue;
}
JSONArray entries = requiredArray(pool, "pieces", "Jigsaw pool '" + poolKey + "'", closure.errors);
if (entries != null) {
for (int index = 0; index < entries.length(); index++) {
JSONObject entry = entries.optJSONObject(index);
if (entry == null) {
closure.errors.add("Jigsaw pool '" + poolKey + "' has a non-object piece entry at index " + index + ".");
continue;
}
if (isEmptyEntry(entry, poolKey, index, closure.errors)) {
continue;
}
String pieceKey = requiredString(entry, "piece",
"Piece entry " + index + " in jigsaw pool '" + poolKey + "'", closure.errors);
if (pieceKey != null) {
enqueueKey(closure.pieceQueue, pieceKey,
"piece in pool '" + poolKey + "'", closure.errors);
}
}
}
String fallback = optionalString(pool, "fallback", "Jigsaw pool '" + poolKey + "'", closure.errors);
enqueueOptionalKey(closure.poolQueue, fallback,
"fallback pool for '" + poolKey + "'", closure.errors);
}
}
private static void collectPieces(Path sourceRoot, MutableClosure closure) {
while (!closure.pieceQueue.isEmpty()) {
String pieceKey = closure.pieceQueue.removeFirst();
if (!closure.pieces.add(pieceKey)) {
continue;
}
JSONObject piece = readJson(sourceRoot, PIECES, pieceKey, closure.errors);
if (piece == null) {
continue;
}
String objectKey = requiredString(piece, "object", "Jigsaw piece '" + pieceKey + "'", closure.errors);
if (objectKey != null) {
addRequiredKey(closure.objects, objectKey,
"object for piece '" + pieceKey + "'", closure.errors);
}
JSONArray connectors = optionalArray(piece, "connectors", "Jigsaw piece '" + pieceKey + "'", closure.errors);
if (connectors == null) {
continue;
}
for (int index = 0; index < connectors.length(); index++) {
JSONObject connector = connectors.optJSONObject(index);
if (connector == null) {
closure.errors.add("Jigsaw piece '" + pieceKey + "' has a non-object connector at index " + index + ".");
continue;
}
String poolKey = requiredString(connector, "pool",
"Connector " + index + " in jigsaw piece '" + pieceKey + "'", closure.errors);
if (poolKey != null) {
enqueueKey(closure.poolQueue, poolKey,
"connector pool for piece '" + pieceKey + "'", closure.errors);
}
}
collectPools(sourceRoot, closure);
}
}
private static void validateBinaryResources(Path sourceRoot, MutableClosure closure) {
for (String objectKey : closure.objects) {
resolveExisting(sourceRoot, OBJECTS, objectKey, ".iob", closure.errors);
}
for (String lootKey : closure.loot) {
resolveExisting(sourceRoot, LOOT, lootKey, ".json", closure.errors);
}
}
private static JSONObject readJson(Path sourceRoot, String folder, String key, List<String> errors) {
Path file = resolveExisting(sourceRoot, folder, key, ".json", errors);
if (file == null) {
return null;
}
try {
return new JSONObject(Files.readString(file, StandardCharsets.UTF_8));
} catch (IOException | RuntimeException e) {
errors.add("Invalid " + folder + " resource '" + key + "': " + describe(e));
return null;
}
}
private static Path resolveExisting(Path sourceRoot, String folder, String key, String extension,
List<String> errors) {
Path file = resolveResource(sourceRoot, folder, key, extension, errors);
if (file != null && !Files.isRegularFile(file)) {
errors.add("Missing " + folder + " resource '" + key + "'.");
return null;
}
if (file != null && !isContainedSourceResource(sourceRoot, folder, key, extension, file, errors)) {
return null;
}
return file;
}
private static Path resolveResource(Path root, String folder, String key, String extension, List<String> errors) {
if (!validKey(key)) {
errors.add("Invalid " + folder + " resource key '" + key + "'.");
return null;
}
Path folderRoot = root.resolve(folder).normalize();
Path file = folderRoot.resolve(key + extension).normalize();
if (!file.startsWith(folderRoot)) {
errors.add("Resource key escapes " + folder + ": '" + key + "'.");
return null;
}
return file;
}
private void copyJsonResources(Path targetRoot, String folder, Set<String> keys, boolean minify,
StringBuilder hashes) throws IOException {
for (String key : keys) {
Path source = resolveExistingOrThrow(sourceRoot, folder, key, ".json");
Path target = prepareTargetResource(targetRoot, folder, key, ".json");
String json = new JSONObject(Files.readString(source, StandardCharsets.UTF_8)).toString(minify ? 0 : 4);
Files.writeString(target, json, StandardCharsets.UTF_8);
hashes.append(IO.hash(json));
}
}
private void copyBinaryResources(Path targetRoot, String folder, Set<String> keys,
StringBuilder hashes) throws IOException {
for (String key : keys) {
Path source = resolveExistingOrThrow(sourceRoot, folder, key, ".iob");
Path target = prepareTargetResource(targetRoot, folder, key, ".iob");
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
hashes.append(IO.hash(source.toFile()));
}
}
private static Path resolveExistingOrThrow(Path root, String folder, String key, String extension) throws IOException {
List<String> errors = new ArrayList<>();
Path file = resolveExisting(root, folder, key, extension, errors);
if (file == null) {
throw new IOException(String.join("; ", errors));
}
return file;
}
private static Path resolveResourceOrThrow(Path root, String folder, String key, String extension) throws IOException {
List<String> errors = new ArrayList<>();
Path file = resolveResource(root, folder, key, extension, errors);
if (file == null) {
throw new IOException(String.join("; ", errors));
}
return file;
}
private static void prepareTargetRoot(Path targetRoot) throws IOException {
if (Files.exists(targetRoot, LinkOption.NOFOLLOW_LINKS)) {
if (!Files.isDirectory(targetRoot)) {
throw new IOException("Structure package target is not a directory: " + targetRoot);
}
return;
}
Files.createDirectories(targetRoot);
}
private static Path prepareTargetResource(Path targetRoot, String folder, String key, String extension) throws IOException {
Path target = resolveResourceOrThrow(targetRoot, folder, key, extension);
Path parent = target.getParent();
Path current = targetRoot;
Path relativeParent = targetRoot.relativize(parent);
for (Path segment : relativeParent) {
Path next = current.resolve(segment);
if (Files.exists(next, LinkOption.NOFOLLOW_LINKS)) {
if (Files.isSymbolicLink(next) || !Files.isDirectory(next, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Structure package target contains a non-directory or symbolic link: " + next);
}
} else {
Files.createDirectory(next);
}
current = next;
}
if (Files.isSymbolicLink(target)) {
throw new IOException("Structure package target is a symbolic link: " + target);
}
return target;
}
private static boolean isContainedSourceResource(Path sourceRoot, String folder, String key, String extension,
Path file, List<String> errors) {
try {
Path realSourceRoot = sourceRoot.toRealPath();
Path realFile = file.toRealPath();
if (!realFile.startsWith(realSourceRoot)) {
errors.add("Resource escapes the structure package through a symbolic link: "
+ folder + "/" + key + extension + ".");
return false;
}
if (!hasExactSourceCase(sourceRoot, file)) {
errors.add("Resource path casing does not match its reference: "
+ folder + "/" + key + extension + ".");
return false;
}
return true;
} catch (IOException exception) {
errors.add("Cannot verify " + folder + " resource '" + key + "': " + describe(exception));
return false;
}
}
private static boolean hasExactSourceCase(Path sourceRoot, Path file) throws IOException {
Path current = sourceRoot;
for (Path segment : sourceRoot.relativize(file)) {
Path exactChild = findExactChild(current, segment.toString());
if (exactChild == null) {
return false;
}
current = exactChild;
}
return true;
}
private static Path findExactChild(Path folder, String name) throws IOException {
try (DirectoryStream<Path> children = Files.newDirectoryStream(folder)) {
for (Path child : children) {
if (child.getFileName().toString().equals(name)) {
return child;
}
}
}
return null;
}
private static void addArrayKeys(JSONArray values, Set<String> target, String kind, List<String> errors) {
if (values == null) {
return;
}
for (int index = 0; index < values.length(); index++) {
Object raw = values.opt(index);
if (!(raw instanceof String value)) {
errors.add("Invalid " + kind + " reference at index " + index + ".");
continue;
}
addRequiredKey(target, value, kind, errors);
}
}
private static void addRequiredKey(Set<String> target, String key, String kind, List<String> errors) {
String canonical = canonicalKey(key);
if (canonical == null) {
errors.add("Missing or invalid " + kind + ".");
return;
}
target.add(canonical);
}
private static void enqueueKey(Deque<String> queue, String key, String kind, List<String> errors) {
String canonical = canonicalKey(key);
if (canonical == null) {
errors.add("Missing or invalid " + kind + ".");
return;
}
queue.addLast(canonical);
}
private static void enqueueOptionalKey(Deque<String> queue, String key, String kind, List<String> errors) {
if (key == null || key.isBlank()) {
return;
}
enqueueKey(queue, key, kind, errors);
}
private static String canonicalKey(String key) {
if (key == null || !key.equals(key.trim())) {
return null;
}
return validKey(key) ? key : null;
}
private static boolean validKey(String key) {
if (key == null || key.isBlank() || !key.equals(key.trim())) {
return false;
}
try {
StructureResourceBundle.validateRelativePath(key);
return true;
} catch (IllegalArgumentException exception) {
return false;
}
}
private static String requiredString(JSONObject object, String field, String resource, List<String> errors) {
Object value = object.opt(field);
if (value instanceof String stringValue) {
return stringValue;
}
errors.add(resource + " requires string field '" + field + "'.");
return null;
}
private static String optionalString(JSONObject object, String field, String resource, List<String> errors) {
if (!object.has(field)) {
return null;
}
return requiredString(object, field, resource, errors);
}
private static JSONArray requiredArray(JSONObject object, String field, String resource, List<String> errors) {
Object value = object.opt(field);
if (value instanceof JSONArray arrayValue) {
return arrayValue;
}
errors.add(resource + " requires array field '" + field + "'.");
return null;
}
private static JSONArray optionalArray(JSONObject object, String field, String resource, List<String> errors) {
if (!object.has(field)) {
return null;
}
return requiredArray(object, field, resource, errors);
}
private static boolean isEmptyEntry(JSONObject entry, String poolKey, int index, List<String> errors) {
if (!entry.has("empty")) {
return false;
}
Object emptyValue = entry.opt("empty");
if (!(emptyValue instanceof Boolean isEmpty)) {
errors.add("Piece entry " + index + " in jigsaw pool '" + poolKey + "' requires boolean field 'empty'.");
return false;
}
if (!isEmpty) {
return false;
}
if (entry.has("piece")) {
errors.add("Empty piece entry " + index + " in jigsaw pool '" + poolKey + "' cannot define field 'piece'.");
}
return true;
}
private static void validatePortableCollisions(Set<String> keys, String folder, List<String> errors) {
Map<String, String> portableKeys = new HashMap<>();
for (String key : keys) {
String portableKey = key.toLowerCase(Locale.ROOT);
String previous = portableKeys.putIfAbsent(portableKey, key);
if (previous != null && !previous.equals(key)) {
errors.add("Case-insensitive " + folder + " resource collision between '"
+ previous + "' and '" + key + "'.");
}
}
}
private static String describe(Exception exception) {
String message = exception.getMessage();
return message == null || message.isBlank() ? exception.getClass().getSimpleName() : message;
}
private static final class MutableClosure {
private final Set<String> structures = new LinkedHashSet<>();
private final Set<String> pools = new LinkedHashSet<>();
private final Set<String> pieces = new LinkedHashSet<>();
private final Set<String> objects = new LinkedHashSet<>();
private final Set<String> loot = new LinkedHashSet<>();
private final List<String> errors = new ArrayList<>();
private final Deque<String> structureQueue = new ArrayDeque<>();
private final Deque<String> poolQueue = new ArrayDeque<>();
private final Deque<String> pieceQueue = new ArrayDeque<>();
}
}
@@ -25,6 +25,7 @@ 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;
@@ -37,6 +38,7 @@ 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.volmlib.util.collection.KList;
@@ -69,7 +71,9 @@ import java.awt.Desktop;
import java.awt.GraphicsEnvironment;
import java.io.File;
import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -623,7 +627,13 @@ public class IrisProject {
IrisData dm = IrisData.get(path);
IrisDimension dimension = dm.getDimensionLoader().load(dimm);
File folder = new File(IrisPlatforms.get().dataFolder(), "exports/" + dimension.getLoadKey());
folder.mkdirs();
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<>();
@@ -647,6 +657,10 @@ public class IrisProject {
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();
@@ -704,6 +718,11 @@ public class IrisProject {
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));
@@ -778,6 +797,20 @@ public class IrisProject {
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<>();
@@ -293,7 +293,6 @@ public class SchemaBuilder {
if (!definitions.containsKey(key)) {
JSONObject j = new JSONObject();
j.put("enum", new JSONArray(StructureSchemaKeys.collect(
IrisPlatforms.get().registries().structureKeys(),
Arrays.asList(data.getStructureLoader().getPossibleKeys()),
Arrays.asList(data.getJigsawPieceLoader().getPossibleKeys())).toArray(new String[0])));
definitions.put(key, j);
@@ -585,7 +584,6 @@ public class SchemaBuilder {
if (!definitions.containsKey(key)) {
JSONObject j = new JSONObject();
j.put("enum", new JSONArray(StructureSchemaKeys.collect(
IrisPlatforms.get().registries().structureKeys(),
Arrays.asList(data.getStructureLoader().getPossibleKeys()),
Arrays.asList(data.getJigsawPieceLoader().getPossibleKeys())).toArray(new String[0])));
definitions.put(key, j);
@@ -595,6 +593,42 @@ public class SchemaBuilder {
items.put("$ref", "#/definitions/" + key);
prop.put("items", items);
description.add(SYMBOL_TYPE__N + " Must be a valid vanilla, datapack, or imported Iris structure (use ctrl+space for auto complete!)");
} else if (k.isAnnotationPresent(RegistryListVanillaStructure.class)) {
fancyType = "List<Vanilla Structure>";
String key = "enum-vanilla-structure";
if (!definitions.containsKey(key)) {
JSONObject j = new JSONObject();
JSONArray values = new JSONArray();
for (String structureKey : IrisPlatforms.get().registries().structureKeys()) {
values.put(structureKey);
}
j.put("enum", values);
definitions.put(key, j);
}
JSONObject items = new JSONObject();
items.put("$ref", "#/definitions/" + key);
prop.put("items", items);
description.add(SYMBOL_TYPE__N + " Must be a valid vanilla/datapack structure key (use ctrl+space for auto complete!)");
} else if (k.isAnnotationPresent(RegistryListVanillaStructureSet.class)) {
fancyType = "List<Vanilla Structure Set>";
String key = "enum-vanilla-structure-set";
if (!definitions.containsKey(key)) {
JSONObject j = new JSONObject();
JSONArray values = new JSONArray();
for (String structureSetKey : IrisPlatforms.get().structureHooks().structureSetKeys()) {
values.put(structureSetKey);
}
j.put("enum", values);
definitions.put(key, j);
}
JSONObject items = new JSONObject();
items.put("$ref", "#/definitions/" + key);
prop.put("items", items);
description.add(SYMBOL_TYPE__N + " Must be a valid vanilla/datapack structure set key (use ctrl+space for auto complete!)");
} else if (k.isAnnotationPresent(RegistryListBlockType.class)) {
fancyType = "List of Block Types";
String key = "enum-block-type";
@@ -22,12 +22,12 @@ import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.volmlib.util.board.Board;
import art.arcane.volmlib.util.board.BoardProvider;
import art.arcane.volmlib.util.board.BoardSettings;
import art.arcane.volmlib.util.board.ScoreDirection;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form;
import art.arcane.iris.util.common.plugin.IrisService;
@@ -35,6 +35,7 @@ import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.matter.MatterCavern;
import lombok.Data;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
@@ -42,18 +43,23 @@ import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.scoreboard.DisplaySlot;
import org.bukkit.scoreboard.Objective;
import org.bukkit.scoreboard.Scoreboard;
import org.bukkit.scoreboard.Team;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class BoardSVC implements IrisService, BoardProvider {
private final KMap<Player, PlayerBoard> boards = new KMap<>();
private BoardSettings settings;
private boolean boardEnabled;
private final Map<Player, PlayerBoard> boards = new ConcurrentHashMap<>();
private final Set<UUID> hiddenPlayers = ConcurrentHashMap.newKeySet();
private volatile BoardSettings settings;
private volatile boolean boardEnabled;
@Override
public void onEnable() {
@@ -80,16 +86,12 @@ public class BoardSVC implements IrisService, BoardProvider {
return;
}
Objective named = main.getObjective("board");
if (named != null) {
named.unregister();
}
Objective sidebar = main.getObjective(DisplaySlot.SIDEBAR);
if (sidebar != null && "board".equals(sidebar.getName())) {
sidebar.unregister();
Objective objective = main.getObjective("board");
if (objective == null || !"Iris".equalsIgnoreCase(ChatColor.stripColor(objective.getDisplayName()))) {
return;
}
objective.unregister();
Team team = main.getTeam("board");
if (team != null) {
team.unregister();
@@ -106,6 +108,7 @@ public class BoardSVC implements IrisService, BoardProvider {
board.cancel();
}
boards.clear();
hiddenPlayers.clear();
settings = null;
}
@@ -122,6 +125,7 @@ public class BoardSVC implements IrisService, BoardProvider {
@EventHandler
public void on(PlayerQuitEvent e) {
remove(e.getPlayer());
clearPlayerPreference(e.getPlayer().getUniqueId());
}
public void updatePlayer(Player p) {
@@ -152,12 +156,19 @@ public class BoardSVC implements IrisService, BoardProvider {
return;
}
var board = boards.remove(player);
PlayerBoard board = boards.remove(player);
if (board != null) {
board.cancel();
}
}
public boolean toggle(Player player) {
Objects.requireNonNull(player, "player");
boolean visible = togglePlayerBoard(player.getUniqueId());
updatePlayer(player);
return visible;
}
@Override
public String getTitle(Player player) {
return C.GREEN + "Iris";
@@ -177,31 +188,66 @@ public class BoardSVC implements IrisService, BoardProvider {
return false;
}
World world = player.getWorld();
if (!IrisToolbelt.isIrisWorld(world)) {
return false;
return isPlayerBoardEnabled(player.getUniqueId())
&& isStudioGeneratorEligible(IrisToolbelt.access(player.getWorld()));
}
static boolean isStudioGeneratorEligible(PlatformChunkGenerator generator) {
return generator != null
&& generator.isStudio()
&& !generator.isClosing()
&& generator.getEngine() != null;
}
boolean isPlayerBoardEnabled(UUID playerId) {
return playerId != null && !hiddenPlayers.contains(playerId);
}
boolean togglePlayerBoard(UUID playerId) {
Objects.requireNonNull(playerId, "playerId");
if (hiddenPlayers.remove(playerId)) {
return true;
}
PlatformChunkGenerator access = IrisToolbelt.access(world);
return access != null && access.getEngine() != null;
hiddenPlayers.add(playerId);
return false;
}
void clearPlayerPreference(UUID playerId) {
if (playerId != null) {
hiddenPlayers.remove(playerId);
}
}
static Scoreboard selectScoreboardToRestore(Scoreboard active, Scoreboard iris, Scoreboard previous) {
return Objects.equals(active, iris) ? previous : active;
}
@Data
public class PlayerBoard {
private final Player player;
private final Board board;
private final Scoreboard previousScoreboard;
private final Scoreboard irisScoreboard;
private volatile List<String> lines;
private volatile boolean cancelled;
public PlayerBoard(Player player) {
this.player = player;
Scoreboard previous = null;
Scoreboard assigned = null;
try {
previous = player.getScoreboard();
if (Bukkit.getScoreboardManager() != null
&& player.getScoreboard().equals(Bukkit.getScoreboardManager().getMainScoreboard())) {
&& Objects.equals(previous, Bukkit.getScoreboardManager().getMainScoreboard())) {
player.setScoreboard(Bukkit.getScoreboardManager().getNewScoreboard());
}
} catch (Throwable ignored) {
assigned = player.getScoreboard();
} catch (Throwable e) {
IrisLogging.reportError("Failed to prepare the Studio scoreboard for " + player.getName() + ".", e);
}
this.previousScoreboard = previous;
this.irisScoreboard = assigned;
this.board = new Board(player, settings);
this.lines = new ArrayList<>();
this.cancelled = false;
@@ -216,19 +262,13 @@ public class BoardSVC implements IrisService, BoardProvider {
}
private void tick() {
if (!boardEnabled || !player.isOnline()) {
return;
}
if (cancelled) {
board.remove();
if (cancelled || !boardEnabled || !player.isOnline()) {
return;
}
if (!isEligibleWorld(player)) {
boards.remove(player);
cancelled = true;
board.remove();
boards.remove(player, this);
cancel();
return;
}
@@ -243,21 +283,49 @@ public class BoardSVC implements IrisService, BoardProvider {
}
cancelled = true;
if (J.isOwnedByCurrentRegion(player) && player.isOnline()) {
board.remove();
removeNow();
} else {
J.runEntity(player, board::remove);
J.runEntity(player, this::removeNow);
}
}
private void removeNow() {
Scoreboard activeScoreboard = null;
try {
activeScoreboard = player.getScoreboard();
board.remove();
if (!player.isOnline()) {
return;
}
Scoreboard restore = selectScoreboardToRestore(
activeScoreboard,
irisScoreboard,
previousScoreboard);
if (restore != null && !Objects.equals(player.getScoreboard(), restore)) {
player.setScoreboard(restore);
}
} catch (Throwable e) {
IrisLogging.reportError("Failed to remove the Studio scoreboard for " + player.getName() + ".", e);
if (activeScoreboard != null && player.isOnline()) {
player.setScoreboard(activeScoreboard);
}
}
}
public void update() {
final World world = player.getWorld();
final Location loc = player.getLocation();
World world = player.getWorld();
Location loc = player.getLocation();
final var access = IrisToolbelt.access(world);
if (access == null) return;
PlatformChunkGenerator access = IrisToolbelt.access(world);
if (access == null) {
return;
}
final var engine = access.getEngine();
if (engine == null) return;
Engine engine = access.getEngine();
if (engine == null) {
return;
}
int x = loc.getBlockX();
int y = loc.getBlockY() - world.getMinHeight();
@@ -22,7 +22,6 @@ import art.arcane.iris.core.IrisWorldStorage;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.tools.PlausibilizeMode;
import art.arcane.iris.core.tools.TreePlausibilizer;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.util.common.format.C;
@@ -106,7 +105,7 @@ public final class FeatureImporter {
object.shrinkwrap();
if (row.group().equals("trees") || row.group().equals("fallen_trees")) {
try {
TreePlausibilizer.apply(object, PlausibilizeMode.NORMALIZE, TreePlausibilizer.DEFAULT_SHELL_RADIUS);
TreePlausibilizer.apply(object, TreePlausibilizer.seedOf(row.key() + "#" + written), TreePlausibilizer.DEFAULT_REACH);
} catch (Throwable e) {
IrisLogging.reportError(e);
}
@@ -0,0 +1,35 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.structure;
public final class NativeStructureLocateCapability {
private static final String MONUMENT_KEY = "minecraft:monument";
private static final String UNAVAILABLE_MESSAGE = "Native monument generation is enabled, but synchronous monument locating is unavailable because a cold search can stall the server thread. The locate request was not run.";
private NativeStructureLocateCapability() {
}
public static boolean isPaperUnavailable(String structureKey) {
return structureKey != null && MONUMENT_KEY.equalsIgnoreCase(structureKey.trim());
}
public static String unavailableMessage() {
return UNAVAILABLE_MESSAGE;
}
}
@@ -48,6 +48,7 @@ public final class StructureCaptureImporter {
}
public static Report importAllStructures(IrisData data, StructureImporter.Mode mode, VolmitSender sender) {
StructureImporter.Mode activeMode = mode == null ? StructureImporter.Mode.ADD_ONLY : mode;
if (!INMS.get().supportsStructureCapture()) {
sender.sendMessage(C.YELLOW + "Structure capture is not supported by the active NMS binding; skipping the capture pass.");
return new Report(0, 0, 0, 0);
@@ -65,7 +66,7 @@ public final class StructureCaptureImporter {
}
String name = StructureImporter.deriveName(key);
File structureFile = new File(data.getDataFolder(), "structures/" + name + ".json");
if (structureFile.exists()) {
if (structureFile.exists() && activeMode != StructureImporter.Mode.OVERWRITE) {
continue;
}
targets.add(key);
@@ -100,17 +101,20 @@ public final class StructureCaptureImporter {
continue;
}
object.shrinkwrap();
int span = Math.max(object.getW(), object.getD());
File objectFile = new File(data.getDataFolder(), "objects/" + name + ".iob");
objectFile.getParentFile().mkdirs();
object.write(objectFile);
StructureImporter.writeSinglePieceStructure(data, name, key, span, "CENTER_HEIGHT");
StructureImporter.Result result = StructureImporter.writeSinglePieceStructure(
data, name, key, object, "CENTER_HEIGHT", activeMode);
if (!result.success()) {
failed++;
sender.sendMessage(C.RED + "[fail] " + key + ": " + result.message());
continue;
}
imported++;
sender.sendMessage(C.GRAY + "[capture] " + key + " -> objects/" + name + ".iob (" + object.getW() + "x" + object.getH() + "x" + object.getD() + ")");
} catch (Throwable e) {
failed++;
sender.sendMessage(C.RED + "[fail] " + key + ": " + e.getMessage());
IrisLogging.reportError(e);
e.printStackTrace();
}
int processed = imported + skipped + failed;
@@ -187,22 +191,33 @@ public final class StructureCaptureImporter {
});
if (!scheduled) {
return null;
throw captureFailure(key, anchorChunkX, anchorChunkZ,
"region task was not accepted", null);
}
try {
if (!latch.await(REGION_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
return null;
throw captureFailure(key, anchorChunkX, anchorChunkZ,
"region task did not complete within " + REGION_TIMEOUT_SECONDS + " seconds", null);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
throw captureFailure(key, anchorChunkX, anchorChunkZ,
"interrupted while waiting for the region task", e);
}
if (errorRef.get() != null) {
IrisLogging.reportError(errorRef.get());
return null;
Throwable error = errorRef.get();
if (error != null) {
throw captureFailure(key, anchorChunkX, anchorChunkZ,
"platform placement failed", error);
}
return objectRef.get();
}
static IllegalStateException captureFailure(String key, int chunkX, int chunkZ,
String detail, Throwable cause) {
String message = "Structure capture failed for '" + key + "' at chunk "
+ chunkX + "," + chunkZ + ": " + detail;
return cause == null ? new IllegalStateException(message) : new IllegalStateException(message, cause);
}
}
@@ -19,9 +19,21 @@
package art.arcane.iris.core.structure;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.structure.authoring.IrisStructureBundleFactory;
import art.arcane.iris.core.structure.authoring.StructureBackend;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import art.arcane.iris.core.structure.authoring.StructureKey;
import art.arcane.iris.core.structure.authoring.StructureLoss;
import art.arcane.iris.core.structure.authoring.StructureResourceBundle;
import art.arcane.iris.core.structure.authoring.StructureSource;
import art.arcane.iris.core.structure.authoring.StructureTransactionWriter;
import art.arcane.iris.core.structure.authoring.StructureWriteMode;
import art.arcane.iris.core.structure.authoring.StructureWriteResult;
import art.arcane.iris.engine.framework.structure.StructureResourceBundleGraphCompiler;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.LegacyTileData;
import art.arcane.volmlib.util.io.IO;
import art.arcane.iris.spi.IrisLogging;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.bukkit.Bukkit;
import org.bukkit.Location;
@@ -33,40 +45,62 @@ import org.bukkit.structure.Palette;
import org.bukkit.structure.Structure;
import org.bukkit.util.BlockVector;
import java.util.Optional;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
public final class StructureImporter {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
public enum Mode {
OVERWRITE,
ADD_ONLY,
MERGE
ADD_ONLY
}
public record Result(boolean success, String message, int blocks) {
public record Result(boolean success, String message, int blocks, List<StructureLoss> losses) {
public Result {
losses = List.copyOf(losses);
}
}
record CapturedStructure(
IrisObject object,
int blocks,
int tiles,
int width,
int height,
int depth,
int structureMarkers,
List<StructureCapability> capabilities,
List<StructureLoss> losses
) {
CapturedStructure {
Objects.requireNonNull(object);
capabilities = List.copyOf(capabilities);
losses = List.copyOf(losses);
}
}
private StructureImporter() {
}
public static Mode parseMode(String s) {
if (s == null) {
return Mode.OVERWRITE;
if (s == null || s.isBlank()) {
return Mode.ADD_ONLY;
}
return switch (s.toLowerCase().replace('-', '_')) {
case "add_only", "addonly", "add" -> Mode.ADD_ONLY;
case "merge" -> Mode.MERGE;
default -> Mode.OVERWRITE;
case "overwrite", "replace" -> Mode.OVERWRITE;
default -> throw new IllegalArgumentException("Unknown structure import mode: " + s);
};
}
@@ -83,35 +117,93 @@ public final class StructureImporter {
}
public static Result importStructure(IrisData data, NamespacedKey key, String name, Mode mode, boolean objectOnly) {
Mode activeMode = mode == null ? Mode.ADD_ONLY : mode;
Structure structure;
try {
structure = Bukkit.getStructureManager().loadStructure(key);
} catch (Throwable e) {
return new Result(false, "Failed to load structure " + key + ": " + e.getMessage(), 0);
IrisLogging.reportError(e);
e.printStackTrace();
return new Result(false, "Failed to load structure " + key + ": " + e.getMessage(), 0, List.of());
}
if (structure == null || structure.getPalettes().isEmpty()) {
return new Result(false, "No loadable structure NBT for key " + key + " (jigsaw structures must be imported by their piece keys)", 0);
return new Result(false, "No loadable structure NBT for key " + key + " (jigsaw structures must be imported by their piece keys)", 0, List.of());
}
BlockVector size = structure.getSize();
CapturedStructure captured;
try {
captured = captureStructure(structure);
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
return new Result(false, "Failed to capture structure " + key + ": " + e.getMessage(), 0, List.of());
}
IrisObject object = captured.object();
int count = captured.blocks();
int tiles = captured.tiles();
int w = captured.width();
int h = captured.height();
int d = captured.depth();
List<StructureLoss> losses = new ArrayList<>(captured.losses());
if (captured.structureMarkers() > 0) {
losses.add(StructureLoss.warning(
StructureCapability.CONNECTORS,
"connectors_not_imported",
captured.structureMarkers() + " jigsaw or structure marker(s) were flattened without importing their structure graph."));
}
String writeNote = "";
try {
StructureKey sourceKey = StructureKey.parse(key.toString());
StructureKey bundleKey = new StructureKey("iris", name);
StructureSource.Kind sourceKind = key.getNamespace().equals("minecraft")
? StructureSource.Kind.VANILLA : StructureSource.Kind.DATAPACK;
StructureSource source = StructureSource.of(sourceKind, sourceKey);
IrisStructureBundleFactory.SinglePieceOptions options = new IrisStructureBundleFactory.SinglePieceOptions(
bundleKey, source, name, object, Math.max(w, d), "CENTER_HEIGHT", objectOnly,
captured.capabilities(), losses);
StructureResourceBundle bundle = IrisStructureBundleFactory.singlePiece(options);
if (!objectOnly) {
StructureResourceBundleGraphCompiler.requireViable(bundle);
}
StructureWriteMode writeMode = activeMode == Mode.ADD_ONLY
? StructureWriteMode.ADD_ONLY : StructureWriteMode.OVERWRITE;
StructureWriteResult writeResult = new StructureTransactionWriter(data.getDataFolder().toPath())
.write(bundle, writeMode);
reportWriteFailure(writeResult);
if (!writeResult.successful()) {
return new Result(false, writeFailureMessage(name, activeMode, writeResult), count, losses);
}
if (writeResult.committed()) {
data.invalidateStructureResources();
}
writeNote = writeResultNote(writeResult);
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
return new Result(false, "Failed writing import for '" + name + "': " + e.getMessage(), count, losses);
}
String lossSummary = losses.isEmpty() ? "" : ", " + losses.size() + " fidelity warning(s) recorded";
return new Result(true, "Imported " + key + " as '" + name + "' (" + count + " blocks, " + tiles
+ " tiles, " + w + "x" + h + "x" + d + lossSummary + ")" + writeNote, count, losses);
}
static CapturedStructure captureStructure(Structure structure) {
Structure activeStructure = Objects.requireNonNull(structure);
if (activeStructure.getPalettes().isEmpty()) {
throw new IllegalArgumentException("Structure has no palettes");
}
BlockVector size = activeStructure.getSize();
int w = Math.max(1, size.getBlockX());
int h = Math.max(1, size.getBlockY());
int d = Math.max(1, size.getBlockZ());
File objectFile = new File(data.getDataFolder(), "objects/" + name + ".iob");
File pieceFile = new File(data.getDataFolder(), "jigsaw-pieces/" + name + ".json");
File poolFile = new File(data.getDataFolder(), "jigsaw-pools/" + name + ".json");
File structureFile = new File(data.getDataFolder(), "structures/" + name + ".json");
boolean exists = objectOnly ? objectFile.exists() : (objectFile.exists() || structureFile.exists());
if (mode == Mode.ADD_ONLY && exists) {
return new Result(false, "Skipped (add-only): '" + name + "' already exists", 0);
}
IrisObject object = new IrisObject(w, h, d);
int count = 0;
int tiles = 0;
Palette palette = structure.getPalettes().get(0);
int structureMarkers = 0;
Palette palette = activeStructure.getPalettes().get(0);
for (BlockState block : palette.getBlocks()) {
Location loc = block.getLocation();
int x = loc.getBlockX();
@@ -126,6 +218,9 @@ public final class StructureImporter {
continue;
}
boolean structural = mat == Material.JIGSAW || mat == Material.STRUCTURE_BLOCK;
if (structural) {
structureMarkers++;
}
if (mat == Material.JIGSAW) {
BlockData resolved = readJigsawFinalState(block);
if (resolved == null || isAir(resolved)) {
@@ -146,22 +241,22 @@ public final class StructureImporter {
}
}
try {
objectFile.getParentFile().mkdirs();
object.write(objectFile);
writeJson(pieceFile, pieceJson(name));
if (objectOnly) {
IO.deleteUp(poolFile);
IO.deleteUp(structureFile);
} else {
writeJson(poolFile, poolJson(name));
writeJson(structureFile, structureJson(name, key.toString(), Math.max(w, d)));
}
} catch (Throwable e) {
return new Result(false, "Failed writing import for '" + name + "': " + e.getMessage(), count);
List<StructureCapability> capabilities = new ArrayList<>();
capabilities.add(StructureCapability.BLOCKS);
if (tiles > 0) {
capabilities.add(StructureCapability.BLOCK_ENTITIES);
}
return new Result(true, "Imported " + key + " as '" + name + "' (" + count + " blocks, " + tiles + " tiles, " + w + "x" + h + "x" + d + ")", count);
return new CapturedStructure(
object,
count,
tiles,
w,
h,
d,
structureMarkers,
capabilities,
importLosses(activeStructure, structureMarkers)
);
}
private static boolean isAir(BlockData data) {
@@ -169,6 +264,55 @@ public final class StructureImporter {
return m == Material.AIR || m == Material.CAVE_AIR || m == Material.VOID_AIR;
}
private static List<StructureLoss> importLosses(Structure structure, int structureMarkers) {
List<StructureLoss> losses = new ArrayList<>();
if (structure.getPaletteCount() > 1) {
losses.add(StructureLoss.warning(
StructureCapability.BLOCKS,
"palette_variants_not_imported",
"Only palette 0 was converted; " + (structure.getPaletteCount() - 1) + " additional palette(s) remain native-only."));
}
if (structure.getEntityCount() > 0) {
losses.add(StructureLoss.warning(
StructureCapability.ENTITIES,
"entities_not_imported",
structure.getEntityCount() + " structure entit" + (structure.getEntityCount() == 1 ? "y was" : "ies were")
+ " not converted into the Iris snapshot."));
}
if (structureMarkers > 0) {
losses.add(StructureLoss.warning(
StructureCapability.BLOCKS,
"structure_markers_resolved",
structureMarkers + " jigsaw or structure marker(s) were resolved to final blocks or omitted from the Iris snapshot."));
}
return losses;
}
private static void reportWriteFailure(StructureWriteResult result) {
result.failure().ifPresent((Throwable failure) -> {
IrisLogging.reportError(failure);
failure.printStackTrace();
});
}
private static String writeResultNote(StructureWriteResult result) {
return result.status() == StructureWriteResult.Status.COMMITTED_CLEANUP_REQUIRED
? " (committed; staging cleanup is required, see console)" : "";
}
private static String writeFailureMessage(String name, Mode mode, StructureWriteResult result) {
if (result.status() == StructureWriteResult.Status.ADD_ONLY_CONFLICT) {
return "Skipped (add-only): '" + name + "' already exists";
}
if (!result.conflicts().isEmpty()) {
StructureWriteResult.Conflict conflict = result.conflicts().getFirst();
return "Import conflict for '" + name + "': " + conflict.relativePath() + " is "
+ conflict.reason().name().toLowerCase() + ". Existing authored files were preserved.";
}
String failure = result.failure().map(Throwable::getMessage).orElse(result.status().name());
return "Failed writing import for '" + name + "' in " + mode.name().toLowerCase() + " mode: " + failure;
}
private static BlockData readJigsawFinalState(BlockState block) {
try {
Object nbt = block.getClass().getMethod("getSnapshotNBT").invoke(block);
@@ -187,6 +331,8 @@ public final class StructureImporter {
}
return Bukkit.createBlockData(finalState);
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
return null;
}
}
@@ -195,27 +341,26 @@ public final class StructureImporter {
try {
return LegacyTileData.fromBukkit(block);
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
return null;
}
}
public static Result importTemplateGroup(IrisData data, String groupName, String vanillaSource, String prefix, Mode mode) {
Mode activeMode = mode == null ? Mode.ADD_ONLY : mode;
File objectsRoot = new File(data.getDataFolder(), "objects");
File prefixDir = new File(objectsRoot, prefix);
if (!prefixDir.isDirectory()) {
return new Result(false, "No imported templates under objects/" + prefix + " for " + groupName, 0);
return new Result(false, "No imported templates under objects/" + prefix + " for " + groupName, 0, List.of());
}
List<File> iobs = new ArrayList<>();
collectIob(prefixDir, iobs);
if (iobs.isEmpty()) {
return new Result(false, "No .iob templates under objects/" + prefix + " for " + groupName, 0);
}
File structureFile = new File(data.getDataFolder(), "structures/" + groupName + ".json");
if (mode == Mode.ADD_ONLY && structureFile.exists()) {
return new Result(false, "Skipped (add-only): structure '" + groupName + "' already exists", 0);
return new Result(false, "No .iob templates under objects/" + prefix + " for " + groupName, 0, List.of());
}
iobs.sort(Comparator.comparing(File::getAbsolutePath));
String rootPath = objectsRoot.getAbsolutePath() + File.separator;
List<String> pieceNames = new ArrayList<>();
@@ -226,20 +371,55 @@ public final class StructureImporter {
rel = rel.substring(0, rel.length() - ".iob".length());
}
pieceNames.add(rel);
maxSpan = Math.max(maxSpan, readObjectSpan(iob));
try {
maxSpan = Math.max(maxSpan, readObjectSpan(iob));
} catch (IOException e) {
IrisLogging.reportError(e);
e.printStackTrace();
return new Result(false, "Cannot read imported object '" + rel + "': " + e.getMessage(), 0, List.of());
}
}
try {
for (String piece : pieceNames) {
writeJson(new File(data.getDataFolder(), "jigsaw-pieces/" + piece + ".json"), pieceJson(piece));
File pieceFile = new File(data.getDataFolder(), "jigsaw-pieces/" + piece + ".json");
if (!pieceFile.isFile()) {
return new Result(false, "Missing imported jigsaw piece '" + piece
+ "'; import the source templates before building group '" + groupName + "'", 0, List.of());
}
}
writeJson(new File(data.getDataFolder(), "jigsaw-pools/" + groupName + ".json"), poolJsonMulti(pieceNames));
writeJson(structureFile, structureJson(groupName, vanillaSource, maxSpan));
StructureKey sourceKey = StructureKey.parse(vanillaSource, "minecraft");
StructureSource.Kind sourceKind = sourceKey.namespace().equals("minecraft")
? StructureSource.Kind.VANILLA : StructureSource.Kind.DATAPACK;
StructureResourceBundle bundle = StructureResourceBundle.builder(new StructureKey("iris", groupName))
.source(StructureSource.of(sourceKind, sourceKey))
.backend(StructureBackend.IRIS_ASSEMBLY)
.capability(StructureCapability.BLOCKS)
.capability(StructureCapability.IRIS_PLACEMENT)
.textResource("jigsaw-pools/" + groupName + ".json", GSON.toJson(poolJsonMulti(pieceNames)))
.textResource("structures/" + groupName + ".json",
GSON.toJson(structureJson(groupName, vanillaSource, maxSpan)))
.build();
StructureWriteMode writeMode = activeMode == Mode.ADD_ONLY
? StructureWriteMode.ADD_ONLY : StructureWriteMode.OVERWRITE;
StructureWriteResult writeResult = new StructureTransactionWriter(data.getDataFolder().toPath())
.write(bundle, writeMode);
reportWriteFailure(writeResult);
if (!writeResult.successful()) {
return new Result(false, writeFailureMessage(groupName, activeMode, writeResult), 0, List.of());
}
if (writeResult.committed()) {
data.invalidateStructureResources();
}
return new Result(true, "Built structure '" + groupName + "' from " + pieceNames.size()
+ " template variants" + writeResultNote(writeResult), pieceNames.size(), List.of());
} catch (Throwable e) {
return new Result(false, "Failed writing group structure '" + groupName + "': " + e.getMessage(), 0);
IrisLogging.reportError(e);
e.printStackTrace();
return new Result(false, "Failed writing group structure '" + groupName + "': " + e.getMessage(), 0, List.of());
}
return new Result(true, "Built structure '" + groupName + "' from " + pieceNames.size() + " template variants", pieceNames.size());
}
private static void collectIob(File dir, List<File> out) {
@@ -256,14 +436,15 @@ public final class StructureImporter {
}
}
private static int readObjectSpan(File iob) {
private static int readObjectSpan(File iob) throws IOException {
try (DataInputStream din = new DataInputStream(new BufferedInputStream(new FileInputStream(iob)))) {
int w = din.readInt();
din.readInt();
int h = din.readInt();
int d = din.readInt();
return Math.max(1, Math.max(w, d));
} catch (Throwable e) {
return 1;
if (w < 1 || h < 1 || d < 1) {
throw new IOException("Invalid IOB dimensions " + w + "x" + h + "x" + d + " in " + iob);
}
return Math.max(w, d);
}
}
@@ -280,33 +461,50 @@ public final class StructureImporter {
return pool;
}
private static Map<String, Object> pieceJson(String name) {
Map<String, Object> piece = new LinkedHashMap<>();
piece.put("object", name);
piece.put("connectors", new ArrayList<>());
piece.put("rotatable", true);
return piece;
}
private static Map<String, Object> poolJson(String name) {
Map<String, Object> entry = new LinkedHashMap<>();
entry.put("piece", name);
entry.put("weight", 1);
List<Object> pieces = new ArrayList<>();
pieces.add(entry);
Map<String, Object> pool = new LinkedHashMap<>();
pool.put("pieces", pieces);
return pool;
}
public static void writeSinglePieceStructure(IrisData data, String name, String vanillaSource, int maxSpan, String placeMode) throws Exception {
writeJson(new File(data.getDataFolder(), "jigsaw-pieces/" + name + ".json"), pieceJson(name));
writeJson(new File(data.getDataFolder(), "jigsaw-pools/" + name + ".json"), poolJson(name));
Map<String, Object> structure = structureJson(name, vanillaSource, maxSpan);
if (placeMode != null && !placeMode.isEmpty()) {
structure.put("placeMode", placeMode);
public static Result writeSinglePieceStructure(
IrisData data,
String name,
String vanillaSource,
IrisObject object,
String placeMode,
Mode mode
) {
Mode activeMode = mode == null ? Mode.ADD_ONLY : mode;
int span = Math.max(object.getW(), object.getD());
try {
StructureKey sourceKey = StructureKey.parse(vanillaSource, "minecraft");
StructureSource.Kind sourceKind = sourceKey.namespace().equals("minecraft")
? StructureSource.Kind.VANILLA : StructureSource.Kind.DATAPACK;
IrisStructureBundleFactory.SinglePieceOptions options = new IrisStructureBundleFactory.SinglePieceOptions(
new StructureKey("iris", name),
StructureSource.of(sourceKind, sourceKey),
name,
object,
span,
placeMode,
false,
List.of(StructureCapability.BLOCKS, StructureCapability.IRIS_PLACEMENT),
List.of());
StructureResourceBundle bundle = IrisStructureBundleFactory.singlePiece(options);
StructureResourceBundleGraphCompiler.requireViable(bundle);
StructureWriteMode writeMode = activeMode == Mode.ADD_ONLY
? StructureWriteMode.ADD_ONLY : StructureWriteMode.OVERWRITE;
StructureWriteResult writeResult = new StructureTransactionWriter(data.getDataFolder().toPath())
.write(bundle, writeMode);
reportWriteFailure(writeResult);
if (!writeResult.successful()) {
return new Result(false, writeFailureMessage(name, activeMode, writeResult), 0, List.of());
}
if (writeResult.committed()) {
data.invalidateStructureResources();
}
return new Result(true, "Captured '" + vanillaSource + "' as '" + name + "'"
+ writeResultNote(writeResult), object.getBlocks().size(), List.of());
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
return new Result(false, "Failed writing capture for '" + name + "': " + e.getMessage(), 0, List.of());
}
writeJson(new File(data.getDataFolder(), "structures/" + name + ".json"), structure);
}
private static Map<String, Object> structureJson(String name, String source, int maxSpan) {
@@ -319,9 +517,4 @@ public final class StructureImporter {
return root;
}
private static void writeJson(File file, Map<String, Object> content) throws Exception {
file.getParentFile().mkdirs();
String json = new GsonBuilder().setPrettyPrinting().create().toJson(content);
Files.writeString(file.toPath(), json, StandardCharsets.UTF_8);
}
}
@@ -29,7 +29,7 @@ public final class StructureSchemaKeys {
private StructureSchemaKeys() {
}
public static KList<String> collect(Collection<String> vanillaStructureKeys, Collection<String> importedStructureKeys, Collection<String> jigsawPieceKeys) {
public static KList<String> collect(Collection<String> structureKeys, Collection<String> jigsawPieceKeys) {
Set<String> pieces = new HashSet<>();
if (jigsawPieceKeys != null) {
for (String piece : jigsawPieceKeys) {
@@ -40,17 +40,8 @@ public final class StructureSchemaKeys {
}
TreeSet<String> merged = new TreeSet<>();
if (vanillaStructureKeys != null) {
for (String key : vanillaStructureKeys) {
if (key == null || key.isBlank()) {
continue;
}
merged.add(StructureImporter.deriveName(key));
}
}
if (importedStructureKeys != null) {
for (String key : importedStructureKeys) {
if (structureKeys != null) {
for (String key : structureKeys) {
if (key == null || key.isBlank()) {
continue;
}
@@ -19,18 +19,28 @@
package art.arcane.iris.core.structure;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.structure.authoring.StructureBackend;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import art.arcane.iris.core.structure.authoring.StructureKey;
import art.arcane.iris.core.structure.authoring.StructureLoss;
import art.arcane.iris.core.structure.authoring.StructureResourceBundle;
import art.arcane.iris.core.structure.authoring.StructureSource;
import art.arcane.iris.core.structure.authoring.StructureTransactionWriter;
import art.arcane.iris.core.structure.authoring.StructureWriteMode;
import art.arcane.iris.core.structure.authoring.StructureWriteResult;
import art.arcane.iris.engine.framework.structure.StructureResourceBundleGraphCompiler;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.volmlib.util.io.IO;
import art.arcane.iris.spi.IrisLogging;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.structure.Structure;
import java.io.File;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
@@ -41,16 +51,24 @@ import java.util.Map;
import java.util.Set;
public final class VillageImporter {
public record Result(boolean success, String message, int pools, int pieces) {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
public record Result(boolean success, String message, int pools, int pieces, List<StructureLoss> losses) {
public Result {
losses = List.copyOf(losses);
}
}
private VillageImporter() {
}
public static Result importVillage(IrisData data, NamespacedKey structureKey, String name, StructureImporter.Mode mode) {
StructureImporter.Mode activeMode = mode == null ? StructureImporter.Mode.ADD_ONLY : mode;
List<StructureLoss> losses = new ArrayList<>();
Object server;
Object registryAccess;
Object structureManager;
String writeNote = "";
try {
Object craftServer = Bukkit.getServer();
Object dedicated = invoke(craftServer, "getHandle");
@@ -58,28 +76,33 @@ public final class VillageImporter {
registryAccess = resolveRegistryAccess(server);
structureManager = invoke(server, "getStructureManager");
} catch (Throwable e) {
return new Result(false, "Failed to access server registries via reflection: " + e, 0, 0);
reportFailure(e);
return failed("Failed to access server registries via reflection: " + e, losses);
}
if (registryAccess == null) {
return new Result(false, "Could not resolve RegistryAccess from the server", 0, 0);
return failed("Could not resolve RegistryAccess from the server", losses);
}
Object startPool;
int maxDepth;
int maxDistanceFromCenter;
try {
Object structureRegistry = lookupRegistry(registryAccess, "STRUCTURE");
Object structure = registryGet(structureRegistry, structureKey);
if (structure == null) {
return new Result(false, "No structure registered for key " + structureKey, 0, 0);
return failed("No structure registered for key " + structureKey, losses);
}
if (!structure.getClass().getName().endsWith("JigsawStructure")) {
return new Result(false, "Structure " + structureKey + " is not a jigsaw structure (" + structure.getClass().getSimpleName() + "); use 'import' for single-template structures", 0, 0);
return failed("Structure " + structureKey + " is not a jigsaw structure ("
+ structure.getClass().getSimpleName() + "); use 'import' for single-template structures", losses);
}
Object startPoolHolder = invoke(structure, "getStartPool");
startPool = unwrapHolder(startPoolHolder);
maxDepth = readIntField(structure, "maxDepth", 7);
maxDepth = readIntMember(structure, "maxDepth");
maxDistanceFromCenter = readIntMember(structure, "maxDistanceFromCenter");
} catch (Throwable e) {
return new Result(false, "Failed to read jigsaw structure graph: " + e, 0, 0);
reportFailure(e);
return failed("Failed to read jigsaw structure graph: " + e, losses);
}
Object templatePoolRegistry;
@@ -88,32 +111,37 @@ public final class VillageImporter {
templatePoolRegistry = lookupRegistry(registryAccess, "TEMPLATE_POOL");
random = new java.util.Random(structureKey.hashCode());
} catch (Throwable e) {
return new Result(false, "Failed to access TEMPLATE_POOL registry: " + e, 0, 0);
reportFailure(e);
return failed("Failed to access TEMPLATE_POOL registry: " + e, losses);
}
String startPoolKey;
try {
startPoolKey = registryKeyOf(templatePoolRegistry, startPool);
} catch (Throwable e) {
startPoolKey = null;
reportFailure(e);
return failed("Could not resolve the start pool key for " + structureKey + ": " + e, losses);
}
if (startPoolKey == null) {
return new Result(false, "Could not resolve the start pool key for " + structureKey, 0, 0);
}
if (mode == StructureImporter.Mode.ADD_ONLY && new File(data.getDataFolder(), "structures/" + name + ".json").exists()) {
return new Result(false, "Skipped (add-only): '" + name + "' already exists", 0, 0);
return failed("Could not resolve the start pool key for " + structureKey, losses);
}
Set<String> visitedPools = new HashSet<>();
Set<String> importedPieces = new HashSet<>();
Deque<String> poolQueue = new ArrayDeque<>();
poolQueue.add(startPoolKey);
Map<String, Map<String, Object>> emittedPools = new LinkedHashMap<>();
Map<String, Map<String, Object>> emittedPieces = new LinkedHashMap<>();
List<String> errors = new ArrayList<>();
String emptyPieceName = null;
Map<String, IrisObject> emittedObjects = new LinkedHashMap<>();
Set<StructureCapability> capabilities = new HashSet<>();
capabilities.add(StructureCapability.BLOCKS);
capabilities.add(StructureCapability.CONNECTORS);
capabilities.add(StructureCapability.IRIS_PLACEMENT);
List<String> fatalErrors = new ArrayList<>();
losses.add(StructureLoss.warning(
StructureCapability.NATIVE_PLACEMENT,
"native_placement_settings_not_imported",
"Native jigsaw placement settings other than the start pool, maximum depth, and maximum distance are not represented by the Iris assembly."));
int pieceBlocks = 0;
while (!poolQueue.isEmpty()) {
@@ -125,10 +153,12 @@ public final class VillageImporter {
try {
pool = registryGetByKey(templatePoolRegistry, poolKey);
} catch (Throwable e) {
errors.add("pool " + poolKey + ": " + e.getMessage());
reportFailure(e);
fatalErrors.add("pool " + poolKey + ": " + e.getMessage());
continue;
}
if (pool == null) {
fatalErrors.add("pool " + poolKey + " is not registered");
continue;
}
@@ -140,7 +170,13 @@ public final class VillageImporter {
Object fallbackHolder = invoke(pool, "getFallback");
Object fallbackPool = unwrapHolder(fallbackHolder);
fallbackKey = registryKeyOf(templatePoolRegistry, fallbackPool);
} catch (Throwable ignored) {
} catch (Throwable e) {
reportFailure(e);
losses.add(StructureLoss.warning(
StructureCapability.CONNECTORS,
"fallback_pool_not_imported",
"The fallback for source pool " + poolKey + " could not be resolved: " + failureDetail(e))
.affecting("jigsaw-pools/" + irisPoolName + ".json"));
}
if (fallbackKey != null && !fallbackKey.equals(poolKey)) {
poolQueue.add(fallbackKey);
@@ -150,7 +186,8 @@ public final class VillageImporter {
try {
templates = (List<?>) invoke(pool, "getTemplates");
} catch (Throwable e) {
errors.add("templates " + poolKey + ": " + e.getMessage());
reportFailure(e);
fatalErrors.add("templates " + poolKey + ": " + e.getMessage());
templates = List.of();
}
@@ -162,55 +199,94 @@ public final class VillageImporter {
Object second = invoke(pair, "getSecond");
weight = second instanceof Number ? Math.max(1, ((Number) second).intValue()) : 1;
} catch (Throwable e) {
reportFailure(e);
losses.add(StructureLoss.warning(
StructureCapability.LIST_ELEMENTS,
"pool_entry_not_imported",
"A source entry in pool " + poolKey + " could not be read: " + failureDetail(e))
.affecting("jigsaw-pools/" + irisPoolName + ".json"));
continue;
}
if (element == null) {
continue;
}
String templateLocation = templateLocationOf(element);
String templateLocation;
try {
templateLocation = templateLocationOf(element);
} catch (Throwable e) {
reportFailure(e);
losses.add(StructureLoss.warning(
StructureCapability.BLOCKS,
"template_location_not_imported",
"A source template location in pool " + poolKey + " could not be read: " + failureDetail(e))
.affecting("jigsaw-pools/" + irisPoolName + ".json"));
continue;
}
if (templateLocation == null) {
String elementType = element.getClass().getSimpleName();
if (elementType.endsWith("EmptyPoolElement")) {
if (emptyPieceName == null) {
emptyPieceName = name + "/piece/empty";
if (importedPieces.add(emptyPieceName) && writeEmptyPiece(data, emptyPieceName)) {
emittedPieces.put(emptyPieceName, pieceJson(emptyPieceName, new ArrayList<Map<String, Object>>()));
}
}
Map<String, Object> emptyEntry = new LinkedHashMap<>();
emptyEntry.put("piece", emptyPieceName);
emptyEntry.put("empty", true);
emptyEntry.put("weight", weight);
pieceEntries.add(emptyEntry);
} else {
errors.add("skipped unsupported element " + elementType + " in pool " + poolKey);
StructureCapability unsupportedCapability = unsupportedCapability(elementType);
losses.add(StructureLoss.warning(
unsupportedCapability,
"unsupported_pool_element",
"Skipped unsupported " + elementType + " in source pool " + poolKey + ".")
.affecting("jigsaw-pools/" + irisPoolName + ".json"));
}
continue;
}
NamespacedKey pieceNbtKey = NamespacedKey.fromString(templateLocation.toLowerCase());
if (pieceNbtKey == null) {
errors.add("bad piece key " + templateLocation);
fatalErrors.add("invalid piece key " + templateLocation + " in pool " + poolKey);
continue;
}
String irisPieceName = pieceName(name, templateLocation);
if (importedPieces.add(irisPieceName)) {
StructureImporter.Result imported = StructureImporter.importStructure(data, pieceNbtKey, irisPieceName, mode);
if (!imported.success()) {
errors.add(templateLocation + ": " + imported.message());
if (!emittedPieces.containsKey(irisPieceName)) {
Structure sourceTemplate;
try {
sourceTemplate = Bukkit.getStructureManager().loadStructure(pieceNbtKey);
} catch (Throwable e) {
reportFailure(e);
fatalErrors.add(templateLocation + ": failed to load structure template: " + failureDetail(e));
continue;
}
if (sourceTemplate == null || sourceTemplate.getPalettes().isEmpty()) {
fatalErrors.add(templateLocation + ": no loadable structure template was registered");
continue;
}
pieceBlocks += imported.blocks();
removeStrayPieceArtifacts(data, irisPieceName);
Connectors result = readConnectors(element, structureManager, random, name);
StructureImporter.CapturedStructure captured;
try {
captured = StructureImporter.captureStructure(sourceTemplate);
} catch (Throwable e) {
reportFailure(e);
fatalErrors.add(templateLocation + ": failed to capture structure template: " + failureDetail(e));
continue;
}
pieceBlocks += captured.blocks();
capabilities.addAll(captured.capabilities());
for (StructureLoss loss : captured.losses()) {
losses.add(loss.affecting("objects/" + irisPieceName + ".iob"));
}
emittedObjects.put(irisPieceName, captured.object());
Connectors result = readConnectors(element, structureManager, random, name, irisPieceName);
emittedPieces.put(irisPieceName, pieceJson(irisPieceName, result.json()));
poolQueue.addAll(result.targetPoolKeys());
losses.addAll(result.losses());
}
Map<String, Object> entry = new LinkedHashMap<>();
entry.put("piece", irisPieceName);
entry.put("weight", weight);
pieceEntries.add(entry);
if (emittedPieces.containsKey(irisPieceName)) {
Map<String, Object> entry = new LinkedHashMap<>();
entry.put("piece", irisPieceName);
entry.put("weight", weight);
pieceEntries.add(entry);
}
}
Map<String, Object> poolJson = new LinkedHashMap<>();
@@ -221,112 +297,222 @@ public final class VillageImporter {
emittedPools.put(irisPoolName, poolJson);
}
if (!fatalErrors.isEmpty()) {
return failed("Failed to capture the complete graph for " + structureKey + ": " + fatalErrors.getFirst()
+ (fatalErrors.size() == 1 ? "" : " (" + (fatalErrors.size() - 1) + " more)"), losses);
}
if (emittedPieces.isEmpty()) {
return new Result(false, "Imported 0 pieces for " + structureKey + (errors.isEmpty() ? "" : " (" + errors.get(0) + ")"), 0, 0);
return failed("Imported 0 pieces for " + structureKey, losses);
}
for (StructureLoss loss : losses) {
if (loss.capability() == StructureCapability.CONNECTORS) {
capabilities.remove(StructureCapability.CONNECTORS);
break;
}
}
try {
for (Map.Entry<String, Map<String, Object>> e : emittedPieces.entrySet()) {
writeJson(new File(data.getDataFolder(), "jigsaw-pieces/" + e.getKey() + ".json"), e.getValue());
StructureKey sourceKey = StructureKey.parse(structureKey.toString());
StructureSource.Kind sourceKind = structureKey.getNamespace().equals("minecraft")
? StructureSource.Kind.VANILLA : StructureSource.Kind.DATAPACK;
StructureSource source = new StructureSource(sourceKind, sourceKey, Bukkit.getBukkitVersion(), "");
Map<String, byte[]> objectResources = new LinkedHashMap<>();
for (Map.Entry<String, IrisObject> entry : emittedObjects.entrySet()) {
objectResources.put(entry.getKey(), serialize(entry.getValue()));
}
for (Map.Entry<String, Map<String, Object>> e : emittedPools.entrySet()) {
writeJson(new File(data.getDataFolder(), "jigsaw-pools/" + e.getKey() + ".json"), e.getValue());
Map<String, Object> rootStructure = structureJson(
structureKey.toString(),
poolName(name, startPoolKey),
maxDepth,
maxDistanceFromCenter);
StructureResourceBundle bundle = buildBundle(
new StructureKey("iris", name),
source,
objectResources,
emittedPieces,
emittedPools,
rootStructure,
capabilities,
losses);
StructureResourceBundleGraphCompiler.requireViable(bundle);
StructureWriteMode writeMode = activeMode == StructureImporter.Mode.OVERWRITE
? StructureWriteMode.OVERWRITE : StructureWriteMode.ADD_ONLY;
StructureWriteResult writeResult = new StructureTransactionWriter(data.getDataFolder().toPath())
.write(bundle, writeMode);
reportWriteFailure(writeResult);
if (!writeResult.successful()) {
return new Result(false, writeFailureMessage(name, writeResult), emittedPools.size(),
emittedPieces.size(), losses);
}
writeJson(new File(data.getDataFolder(), "structures/" + name + ".json"), structureJson(name, structureKey.toString(), poolName(name, startPoolKey), maxDepth));
if (writeResult.committed()) {
data.invalidateStructureResources();
}
writeNote = writeResultNote(writeResult);
} catch (Throwable e) {
return new Result(false, "Failed writing jigsaw resources for '" + name + "': " + e, emittedPools.size(), emittedPieces.size());
reportFailure(e);
return new Result(false, "Failed writing jigsaw resources for '" + name + "': " + e,
emittedPools.size(), emittedPieces.size(), losses);
}
String msg = "Imported village " + structureKey + " as '" + name + "': " + emittedPieces.size() + " pieces, " + emittedPools.size() + " pools, " + pieceBlocks + " blocks";
if (!errors.isEmpty()) {
msg += " (" + errors.size() + " piece(s) skipped; first: " + errors.get(0) + ")";
if (!losses.isEmpty()) {
msg += " (" + losses.size() + " fidelity warning(s) recorded)";
}
return new Result(true, msg, emittedPools.size(), emittedPieces.size());
return new Result(true, msg + writeNote, emittedPools.size(), emittedPieces.size(), losses);
}
private record Connectors(List<Map<String, Object>> json, Set<String> targetPoolKeys) {
static StructureResourceBundle buildBundle(
StructureKey bundleKey,
StructureSource source,
Map<String, byte[]> objects,
Map<String, Map<String, Object>> pieces,
Map<String, Map<String, Object>> pools,
Map<String, Object> structure,
Set<StructureCapability> capabilities,
List<StructureLoss> losses
) {
StructureResourceBundle.Builder bundle = StructureResourceBundle.builder(bundleKey)
.source(source)
.backend(StructureBackend.IRIS_ASSEMBLY)
.capabilities(capabilities)
.losses(losses);
for (Map.Entry<String, byte[]> entry : objects.entrySet()) {
bundle.resource("objects/" + entry.getKey() + ".iob", entry.getValue());
}
for (Map.Entry<String, Map<String, Object>> entry : pieces.entrySet()) {
bundle.textResource("jigsaw-pieces/" + entry.getKey() + ".json", GSON.toJson(entry.getValue()));
}
for (Map.Entry<String, Map<String, Object>> entry : pools.entrySet()) {
bundle.textResource("jigsaw-pools/" + entry.getKey() + ".json", GSON.toJson(entry.getValue()));
}
bundle.textResource("structures/" + bundleKey.path() + ".json", GSON.toJson(structure));
return bundle.build();
}
private static Connectors readConnectors(Object element, Object structureManager, java.util.Random random, String baseName) {
private record Connectors(
List<Map<String, Object>> json,
Set<String> targetPoolKeys,
List<StructureLoss> losses
) {
}
private static Connectors readConnectors(
Object element,
Object structureManager,
java.util.Random random,
String baseName,
String pieceName
) {
List<Map<String, Object>> connectors = new ArrayList<>();
Set<String> targets = new HashSet<>();
List<StructureLoss> losses = new ArrayList<>();
String affectedResource = "jigsaw-pieces/" + pieceName + ".json";
try {
Object zero = staticField("net.minecraft.core.BlockPos", "ZERO");
Object rotationNone = staticField("net.minecraft.world.level.block.Rotation", "NONE");
Method m = findMethod4(element.getClass(), "getShuffledJigsawBlocks");
if (m == null) {
return new Connectors(connectors, targets);
losses.add(StructureLoss.warning(
StructureCapability.CONNECTORS,
"connector_extraction_unavailable",
"The source pool element does not expose jigsaw connector extraction on this server version.")
.affecting(affectedResource));
return new Connectors(connectors, targets, losses);
}
m.setAccessible(true);
Object random0 = freshRandomSource(random);
List<?> blocks = (List<?>) m.invoke(element, structureManager, zero, rotationNone, random0);
if (blocks == null) {
return new Connectors(connectors, targets);
losses.add(StructureLoss.warning(
StructureCapability.CONNECTORS,
"connector_extraction_returned_null",
"The source pool element returned no connector collection.")
.affecting(affectedResource));
return new Connectors(connectors, targets, losses);
}
for (Object jigsaw : blocks) {
String[] rawPoolKey = new String[1];
Map<String, Object> connector = connectorFrom(jigsaw, baseName, rawPoolKey);
if (connector != null) {
try {
Map<String, Object> connector = connectorFrom(jigsaw, baseName, rawPoolKey);
connectors.add(connector);
if (rawPoolKey[0] != null && !rawPoolKey[0].isEmpty()) {
targets.add(rawPoolKey[0]);
}
} catch (Throwable e) {
reportFailure(e);
losses.add(StructureLoss.warning(
StructureCapability.CONNECTORS,
"connector_not_imported",
"A source jigsaw connector could not be converted: " + failureDetail(e))
.affecting(affectedResource));
}
}
} catch (Throwable ignored) {
} catch (Throwable e) {
reportFailure(e);
losses.add(StructureLoss.warning(
StructureCapability.CONNECTORS,
"connector_extraction_failed",
"Source jigsaw connectors could not be extracted: " + failureDetail(e))
.affecting(affectedResource));
}
return new Connectors(connectors, targets);
return new Connectors(connectors, targets, losses);
}
private static Map<String, Object> connectorFrom(Object jigsaw, String baseName, String[] rawPoolKeyOut) {
try {
Object info = invoke(jigsaw, "info");
Object pos = invoke(info, "pos");
Object blockState = invoke(info, "state");
int x = readInt(pos, "getX");
int y = readInt(pos, "getY");
int z = readInt(pos, "getZ");
private static Map<String, Object> connectorFrom(Object jigsaw, String baseName, String[] rawPoolKeyOut) throws Exception {
Object info = invoke(jigsaw, "info");
Object pos = invoke(info, "pos");
Object blockState = invoke(info, "state");
int x = readInt(pos, "getX");
int y = readInt(pos, "getY");
int z = readInt(pos, "getZ");
Object poolKey = invoke(jigsaw, "pool");
String poolId = identifierString(invoke(poolKey, "identifier"));
Object nameId = invoke(jigsaw, "name");
Object targetId = invoke(jigsaw, "target");
Object jointType = invoke(jigsaw, "jointType");
Object poolKey = invoke(jigsaw, "pool");
String poolId = identifierString(invoke(poolKey, "identifier"));
Object nameId = invoke(jigsaw, "name");
Object targetId = invoke(jigsaw, "target");
Object jointType = invoke(jigsaw, "jointType");
String front = frontFacing(blockState);
rawPoolKeyOut[0] = poolId;
String front = frontFacing(blockState);
String top = topFacing(blockState);
rawPoolKeyOut[0] = poolId;
Map<String, Object> connector = new LinkedHashMap<>();
Map<String, Object> position = new LinkedHashMap<>();
position.put("x", x);
position.put("y", y);
position.put("z", z);
connector.put("position", position);
connector.put("direction", irisDirection(front));
connector.put("pool", poolId == null ? "" : poolName(baseName, poolId));
connector.put("name", identifierString(nameId));
connector.put("targetName", identifierString(targetId));
connector.put("joint", jointType != null && jointType.toString().toUpperCase().contains("ALIGN") ? "ALIGNED" : "ROLLABLE");
return connector;
} catch (Throwable e) {
return null;
}
Map<String, Object> connector = new LinkedHashMap<>();
Map<String, Object> position = new LinkedHashMap<>();
position.put("x", x);
position.put("y", y);
position.put("z", z);
connector.put("position", position);
connector.put("direction", irisDirection(front));
connector.put("top", irisDirection(top));
connector.put("pool", poolId == null ? "" : poolName(baseName, poolId));
connector.put("name", identifierString(nameId));
connector.put("targetName", identifierString(targetId));
connector.put("joint", jointType != null && jointType.toString().toUpperCase().contains("ALIGN") ? "ALIGNED" : "ROLLABLE");
return connector;
}
private static String frontFacing(Object blockState) {
try {
Class<?> jigsawBlock = Class.forName("net.minecraft.world.level.block.JigsawBlock");
Method getFront = jigsawBlock.getMethod("getFrontFacing", Class.forName("net.minecraft.world.level.block.state.BlockState"));
Object direction = getFront.invoke(null, blockState);
if (direction == null) {
return "north";
}
Method getName = direction.getClass().getMethod("getName");
getName.setAccessible(true);
return String.valueOf(getName.invoke(direction)).toLowerCase();
} catch (Throwable e) {
private static String frontFacing(Object blockState) throws Exception {
Class<?> jigsawBlock = Class.forName("net.minecraft.world.level.block.JigsawBlock");
Method getFront = jigsawBlock.getMethod("getFrontFacing", Class.forName("net.minecraft.world.level.block.state.BlockState"));
Object direction = getFront.invoke(null, blockState);
if (direction == null) {
return "north";
}
Method getName = direction.getClass().getMethod("getName");
getName.setAccessible(true);
return String.valueOf(getName.invoke(direction)).toLowerCase();
}
private static String topFacing(Object blockState) throws Exception {
Class<?> jigsawBlock = Class.forName("net.minecraft.world.level.block.JigsawBlock");
Method getTop = jigsawBlock.getMethod("getTopFacing", Class.forName("net.minecraft.world.level.block.state.BlockState"));
Object direction = getTop.invoke(null, blockState);
if (direction == null) {
return "up";
}
Method getName = direction.getClass().getMethod("getName");
getName.setAccessible(true);
return String.valueOf(getName.invoke(direction)).toLowerCase();
}
private static String irisDirection(String front) {
@@ -340,18 +526,14 @@ public final class VillageImporter {
};
}
private static String templateLocationOf(Object element) {
try {
Method m = findMethod(element.getClass(), "getTemplateLocation");
if (m == null) {
return null;
}
m.setAccessible(true);
Object id = m.invoke(element);
return identifierString(id);
} catch (Throwable e) {
private static String templateLocationOf(Object element) throws Exception {
Method m = findMethod(element.getClass(), "getTemplateLocation");
if (m == null) {
return null;
}
m.setAccessible(true);
Object id = m.invoke(element);
return identifierString(id);
}
private static Object resolveRegistryAccess(Object server) {
@@ -376,7 +558,8 @@ public final class VillageImporter {
}
}
}
} catch (Throwable ignored) {
} catch (Throwable e) {
reportFailure(e);
}
return null;
}
@@ -503,14 +686,27 @@ public final class VillageImporter {
return opt;
}
private static int readIntField(Object o, String fieldName, int fallback) {
try {
Field f = o.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
return f.getInt(o);
} catch (Throwable e) {
return fallback;
private static int readIntMember(Object value, String memberName) throws Exception {
Class<?> type = value.getClass();
while (type != null) {
try {
Field field = type.getDeclaredField(memberName);
field.setAccessible(true);
return field.getInt(value);
} catch (NoSuchFieldException ignored) {
type = type.getSuperclass();
}
}
Method method = findMethod(value.getClass(), memberName);
if (method != null && Number.class.isAssignableFrom(boxedType(method.getReturnType()))) {
method.setAccessible(true);
return ((Number) method.invoke(value)).intValue();
}
throw new NoSuchFieldException(memberName + " on " + value.getClass().getName());
}
private static Class<?> boxedType(Class<?> type) {
return type == int.class ? Integer.class : type;
}
private static int readInt(Object o, String method) throws Exception {
@@ -576,16 +772,14 @@ public final class VillageImporter {
return create.invoke(null, random.nextLong());
}
private static String poolName(String base, String poolKey) {
return base + "/pool/" + sanitize(poolKey);
static String poolName(String base, String poolKey) {
StructureKey key = StructureKey.parse(poolKey);
return base + "/pool/" + key.namespace() + "/" + key.path();
}
private static String pieceName(String base, String templateLocation) {
return base + "/piece/" + sanitize(templateLocation);
}
private static String sanitize(String key) {
return key.replace(':', '_').replace('/', '_');
static String pieceName(String base, String templateLocation) {
StructureKey key = StructureKey.parse(templateLocation);
return base + "/piece/" + key.namespace() + "/" + key.path();
}
private static Map<String, Object> pieceJson(String pieceName, List<Map<String, Object>> connectors) {
@@ -596,37 +790,71 @@ public final class VillageImporter {
return piece;
}
private static Map<String, Object> structureJson(String name, String source, String startPool, int maxDepth) {
static Map<String, Object> structureJson(
String source,
String startPool,
int maxDepth,
int maxDistanceFromCenter
) {
Map<String, Object> root = new LinkedHashMap<>();
root.put("startPool", startPool);
root.put("maxDepth", Math.max(1, Math.min(30, maxDepth)));
root.put("maxSizeChunks", 8);
int maxSizeChunks = Math.max(1, Math.min(32, (Math.max(1, maxDistanceFromCenter) + 15) / 16));
root.put("maxSizeChunks", maxSizeChunks);
root.put("placeMode", "STRUCTURE_PIECE");
root.put("vanillaSource", source);
return root;
}
private static void removeStrayPieceArtifacts(IrisData data, String pieceName) {
IO.deleteUp(new File(data.getDataFolder(), "jigsaw-pools/" + pieceName + ".json"));
IO.deleteUp(new File(data.getDataFolder(), "structures/" + pieceName + ".json"));
}
private static boolean writeEmptyPiece(IrisData data, String pieceName) {
try {
File objectFile = new File(data.getDataFolder(), "objects/" + pieceName + ".iob");
objectFile.getParentFile().mkdirs();
IrisObject object = new IrisObject(1, 1, 1);
object.setUnsigned(0, 0, 0, art.arcane.iris.platform.bukkit.BukkitBlockState.of(Material.AIR.createBlockData()));
object.write(objectFile);
return true;
} catch (Throwable e) {
return false;
private static StructureCapability unsupportedCapability(String elementType) {
if (elementType.endsWith("ListPoolElement")) {
return StructureCapability.LIST_ELEMENTS;
}
if (elementType.endsWith("FeaturePoolElement")) {
return StructureCapability.FEATURE_ELEMENTS;
}
return StructureCapability.BLOCKS;
}
private static void writeJson(File file, Map<String, Object> content) throws Exception {
file.getParentFile().mkdirs();
String json = new GsonBuilder().setPrettyPrinting().create().toJson(content);
Files.writeString(file.toPath(), json, StandardCharsets.UTF_8);
private static byte[] serialize(IrisObject object) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
object.write(output);
return output.toByteArray();
}
private static Result failed(String message, List<StructureLoss> losses) {
return new Result(false, message, 0, 0, losses);
}
private static void reportFailure(Throwable failure) {
IrisLogging.reportError(failure);
failure.printStackTrace();
}
private static void reportWriteFailure(StructureWriteResult result) {
result.failure().ifPresent(VillageImporter::reportFailure);
}
private static String writeResultNote(StructureWriteResult result) {
return result.status() == StructureWriteResult.Status.COMMITTED_CLEANUP_REQUIRED
? " (committed; staging cleanup is required, see console)" : "";
}
private static String writeFailureMessage(String name, StructureWriteResult result) {
if (result.status() == StructureWriteResult.Status.ADD_ONLY_CONFLICT) {
return "Skipped (add-only): '" + name + "' already exists";
}
if (!result.conflicts().isEmpty()) {
StructureWriteResult.Conflict conflict = result.conflicts().getFirst();
return "Import conflict for '" + name + "': " + conflict.relativePath() + " is "
+ conflict.reason().name().toLowerCase() + ". Existing authored files were preserved.";
}
String failure = result.failure().map(VillageImporter::failureDetail).orElse(result.status().name());
return "Failed writing jigsaw import for '" + name + "': " + failure;
}
private static String failureDetail(Throwable failure) {
String message = failure.getMessage();
return message == null || message.isBlank() ? failure.getClass().getSimpleName() : message;
}
}
@@ -0,0 +1,107 @@
package art.arcane.iris.core.structure.authoring;
import art.arcane.iris.engine.object.IrisObject;
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.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public final class IrisStructureBundleFactory {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private IrisStructureBundleFactory() {
}
public static StructureResourceBundle singlePiece(SinglePieceOptions options) throws IOException {
SinglePieceOptions activeOptions = Objects.requireNonNull(options);
StructureResourceBundle.Builder bundle = StructureResourceBundle.builder(activeOptions.bundleKey())
.source(activeOptions.source())
.backend(StructureBackend.SNAPSHOT)
.capabilities(activeOptions.capabilities())
.losses(activeOptions.losses())
.resource("objects/" + activeOptions.resourceKey() + ".iob", serialize(activeOptions.object()))
.textResource("jigsaw-pieces/" + activeOptions.resourceKey() + ".json",
GSON.toJson(pieceJson(activeOptions.resourceKey())));
if (!activeOptions.objectOnly()) {
bundle.capability(StructureCapability.IRIS_PLACEMENT)
.textResource("jigsaw-pools/" + activeOptions.resourceKey() + ".json",
GSON.toJson(poolJson(activeOptions.resourceKey())))
.textResource("structures/" + activeOptions.resourceKey() + ".json",
GSON.toJson(structureJson(activeOptions)));
}
return bundle.build();
}
private static byte[] serialize(IrisObject object) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
object.write(output);
return output.toByteArray();
}
private static Map<String, Object> pieceJson(String resourceKey) {
Map<String, Object> piece = new LinkedHashMap<>();
piece.put("object", resourceKey);
piece.put("connectors", new ArrayList<>());
piece.put("rotatable", true);
return piece;
}
private static Map<String, Object> poolJson(String resourceKey) {
Map<String, Object> entry = new LinkedHashMap<>();
entry.put("piece", resourceKey);
entry.put("weight", 1);
List<Object> pieces = new ArrayList<>();
pieces.add(entry);
Map<String, Object> pool = new LinkedHashMap<>();
pool.put("pieces", pieces);
return pool;
}
private static Map<String, Object> structureJson(SinglePieceOptions options) {
Map<String, Object> structure = new LinkedHashMap<>();
structure.put("startPool", options.resourceKey());
structure.put("maxDepth", 1);
structure.put("maxSizeChunks", Math.max(1, (options.maxSpan() / 16) + 1));
structure.put("placeMode", options.placeMode());
structure.put("vanillaSource", options.source().key().value());
return structure;
}
public record SinglePieceOptions(
StructureKey bundleKey,
StructureSource source,
String resourceKey,
IrisObject object,
int maxSpan,
String placeMode,
boolean objectOnly,
Collection<StructureCapability> capabilities,
Collection<StructureLoss> losses
) {
public SinglePieceOptions {
Objects.requireNonNull(bundleKey);
Objects.requireNonNull(source);
Objects.requireNonNull(resourceKey);
Objects.requireNonNull(object);
Objects.requireNonNull(placeMode);
Objects.requireNonNull(capabilities);
Objects.requireNonNull(losses);
StructureResourceBundle.validateRelativePath("structures/" + resourceKey + ".json");
if (maxSpan < 1) {
throw new IllegalArgumentException("Structure span must be positive");
}
if (placeMode.isBlank()) {
throw new IllegalArgumentException("Structure place mode cannot be blank");
}
capabilities = List.copyOf(capabilities);
losses = List.copyOf(losses);
}
}
}
@@ -16,11 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.tools;
package art.arcane.iris.core.structure.authoring;
public enum PlausibilizeMode {
DEFAULT,
NORMALIZE,
FOLIAGE_OVERATURE,
SMOKE
public enum StructureBackend {
NATIVE,
IRIS_ASSEMBLY,
SNAPSHOT
}
@@ -16,12 +16,20 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.framework;
package art.arcane.iris.core.structure.authoring;
import art.arcane.iris.engine.object.IrisLootReference;
import art.arcane.iris.engine.object.IrisLootTable;
import art.arcane.volmlib.util.collection.KList;
public interface LootProvider {
void injectTables(KList<IrisLootTable> list, IrisLootReference r, boolean fallback);
public enum StructureCapability {
BLOCKS,
BLOCK_ENTITIES,
ENTITIES,
CONNECTORS,
PROCESSORS,
PROJECTION,
LIST_ELEMENTS,
FEATURE_ELEMENTS,
LIQUID_SETTINGS,
TERRAIN_ADAPTATION,
MIRRORING,
NATIVE_PLACEMENT,
IRIS_PLACEMENT
}
@@ -0,0 +1,148 @@
/*
* 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.authoring;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.FileChannel;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Stream;
interface StructureFileOperations {
boolean exists(Path path);
boolean isRegularFile(Path path);
default boolean isDirectory(Path path) {
return Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS);
}
byte[] readAllBytes(Path path) throws IOException;
String sha256(Path path) throws IOException;
void createDirectories(Path path) throws IOException;
void writeNew(Path path, byte[] content) throws IOException;
void move(Path source, Path target) throws IOException;
default void moveNew(Path source, Path target) throws IOException {
move(source, target);
}
void deleteIfExists(Path path) throws IOException;
void deleteTree(Path root) throws IOException;
default List<Path> list(Path root) throws IOException {
try (Stream<Path> stream = Files.list(root)) {
return stream.sorted().toList();
}
}
default void forceFile(Path path) throws IOException {
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.WRITE)) {
channel.force(true);
}
}
default void forceDirectory(Path path) throws IOException {
if (!Files.getFileStore(path).supportsFileAttributeView("posix")) {
return;
}
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
channel.force(true);
}
}
}
final class NioStructureFileOperations implements StructureFileOperations {
@Override
public boolean exists(Path path) {
return Files.exists(path, LinkOption.NOFOLLOW_LINKS);
}
@Override
public boolean isRegularFile(Path path) {
return Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS);
}
@Override
public byte[] readAllBytes(Path path) throws IOException {
return Files.readAllBytes(path);
}
@Override
public String sha256(Path path) throws IOException {
try (InputStream input = Files.newInputStream(path)) {
return StructureHash.sha256(input);
}
}
@Override
public void createDirectories(Path path) throws IOException {
Files.createDirectories(path);
}
@Override
public void writeNew(Path path, byte[] content) throws IOException {
Files.write(path, content, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
}
@Override
public 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);
}
}
@Override
public void moveNew(Path source, Path target) throws IOException {
Files.move(source, target);
}
@Override
public void deleteIfExists(Path path) throws IOException {
Files.deleteIfExists(path);
}
@Override
public void deleteTree(Path root) throws IOException {
if (!exists(root)) {
return;
}
List<Path> paths;
try (Stream<Path> stream = Files.walk(root)) {
paths = stream.sorted(Comparator.reverseOrder()).toList();
}
for (Path path : paths) {
Files.deleteIfExists(path);
}
}
}
@@ -0,0 +1,65 @@
/*
* 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.authoring;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.Objects;
import java.util.regex.Pattern;
public final class StructureHash {
private static final Pattern SHA_256_PATTERN = Pattern.compile("[a-f0-9]{64}");
private StructureHash() {
}
public static String sha256(byte[] content) {
Objects.requireNonNull(content, "content");
MessageDigest digest = createSha256Digest();
return HexFormat.of().formatHex(digest.digest(content));
}
public static String sha256(InputStream input) throws IOException {
Objects.requireNonNull(input, "input");
MessageDigest digest = createSha256Digest();
byte[] buffer = new byte[8192];
int read;
while ((read = input.read(buffer)) >= 0) {
if (read > 0) {
digest.update(buffer, 0, read);
}
}
return HexFormat.of().formatHex(digest.digest());
}
public static boolean isSha256(String value) {
return value != null && SHA_256_PATTERN.matcher(value).matches();
}
private static MessageDigest createSha256Digest() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("SHA-256 is not available", e);
}
}
}
@@ -0,0 +1,87 @@
/*
* 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.authoring;
import java.util.Objects;
import java.util.regex.Pattern;
public record StructureKey(String namespace, String path) implements Comparable<StructureKey> {
private static final Pattern NAMESPACE_PATTERN = Pattern.compile("[a-z0-9._-]+");
private static final Pattern PATH_PATTERN = Pattern.compile("[a-z0-9._/-]+");
public StructureKey {
Objects.requireNonNull(namespace, "namespace");
Objects.requireNonNull(path, "path");
if (!NAMESPACE_PATTERN.matcher(namespace).matches()) {
throw new IllegalArgumentException("Invalid structure namespace: " + namespace);
}
if (!PATH_PATTERN.matcher(path).matches()) {
throw new IllegalArgumentException("Invalid structure path: " + path);
}
validatePathSegments(path);
}
public static StructureKey parse(String value) {
Objects.requireNonNull(value, "value");
int separator = value.indexOf(':');
if (separator <= 0 || separator == value.length() - 1 || separator != value.lastIndexOf(':')) {
throw new IllegalArgumentException("Structure key must use namespace:path: " + value);
}
return new StructureKey(value.substring(0, separator), value.substring(separator + 1));
}
public static StructureKey parse(String value, String defaultNamespace) {
Objects.requireNonNull(value, "value");
Objects.requireNonNull(defaultNamespace, "defaultNamespace");
if (value.indexOf(':') < 0) {
return new StructureKey(defaultNamespace, value);
}
return parse(value);
}
public String value() {
return namespace + ":" + path;
}
@Override
public int compareTo(StructureKey other) {
int namespaceComparison = namespace.compareTo(other.namespace);
if (namespaceComparison != 0) {
return namespaceComparison;
}
return path.compareTo(other.path);
}
@Override
public String toString() {
return value();
}
private static void validatePathSegments(String path) {
if (path.startsWith("/") || path.endsWith("/") || path.contains("//")) {
throw new IllegalArgumentException("Invalid structure path: " + path);
}
String[] segments = path.split("/");
for (String segment : segments) {
if (segment.equals(".") || segment.equals("..")) {
throw new IllegalArgumentException("Invalid structure path: " + path);
}
}
}
}
@@ -0,0 +1,65 @@
/*
* 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.authoring;
import java.util.Objects;
import java.util.regex.Pattern;
public record StructureLoss(
StructureCapability capability,
Severity severity,
String code,
String detail,
String affectedResource
) {
private static final Pattern CODE_PATTERN = Pattern.compile("[a-z0-9._-]+");
public StructureLoss {
Objects.requireNonNull(capability, "capability");
Objects.requireNonNull(severity, "severity");
Objects.requireNonNull(code, "code");
Objects.requireNonNull(detail, "detail");
Objects.requireNonNull(affectedResource, "affectedResource");
if (!CODE_PATTERN.matcher(code).matches()) {
throw new IllegalArgumentException("Invalid structure loss code: " + code);
}
if (detail.isBlank()) {
throw new IllegalArgumentException("Structure loss detail cannot be blank");
}
}
public static StructureLoss warning(StructureCapability capability, String code, String detail) {
return new StructureLoss(capability, Severity.WARNING, code, detail, "");
}
public static StructureLoss error(StructureCapability capability, String code, String detail) {
return new StructureLoss(capability, Severity.ERROR, code, detail, "");
}
public StructureLoss affecting(String resource) {
Objects.requireNonNull(resource, "resource");
return new StructureLoss(capability, severity, code, detail, resource);
}
public enum Severity {
INFO,
WARNING,
ERROR
}
}
@@ -0,0 +1,122 @@
/*
* 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.authoring;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
public record StructureOwnershipManifest(
int schemaVersion,
StructureKey structure,
StructureSource source,
StructureBackend backend,
List<StructureCapability> capabilities,
List<StructureLoss> losses,
Map<String, String> resourceHashes
) {
public static final int CURRENT_SCHEMA_VERSION = 1;
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
public StructureOwnershipManifest {
if (schemaVersion != CURRENT_SCHEMA_VERSION) {
throw new IllegalArgumentException("Unsupported structure ownership manifest schema: " + schemaVersion);
}
Objects.requireNonNull(structure, "structure");
Objects.requireNonNull(source, "source");
Objects.requireNonNull(backend, "backend");
Objects.requireNonNull(capabilities, "capabilities");
Objects.requireNonNull(losses, "losses");
Objects.requireNonNull(resourceHashes, "resourceHashes");
ArrayList<StructureCapability> orderedCapabilities = new ArrayList<>(capabilities);
orderedCapabilities.sort(Comparator.naturalOrder());
capabilities = List.copyOf(orderedCapabilities);
losses = List.copyOf(losses);
TreeMap<String, String> orderedHashes = new TreeMap<>();
TreeMap<String, String> portablePaths = new TreeMap<>();
for (Map.Entry<String, String> entry : resourceHashes.entrySet()) {
String relativePath = StructureResourceBundle.validateRelativePath(entry.getKey());
String portablePath = relativePath.toLowerCase(Locale.ROOT);
String previousPath = portablePaths.putIfAbsent(portablePath, relativePath);
if (previousPath != null) {
throw new IllegalArgumentException(
"Ownership manifest contains case-colliding resources: " + previousPath + " and " + relativePath
);
}
String contentHash = Objects.requireNonNull(entry.getValue(), "contentHash");
if (!StructureHash.isSha256(contentHash)) {
throw new IllegalArgumentException("Invalid SHA-256 hash for resource " + relativePath);
}
orderedHashes.put(relativePath, contentHash);
}
resourceHashes = Collections.unmodifiableMap(orderedHashes);
}
public static StructureOwnershipManifest from(StructureResourceBundle bundle) {
Objects.requireNonNull(bundle, "bundle");
TreeMap<String, String> hashes = new TreeMap<>();
for (StructureResourceBundle.Resource resource : bundle.resources().values()) {
hashes.put(resource.relativePath(), resource.contentHash());
}
return new StructureOwnershipManifest(
CURRENT_SCHEMA_VERSION,
bundle.key(),
bundle.source(),
bundle.backend(),
new ArrayList<>(bundle.capabilities()),
bundle.losses(),
hashes
);
}
public static StructureOwnershipManifest fromJson(byte[] content) {
Objects.requireNonNull(content, "content");
StructureOwnershipManifest manifest = GSON.fromJson(
new String(content, StandardCharsets.UTF_8),
StructureOwnershipManifest.class
);
if (manifest == null) {
throw new IllegalArgumentException("Structure ownership manifest is empty");
}
return manifest;
}
public byte[] toJson() {
return GSON.toJson(this).getBytes(StandardCharsets.UTF_8);
}
public String relativePath() {
return relativePath(structure);
}
public static String relativePath(StructureKey structure) {
Objects.requireNonNull(structure, "structure");
String identityHash = StructureHash.sha256(structure.value().getBytes(StandardCharsets.UTF_8));
return ".iris/structure-manifests/key-" + identityHash + ".json";
}
}
@@ -0,0 +1,56 @@
/*
* 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.authoring;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
public record StructureRecoveryResult(
int restoredPreparedTransactions,
int cleanedCommittedTransactions,
int cleanedOrphanTransactions,
List<Failure> failures
) {
public StructureRecoveryResult {
if (restoredPreparedTransactions < 0
|| cleanedCommittedTransactions < 0
|| cleanedOrphanTransactions < 0) {
throw new IllegalArgumentException("Structure recovery counts cannot be negative");
}
Objects.requireNonNull(failures, "failures");
failures = List.copyOf(failures);
}
public boolean successful() {
return failures.isEmpty();
}
public int recoveredTransactions() {
return restoredPreparedTransactions + cleanedCommittedTransactions + cleanedOrphanTransactions;
}
public record Failure(Path transactionRoot, Throwable cause) {
public Failure {
Objects.requireNonNull(transactionRoot, "transactionRoot");
Objects.requireNonNull(cause, "cause");
transactionRoot = transactionRoot.toAbsolutePath().normalize();
}
}
}
@@ -0,0 +1,234 @@
/*
* 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.authoring;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Pattern;
public final class StructureResourceBundle {
private static final Pattern WINDOWS_RESERVED_NAME = Pattern.compile(
"(?i)(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\\..*)?"
);
private final StructureKey key;
private final StructureSource source;
private final StructureBackend backend;
private final Set<StructureCapability> capabilities;
private final List<StructureLoss> losses;
private final Map<String, Resource> resources;
private StructureResourceBundle(Builder builder) {
key = builder.key;
source = Objects.requireNonNull(builder.source, "source");
backend = Objects.requireNonNull(builder.backend, "backend");
capabilities = immutableCapabilities(builder.capabilities);
losses = List.copyOf(builder.losses);
resources = Collections.unmodifiableMap(new TreeMap<>(builder.resources));
if (resources.isEmpty()) {
throw new IllegalStateException("Structure resource bundle cannot be empty");
}
}
public static Builder builder(StructureKey key) {
return new Builder(key);
}
public StructureKey key() {
return key;
}
public StructureSource source() {
return source;
}
public StructureBackend backend() {
return backend;
}
public Set<StructureCapability> capabilities() {
return capabilities;
}
public List<StructureLoss> losses() {
return losses;
}
public Map<String, Resource> resources() {
return resources;
}
public static String validateRelativePath(String relativePath) {
Objects.requireNonNull(relativePath, "relativePath");
if (relativePath.isBlank() || relativePath.startsWith("/") || relativePath.endsWith("/")) {
throw new IllegalArgumentException("Resource path must be a non-empty relative path: " + relativePath);
}
if (relativePath.indexOf('\\') >= 0 || relativePath.indexOf(':') >= 0 || relativePath.contains("//")) {
throw new IllegalArgumentException("Resource path is not portable: " + relativePath);
}
String[] segments = relativePath.split("/");
if (segments[0].equalsIgnoreCase(".iris")) {
throw new IllegalArgumentException("Resource path uses the reserved .iris directory: " + relativePath);
}
for (String segment : segments) {
validatePathSegment(relativePath, segment);
}
return relativePath;
}
private static Set<StructureCapability> immutableCapabilities(EnumSet<StructureCapability> capabilities) {
if (capabilities.isEmpty()) {
return Set.of();
}
return Collections.unmodifiableSet(EnumSet.copyOf(capabilities));
}
private static void validatePathSegment(String relativePath, String segment) {
if (segment.isEmpty() || segment.equals(".") || segment.equals("..") || segment.endsWith(".") || segment.endsWith(" ")) {
throw new IllegalArgumentException("Resource path is not portable: " + relativePath);
}
if (WINDOWS_RESERVED_NAME.matcher(segment).matches()) {
throw new IllegalArgumentException("Resource path uses a reserved file name: " + relativePath);
}
for (int i = 0; i < segment.length(); i++) {
char character = segment.charAt(i);
if (Character.isISOControl(character) || character == '"' || character == '*' || character == '<'
|| character == '>' || character == '?' || character == '|') {
throw new IllegalArgumentException("Resource path is not portable: " + relativePath);
}
}
}
public static final class Builder {
private final StructureKey key;
private final EnumSet<StructureCapability> capabilities;
private final List<StructureLoss> losses;
private final Map<String, Resource> resources;
private final Map<String, String> portableResourcePaths;
private StructureSource source;
private StructureBackend backend;
private Builder(StructureKey key) {
this.key = Objects.requireNonNull(key, "key");
capabilities = EnumSet.noneOf(StructureCapability.class);
losses = new ArrayList<>();
resources = new TreeMap<>();
portableResourcePaths = new HashMap<>();
}
public Builder source(StructureSource source) {
this.source = Objects.requireNonNull(source, "source");
return this;
}
public Builder backend(StructureBackend backend) {
this.backend = Objects.requireNonNull(backend, "backend");
return this;
}
public Builder capability(StructureCapability capability) {
capabilities.add(Objects.requireNonNull(capability, "capability"));
return this;
}
public Builder capabilities(Collection<StructureCapability> capabilities) {
Objects.requireNonNull(capabilities, "capabilities");
for (StructureCapability capability : capabilities) {
capability(capability);
}
return this;
}
public Builder loss(StructureLoss loss) {
losses.add(Objects.requireNonNull(loss, "loss"));
return this;
}
public Builder losses(Collection<StructureLoss> losses) {
Objects.requireNonNull(losses, "losses");
for (StructureLoss loss : losses) {
loss(loss);
}
return this;
}
public Builder resource(String relativePath, byte[] content) {
Resource resource = new Resource(relativePath, content);
String portablePath = resource.relativePath().toLowerCase(Locale.ROOT);
String previousPath = portableResourcePaths.putIfAbsent(portablePath, resource.relativePath());
if (previousPath != null) {
throw new IllegalArgumentException("Duplicate structure resource: " + relativePath);
}
resources.put(resource.relativePath(), resource);
return this;
}
public Builder textResource(String relativePath, String content) {
Objects.requireNonNull(content, "content");
return resource(relativePath, content.getBytes(StandardCharsets.UTF_8));
}
public StructureResourceBundle build() {
return new StructureResourceBundle(this);
}
}
public static final class Resource {
private final String relativePath;
private final byte[] content;
private final String contentHash;
private Resource(String relativePath, byte[] content) {
this.relativePath = validateRelativePath(relativePath);
Objects.requireNonNull(content, "content");
this.content = content.clone();
contentHash = StructureHash.sha256(this.content);
}
public String relativePath() {
return relativePath;
}
public byte[] content() {
return content.clone();
}
public int size() {
return content.length;
}
public String contentHash() {
return contentHash;
}
byte[] contentForWrite() {
return content;
}
}
}
@@ -0,0 +1,49 @@
/*
* 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.authoring;
import java.util.Objects;
public record StructureSource(Kind kind, StructureKey key, String version, String contentHash) {
public StructureSource {
Objects.requireNonNull(kind, "kind");
Objects.requireNonNull(key, "key");
Objects.requireNonNull(version, "version");
Objects.requireNonNull(contentHash, "contentHash");
if (!contentHash.isEmpty() && !StructureHash.isSha256(contentHash)) {
throw new IllegalArgumentException("Source content hash must be SHA-256");
}
}
public static StructureSource of(Kind kind, StructureKey key) {
return new StructureSource(kind, key, "", "");
}
public static StructureSource identified(Kind kind, StructureKey key, String version, byte[] content) {
return new StructureSource(kind, key, version, StructureHash.sha256(content));
}
public enum Kind {
IRIS,
VANILLA,
DATAPACK,
MOD,
UNKNOWN
}
}
@@ -0,0 +1,139 @@
/*
* 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.authoring;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;
record StructureTransactionJournal(
int schemaVersion,
UUID transactionId,
Phase phase,
List<Target> targets
) {
static final int CURRENT_SCHEMA_VERSION = 1;
static final String FILE_NAME = "transaction.json";
static final String NEXT_FILE_NAME = "transaction.json.next";
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final Pattern MANIFEST_PATH = Pattern.compile(
"\\.iris/structure-manifests/key-[a-f0-9]{64}\\.json"
);
StructureTransactionJournal {
if (schemaVersion != CURRENT_SCHEMA_VERSION) {
throw new IllegalArgumentException("Unsupported structure transaction schema: " + schemaVersion);
}
Objects.requireNonNull(transactionId, "transactionId");
Objects.requireNonNull(phase, "phase");
Objects.requireNonNull(targets, "targets");
if (targets.isEmpty()) {
throw new IllegalArgumentException("Structure transaction must contain at least one target");
}
ArrayList<Target> orderedTargets = new ArrayList<>(targets);
orderedTargets.sort(Comparator.comparing(Target::relativePath));
Set<String> portablePaths = new HashSet<>(orderedTargets.size());
for (Target target : orderedTargets) {
String portablePath = target.relativePath().toLowerCase(Locale.ROOT);
if (!portablePaths.add(portablePath)) {
throw new IllegalArgumentException("Structure transaction contains duplicate target "
+ target.relativePath());
}
}
targets = List.copyOf(orderedTargets);
}
static StructureTransactionJournal prepared(UUID transactionId, List<Target> targets) {
return new StructureTransactionJournal(CURRENT_SCHEMA_VERSION, transactionId, Phase.PREPARED, targets);
}
static StructureTransactionJournal fromJson(byte[] content) {
Objects.requireNonNull(content, "content");
StructureTransactionJournal journal = GSON.fromJson(
new String(content, StandardCharsets.UTF_8),
StructureTransactionJournal.class
);
if (journal == null) {
throw new IllegalArgumentException("Structure transaction journal is empty");
}
return journal;
}
StructureTransactionJournal committed() {
return new StructureTransactionJournal(schemaVersion, transactionId, Phase.COMMITTED, targets);
}
byte[] toJson() {
return GSON.toJson(this).getBytes(StandardCharsets.UTF_8);
}
enum Phase {
PREPARED,
COMMITTED
}
record Target(
String relativePath,
boolean hadOriginal,
String originalHash,
String replacementHash
) {
Target {
Objects.requireNonNull(relativePath, "relativePath");
Objects.requireNonNull(originalHash, "originalHash");
Objects.requireNonNull(replacementHash, "replacementHash");
if (relativePath.startsWith(".iris/")) {
StructureResourceBundle.validateRelativePath("transaction/" + relativePath);
if (!MANIFEST_PATH.matcher(relativePath).matches()) {
throw new IllegalArgumentException("Invalid internal structure transaction target: "
+ relativePath);
}
} else {
relativePath = StructureResourceBundle.validateRelativePath(relativePath);
}
if (hadOriginal && !StructureHash.isSha256(originalHash)) {
throw new IllegalArgumentException("Structure transaction original hash is invalid for "
+ relativePath);
}
if (!hadOriginal && !originalHash.isEmpty()) {
throw new IllegalArgumentException("Structure transaction has a hash for a new target "
+ relativePath);
}
if (!replacementHash.isEmpty() && !StructureHash.isSha256(replacementHash)) {
throw new IllegalArgumentException("Structure transaction replacement hash is invalid for "
+ relativePath);
}
if (!hadOriginal && replacementHash.isEmpty()) {
throw new IllegalArgumentException("Structure transaction new target has no replacement hash for "
+ relativePath);
}
}
}
}
@@ -16,15 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.object;
package art.arcane.iris.core.structure.authoring;
import art.arcane.iris.engine.object.annotations.Desc;
@Desc("Master toggle for native vanilla, mod, and datapack structure generation in a dimension.")
public enum VanillaStructureMode {
@Desc("All native vanilla, mod, and datapack structures generate, except any keys listed in 'disabled'. This is the default. Use this to blacklist a few structures.")
ALL_ON,
@Desc("No native vanilla, mod, or datapack structures generate, except any keys listed in 'enabled'. Use this to whitelist a few structures.")
ALL_OFF
public enum StructureWriteMode {
ADD_ONLY,
OVERWRITE
}
@@ -0,0 +1,39 @@
/*
* 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.authoring;
import java.util.Objects;
public record StructureWriteOptions(StructureWriteMode mode, boolean dryRun) {
public StructureWriteOptions {
Objects.requireNonNull(mode, "mode");
}
public static StructureWriteOptions addOnly() {
return new StructureWriteOptions(StructureWriteMode.ADD_ONLY, false);
}
public static StructureWriteOptions overwrite() {
return new StructureWriteOptions(StructureWriteMode.OVERWRITE, false);
}
public static StructureWriteOptions preview(StructureWriteMode mode) {
return new StructureWriteOptions(mode, true);
}
}
@@ -0,0 +1,119 @@
/*
* 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.authoring;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
public record StructureWriteResult(
Status status,
Action action,
List<Conflict> conflicts,
List<String> affectedResources,
String manifestPath,
Optional<Throwable> failure
) {
public StructureWriteResult {
Objects.requireNonNull(status, "status");
Objects.requireNonNull(action, "action");
Objects.requireNonNull(conflicts, "conflicts");
Objects.requireNonNull(affectedResources, "affectedResources");
Objects.requireNonNull(manifestPath, "manifestPath");
Objects.requireNonNull(failure, "failure");
conflicts = List.copyOf(conflicts);
affectedResources = List.copyOf(affectedResources);
}
public boolean successful() {
return switch (status) {
case DRY_RUN, ADDED, OVERWRITTEN, UNCHANGED, COMMITTED_CLEANUP_REQUIRED -> true;
case ADD_ONLY_CONFLICT, OWNERSHIP_CONFLICT, ROLLED_BACK, FAILED -> false;
};
}
public boolean committed() {
return switch (status) {
case ADDED, OVERWRITTEN, UNCHANGED, COMMITTED_CLEANUP_REQUIRED -> true;
case DRY_RUN, ADD_ONLY_CONFLICT, OWNERSHIP_CONFLICT, ROLLED_BACK, FAILED -> false;
};
}
public enum Status {
DRY_RUN,
ADDED,
OVERWRITTEN,
UNCHANGED,
ADD_ONLY_CONFLICT,
OWNERSHIP_CONFLICT,
ROLLED_BACK,
FAILED,
COMMITTED_CLEANUP_REQUIRED
}
public enum Action {
ADD,
OVERWRITE,
NONE
}
public enum ConflictReason {
RESOURCE_EXISTS,
MANIFEST_EXISTS,
UNOWNED_RESOURCE,
MODIFIED_RESOURCE,
MISSING_OWNED_RESOURCE,
NON_FILE_RESOURCE,
INVALID_MANIFEST
}
public record Conflict(
String relativePath,
ConflictReason reason,
String expectedHash,
String actualHash,
String detail
) {
public Conflict {
Objects.requireNonNull(relativePath, "relativePath");
Objects.requireNonNull(reason, "reason");
Objects.requireNonNull(expectedHash, "expectedHash");
Objects.requireNonNull(actualHash, "actualHash");
Objects.requireNonNull(detail, "detail");
}
public static Conflict at(String relativePath, ConflictReason reason) {
return new Conflict(relativePath, reason, "", "", "");
}
public static Conflict modified(String relativePath, String expectedHash, String actualHash) {
return new Conflict(
relativePath,
ConflictReason.MODIFIED_RESOURCE,
expectedHash,
actualHash,
"Resource content differs from its ownership manifest"
);
}
public static Conflict invalidManifest(String relativePath, String detail) {
return new Conflict(relativePath, ConflictReason.INVALID_MANIFEST, "", "", detail);
}
}
}
@@ -0,0 +1,315 @@
/*
* 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);
}
}
@@ -0,0 +1,401 @@
/*
* 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);
}
}
}
@@ -0,0 +1,74 @@
/*
* 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;
}
}
@@ -0,0 +1,140 @@
/*
* 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);
}
}
}
@@ -0,0 +1,68 @@
/*
* 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);
}
}
}
@@ -0,0 +1,66 @@
/*
* 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
);
}
}
@@ -0,0 +1,113 @@
/*
* 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);
}
}
}
@@ -0,0 +1,52 @@
/*
* 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);
};
}
}
@@ -0,0 +1,233 @@
/*
* 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();
}
}
}
@@ -0,0 +1,54 @@
/*
* 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;
}
}
@@ -0,0 +1,43 @@
/*
* 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);
}
}
@@ -0,0 +1,42 @@
/*
* 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());
}
}
@@ -0,0 +1,170 @@
/*
* 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.tools;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.spi.IrisLogging;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.function.Consumer;
public final class TreePlausibilizeBatch {
private TreePlausibilizeBatch() {
}
public record Target(String key, File file) {
}
public static List<Target> resolve(String target, IrisData data) {
List<Target> out = new ArrayList<>();
if (target == null || target.isEmpty()) {
return out;
}
File direct = new File(target);
if (direct.isFile() && target.toLowerCase(Locale.ROOT).endsWith(".iob")) {
out.add(new Target(direct.getName().replaceAll("\\.iob$", ""), direct));
return out;
}
if (direct.isDirectory()) {
walkIob(direct, direct, out);
return out;
}
if (data != null) {
ResourceLoader<IrisObject> loader = data.getObjectLoader();
if (!target.endsWith("/") && loader.findFile(target) != null) {
out.add(new Target(target, null));
return out;
}
String prefix = target.endsWith("/") ? target : target + "/";
for (String k : loader.getPossibleKeys()) {
if (k.startsWith(prefix)) {
out.add(new Target(k, null));
}
}
}
return out;
}
public static void walkIob(File root, File keyRoot, List<Target> out) {
File[] kids = root.listFiles();
if (kids == null) {
return;
}
for (File f : kids) {
if (f.isDirectory()) {
walkIob(f, keyRoot, out);
} else if (f.getName().toLowerCase(Locale.ROOT).endsWith(".iob")) {
String rel = keyRoot.toPath().relativize(f.toPath()).toString()
.replace(File.separatorChar, '/')
.replaceAll("\\.iob$", "");
out.add(new Target(rel, f));
}
}
}
public static void run(List<Target> targets, boolean dryRun, int reach, IrisData nearest, Consumer<String> out) {
int processed = 0;
int changed = 0;
int skipped = 0;
int failed = 0;
long totalWood = 0L;
long totalBranches = 0L;
long totalConverted = 0L;
long totalRewritten = 0L;
long totalPinned = 0L;
long totalUnreachableBefore = 0L;
long totalUnreachableAfter = 0L;
int progressStep = Math.max(1, targets.size() / 20);
int index = 0;
for (Target t : targets) {
index++;
try {
IrisObject o = load(t, nearest);
if (o == null) {
out.accept("skip " + t.key() + ": failed to load");
skipped++;
continue;
}
long seed = TreePlausibilizer.seedOf(t.key());
TreePlausibilizer.Result r = dryRun
? TreePlausibilizer.analyze(o, seed, reach)
: TreePlausibilizer.apply(o, seed, reach);
if (!dryRun && r.mutated()) {
File dest = o.getLoadFile() != null ? o.getLoadFile() : t.file();
if (dest != null) {
o.write(dest);
changed++;
}
}
processed++;
totalWood += r.woodPlaced();
totalBranches += r.branchesGrown();
totalConverted += r.leavesConvertedToWood();
totalRewritten += r.distancesRewritten();
totalPinned += r.leavesPinnedPersistent();
totalUnreachableBefore += r.unreachableBefore();
totalUnreachableAfter += r.unreachableAfter();
if (r.mutated() || targets.size() == 1) {
out.accept(t.key() + ": +" + r.woodPlaced() + " wood (" + r.branchesGrown() + " branches), "
+ r.leavesConvertedToWood() + " leaves->wood, ~" + r.distancesRewritten() + " distances"
+ (r.leavesPinnedPersistent() > 0 ? ", !" + r.leavesPinnedPersistent() + " pinned" : ""));
}
if (targets.size() > 1 && index % progressStep == 0) {
out.accept("[" + index + "/" + targets.size() + "]");
}
} catch (Throwable e) {
out.accept("fail " + t.key() + ": " + e.getClass().getSimpleName() + ": " + e.getMessage());
IrisLogging.reportError(e);
failed++;
}
}
out.accept("Done: " + processed + " processed, " + changed + " changed, "
+ skipped + " skipped, " + failed + " failed"
+ (dryRun ? " (dry run, nothing written)" : ""));
out.accept("Totals: +" + totalWood + " wood (" + totalBranches + " branches), "
+ totalConverted + " leaves->wood, ~" + totalRewritten + " distances, !"
+ totalPinned + " pinned, unreachable " + totalUnreachableBefore + " -> " + totalUnreachableAfter);
}
private static IrisObject load(Target t, IrisData nearest) throws IOException {
if (t.file() != null) {
IrisObject o = new IrisObject();
o.read(t.file());
o.setLoadFile(t.file());
return o;
}
return IrisData.loadAnyObject(t.key(), nearest);
}
}
File diff suppressed because it is too large Load Diff
@@ -30,7 +30,6 @@ import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.mantle.MantleComponent;
import art.arcane.iris.engine.mantle.components.MantleCarvingComponent;
import art.arcane.iris.engine.mantle.components.MantleFloatingObjectComponent;
import art.arcane.iris.engine.mantle.components.MantleFluidBodyComponent;
import art.arcane.iris.engine.mantle.components.MantleObjectComponent;
import art.arcane.iris.engine.mantle.components.IrisStructureComponent;
import art.arcane.iris.spi.IrisLogging;
@@ -88,7 +87,6 @@ public class IrisEngineMantle implements EngineMantle {
this.mantle = createMantle(engine);
components = new KMap<>();
registerComponent(new MantleCarvingComponent(this));
registerComponent(new MantleFluidBodyComponent(this));
object = new MantleObjectComponent(this);
registerComponent(object);
registerComponent(new MantleFloatingObjectComponent(this));
@@ -965,16 +965,15 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
}
IrisMarker mark = getData().getMarkerLoader().load(t.getTag());
if (mark == null) {
return;
}
IrisPosition pos = new IrisPosition((c.getX() << 4) + x, y, (c.getZ() << 4) + z);
if (mark.isEmptyAbove()) {
boolean remove = c.getBlock(x, y + 1, z).getBlockData().getMaterial().isSolid()
|| c.getBlock(x, y + 2, z).getBlockData().getMaterial().isSolid();
if (remove) {
b.add(pos);
return;
}
if (isMarkerObstructed(c, pos, mark.isEmptyAbove())) {
b.add(pos);
return;
}
for (String i : mark.getSpawners()) {
@@ -1096,7 +1095,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
}
int minY = getEngine().getWorld().minHeight();
int markerY = relative.getY() + minY;
int markerY = toWorldY(relative.getY(), minY);
if (markerY + 2 >= chunk.getWorld().getMaxHeight()) {
return true;
}
@@ -1145,7 +1144,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
});
KList<ItemStack> d = new KList<>();
IrisBiome b = EngineBukkitOps.getBiome(getEngine(), e.getBlock().getLocation().clone().subtract(0, getEngine().getWorld().minHeight(), 0));
IrisBiome b = EngineBukkitOps.getBiome(getEngine(), e.getBlock().getLocation());
List<IrisBlockDrops> dropProviders = filterDrops(b.getBlockDrops(), e, getData());
if (dropProviders.stream().noneMatch(IrisBlockDrops::isSkipParents)) {
@@ -1177,6 +1176,10 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return worldY - minHeight;
}
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());
}
@@ -108,18 +108,28 @@ final class DecoratorCore {
String half = IrisProceduralBlocks.propertyValue(bd, "half");
if (half != null) {
int lowerY = height + 1;
int upperY = height + 2;
if (!canPlaceTwoBlockPlant(data, x, z, lowerY, upperY, caveSkipFluid)) {
return;
}
try {
if (height + 2 < data.getHeight() && (!caveSkipFluid || !B.isFluid(data.get(x, height + 2, z)))) {
data.set(x, height + 2, z, bd.withProperty("half", topHalfValue(half)));
}
PlatformBlockState upper = bd.withProperty("half", topHalfValue(half));
PlatformBlockState lower = fixFacesForHunk(
bd.withProperty("half", bottomHalfValue(half)),
data, x, z, realX, lowerY, realZ, mantle);
data.set(x, lowerY, z, lower);
data.set(x, upperY, z, upper);
} catch (Throwable e) {
IrisLogging.reportError(e);
}
bd = bd.withProperty("half", bottomHalfValue(half));
return;
}
if (height + 1 < data.getHeight() && B.isAir(data.get(x, height + 1, z))) {
data.set(x, height + 1, z, fixFacesForHunk(bd, data, x, z, realX, height + 1, realZ, mantle));
int targetY = height + 1;
if (targetY < data.getHeight() && B.isAir(data.get(x, targetY, z))) {
data.set(x, targetY, z, fixFacesForHunk(bd, data, x, z, realX, targetY, realZ, mantle));
}
}
@@ -169,18 +179,28 @@ final class DecoratorCore {
String half = bd == null ? null : IrisProceduralBlocks.propertyValue(bd, "half");
if (half != null) {
int lowerY = height + 1;
int upperY = height + 2;
if (!canPlaceTwoBlockPlant(data, x, z, lowerY, upperY, caveSkipFluid)) {
return;
}
try {
if (height + 2 < data.getHeight() && (!caveSkipFluid || !B.isFluid(data.get(x, height + 2, z)))) {
data.set(x, height + 2, z, bd.withProperty("half", topHalfValue(half)));
}
PlatformBlockState upper = bd.withProperty("half", topHalfValue(half));
PlatformBlockState lower = fixFacesForHunk(
bd.withProperty("half", bottomHalfValue(half)),
data, x, z, realX, lowerY, realZ, mantle);
data.set(x, lowerY, z, lower);
data.set(x, upperY, z, upper);
} catch (Throwable e) {
IrisLogging.reportError(e);
}
bd = bd.withProperty("half", bottomHalfValue(half));
return;
}
if (height + 1 < data.getHeight() && B.isAir(data.get(x, height + 1, z))) {
data.set(x, height + 1, z, fixFacesForHunk(bd, data, x, z, realX, height + 1, realZ, mantle));
int targetY = height + 1;
if (targetY < data.getHeight() && B.isAir(data.get(x, targetY, z))) {
data.set(x, targetY, z, fixFacesForHunk(bd, data, x, z, realX, targetY, realZ, mantle));
}
}
@@ -212,10 +232,23 @@ final class DecoratorCore {
int stack = computeStack(decorator, rng, realX, realZ, irisData, effectiveMax);
if (stack == 1) {
if (opts.caveSkipFluid && B.isFluid(data.get(x, height, z))) {
int targetY = height + 1;
if (targetY >= data.getHeight()) {
return;
}
data.set(x, height, z, decorator.pickBlockDataTop(rng, irisData, realX, realZ));
PlatformBlockState existing = data.get(x, targetY, z);
if (!canReplaceStackTarget(existing, opts.underwater)
|| (opts.caveSkipFluid && B.isFluid(existing))) {
return;
}
PlatformBlockState block = decorator.pickBlockDataTop(rng, irisData, realX, realZ);
if (block == null || (!opts.underwater && !canGoOn(block, data.get(x, height, z)))) {
return;
}
data.set(x, targetY, z, block);
return;
}
@@ -244,7 +277,9 @@ final class DecoratorCore {
break;
}
if (opts.caveSkipFluid && B.isFluid(data.get(x, height + 1 + i, z))) {
PlatformBlockState existing = data.get(x, height + 1 + i, z);
if (!canReplaceStackTarget(existing, opts.underwater)
|| (opts.caveSkipFluid && B.isFluid(existing))) {
break;
}
@@ -259,22 +294,28 @@ final class DecoratorCore {
static void placeStackDown(IrisDecorator decorator, int x, int z, int realX, int realZ,
int height, int minHeight, Hunk<PlatformBlockState> data,
RNG rng, IrisData irisData, int max, PlaceOpts opts, EngineMantle mantle) {
if (height < 0 || height >= data.getHeight()) {
return;
}
int stack = computeStack(decorator, rng, realX, realZ, irisData, max);
if (stack == 1) {
if (opts.caveSkipFluid && B.isFluid(data.get(x, height, z))) {
return;
}
data.set(x, height, z, fixFacesForHunk(
decorator.pickBlockDataTop(rng, irisData, realX, realZ),
data, x, z, realX, height, realZ, mantle));
PlatformBlockState block = decorator.pickBlockDataTop(rng, irisData, realX, realZ);
if (block == null) {
return;
}
data.set(x, height, z, fixFacesForHunk(block, data, x, z, realX, height, realZ, mantle));
return;
}
for (int i = 0; i < stack; i++) {
int h = height - i;
if (h < minHeight) {
continue;
if (h < 0 || h < minHeight) {
break;
}
double threshold = ((double) i) / (double) (stack - 1);
@@ -282,7 +323,11 @@ final class DecoratorCore {
? decorator.pickBlockDataTop(rng, irisData, realX, realZ)
: decorator.pickBlockData(rng, irisData, realX, realZ);
if (bd != null && IrisProceduralBlocks.materialKey(bd).equals("minecraft:pointed_dripstone")) {
if (bd == null) {
break;
}
if (IrisProceduralBlocks.materialKey(bd).equals("minecraft:pointed_dripstone")) {
bd = dripstoneBlock(stack, i, "down");
}
@@ -304,17 +349,24 @@ final class DecoratorCore {
String half = IrisProceduralBlocks.propertyValue(bd, "half");
if (half != null) {
int lowerY = height + 1;
int upperY = height + 2;
if (max <= 2 || !canPlaceTwoBlockPlant(data, xf, zf, lowerY, upperY, false)) {
return;
}
try {
if (max > 2) {
data.set(xf, height + 2, zf, bd.withProperty("half", topHalfValue(half)));
}
PlatformBlockState upper = bd.withProperty("half", topHalfValue(half));
PlatformBlockState lower = bd.withProperty("half", bottomHalfValue(half));
data.set(xf, lowerY, zf, lower);
data.set(xf, upperY, zf, upper);
} catch (Throwable e) {
IrisLogging.reportError(e);
}
bd = bd.withProperty("half", bottomHalfValue(half));
return;
}
if (max > 1) {
if (max > 1 && height + 1 < data.getHeight()) {
data.set(xf, height + 1, zf, bd);
}
}
@@ -333,7 +385,7 @@ final class DecoratorCore {
int placed = 0;
for (int i = 0; i < stack; i++) {
int h = height + 1 + i;
if (h >= height + max) {
if (h >= height + max || h >= data.getHeight()) {
break;
}
double threshold = stack == 1 ? 0.0 : ((double) i) / (stack - 1);
@@ -385,7 +437,7 @@ final class DecoratorCore {
int xx = rX + f.getModX();
int zz = rZ + f.getModZ();
if (xx < 0 || xx > 15 || zz < 0 || zz > 15 || yy < 0 || yy > hunk.getHeight()) {
if (xx < 0 || xx > 15 || zz < 0 || zz > 15 || yy < 0 || yy >= hunk.getHeight()) {
continue;
}
@@ -414,6 +466,22 @@ final class DecoratorCore {
return ((BlockData) surface.nativeHandle()).isFaceSturdy(BlockFace.UP, BlockSupport.FULL);
}
static boolean canReplaceStackTarget(PlatformBlockState state, boolean allowFluid) {
return B.isAir(state) || allowFluid && B.isFluid(state);
}
private static boolean canPlaceTwoBlockPlant(Hunk<PlatformBlockState> data, int x, int z,
int lowerY, int upperY, boolean caveSkipFluid) {
if (lowerY < 0 || upperY >= data.getHeight()) {
return false;
}
PlatformBlockState lower = data.get(x, lowerY, z);
PlatformBlockState upper = data.get(x, upperY, z);
return B.isAir(lower) && B.isAir(upper)
&& (!caveSkipFluid || !B.isFluid(lower) && !B.isFluid(upper));
}
private static int computeStack(IrisDecorator decorator, RNG rng, double realX, double realZ,
IrisData irisData, int max) {
int stack = decorator.getHeight(rng, realX, realZ, irisData);
@@ -37,9 +37,16 @@ public final class DecoratorPlatformHooks {
boolean canGoOn(PlatformBlockState surface);
}
public static void bind(FaceFixer faceFixer, SurfaceSturdiness surfaceSturdiness) {
public static synchronized Bindings bind(FaceFixer faceFixer, SurfaceSturdiness surfaceSturdiness) {
Bindings previous = new Bindings(FACE_FIXER, SURFACE_STURDINESS);
FACE_FIXER = faceFixer;
SURFACE_STURDINESS = surfaceSturdiness;
return previous;
}
public static synchronized void restore(Bindings bindings) {
FACE_FIXER = bindings.faceFixer();
SURFACE_STURDINESS = bindings.surfaceSturdiness();
}
static FaceFixer faceFixer() {
@@ -49,4 +56,7 @@ public final class DecoratorPlatformHooks {
static SurfaceSturdiness surfaceSturdiness() {
return SURFACE_STURDINESS;
}
public record Bindings(FaceFixer faceFixer, SurfaceSturdiness surfaceSturdiness) {
}
}
@@ -69,6 +69,6 @@ public class IrisCeilingDecorator extends IrisEngineDecorator {
DecoratorCore.PlaceOpts opts = DecoratorCore.SCRATCH_OPTS.get();
opts.reset();
opts.caveSkipFluid = caveSkipFluid;
DecoratorCore.placeStackDown(decorator, x, z, realX, realZ, height, getEngine().getMinHeight(), data, rng, getData(), max, opts, getEngine().getMantle());
DecoratorCore.placeStackDown(decorator, x, z, realX, realZ, height, 0, data, rng, getData(), max, opts, getEngine().getMantle());
}
}
@@ -51,7 +51,7 @@ public class IrisSeaFloorDecorator extends IrisEngineDecorator {
&& !decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ))) {
return;
}
if (height >= 0 || height < getEngine().getHeight()) {
if (height >= 0 && height < getEngine().getHeight()) {
data.set(x, height, z, decorator.getBlockData100(biome, rng, realX, height, realZ, getData()));
}
return;
@@ -47,8 +47,13 @@ public class IrisSeaSurfaceDecorator extends IrisEngineDecorator {
}
if (!decorator.isStacking()) {
if (height >= 0 || height < getEngine().getHeight()) {
data.set(x, height + 1, z, decorator.getBlockData100(biome, rng, realX, height, realZ, getData()));
int targetY = height + 1;
if (height >= 0 && targetY < getEngine().getHeight()
&& DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) {
PlatformBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData());
if (block != null) {
data.set(x, targetY, z, block);
}
}
return;
}
@@ -59,20 +64,34 @@ public class IrisSeaSurfaceDecorator extends IrisEngineDecorator {
}
if (stack == 1) {
data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData()));
int targetY = height + 1;
if (targetY >= data.getHeight() || !DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) {
return;
}
PlatformBlockState block = decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData());
if (block != null) {
data.set(x, targetY, z, block);
}
return;
}
int engineHeight = getEngine().getHeight();
for (int i = 0; i < stack; i++) {
int h = height + i;
if (h >= max || h >= engineHeight) {
continue;
int targetY = h + 1;
if (h >= max || targetY >= engineHeight
|| !DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) {
break;
}
double threshold = ((double) i) / (stack - 1);
data.set(x, h + 1, z, threshold >= decorator.getTopThreshold()
PlatformBlockState block = threshold >= decorator.getTopThreshold()
? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData())
: decorator.getBlockData100(biome, rng, realX, h, realZ, getData()));
: decorator.getBlockData100(biome, rng, realX, h, realZ, getData());
if (block == null) {
break;
}
data.set(x, targetY, z, block);
}
}
}
@@ -66,7 +66,15 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
}
if (!decorator.isStacking()) {
data.set(x, height + 1, z, decorator.getBlockData100(biome, rng, realX, height, realZ, getData()));
int targetY = height + 1;
if (targetY >= data.getHeight()
|| !DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) {
return;
}
PlatformBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData());
if (block != null) {
data.set(x, targetY, z, block);
}
return;
}
@@ -78,16 +86,33 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
}
if (stack == 1) {
data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData()));
int targetY = height + 1;
if (targetY >= data.getHeight() || !DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) {
return;
}
PlatformBlockState block = decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData());
if (block != null) {
data.set(x, targetY, z, block);
}
return;
}
for (int i = 0; i < stack; i++) {
int h = height + i;
int targetY = h + 1;
if (targetY >= data.getHeight()
|| !DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) {
break;
}
double threshold = ((double) i) / (stack - 1);
data.set(x, h + 1, z, threshold >= decorator.getTopThreshold()
PlatformBlockState block = threshold >= decorator.getTopThreshold()
? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData())
: decorator.getBlockData100(biome, rng, realX, h, realZ, getData()));
: decorator.getBlockData100(biome, rng, realX, h, realZ, getData());
if (block == null) {
break;
}
data.set(x, targetY, z, block);
}
}
}
@@ -36,9 +36,6 @@ import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionCarvingEntry;
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
import art.arcane.iris.engine.object.IrisEngineData;
import art.arcane.iris.engine.object.IrisLootMode;
import art.arcane.iris.engine.object.IrisLootReference;
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.IrisPosition;
@@ -83,7 +80,7 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.stream.Collectors;
public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdater, Renderer, Hotloadable {
public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer, Hotloadable {
IrisComplex getComplex();
default @Nullable UpperDimensionContext getUpperContext() {
@@ -319,18 +316,6 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
void blockUpdatedMetric();
@Override
default void injectTables(KList<IrisLootTable> list, IrisLootReference r, boolean fallback) {
if (r.getMode().equals(IrisLootMode.FALLBACK) && !fallback)
return;
if (r.getMode().equals(IrisLootMode.CLEAR) || r.getMode().equals(IrisLootMode.REPLACE)) {
list.clear();
}
list.addAll(r.getLootTables(getComplex()));
}
EngineEffects getEffects();
default MultiBurst burst() {
@@ -591,7 +576,7 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
if (marker.structureAware()) {
IrisObject placedObject = getData().getObjectLoader().load(object);
IrisStructure structure = IrisData.loadAnyStructure(marker.structureKey(), getData());
IrisStructure structure = getData().load(IrisStructure.class, marker.structureKey(), false);
IrisObjectPlacement placement = placedObject == null || structure == null
? null
: structure.createLootPlacement(object);
@@ -0,0 +1,618 @@
/*
* 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.core.IrisSettings;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.util.common.parallel.BurstExecutor;
import art.arcane.iris.util.common.parallel.MultiBurst;
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.math.Position2;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import java.util.ArrayDeque;
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;
import java.util.function.Function;
public final class HintedLocator<T> implements Locator<T> {
private static final int BIOME_STRIDE_CHUNKS = 4;
private static final int REGION_STRIDE_CHUNKS = 16;
private static final int MAX_SAMPLE_RADIUS_CHUNKS = 100000;
private final Locator<T> exact;
private final Function<Engine, SearchPlan> planner;
public HintedLocator(Locator<T> exact, Function<Engine, SearchPlan> planner) {
this.exact = exact;
this.planner = planner;
}
@FunctionalInterface
public interface CoarseSample {
boolean test(int blockX, int blockZ);
}
public static final class SearchPlan {
private final boolean possible;
private final CoarseSample coarse;
private final int strideChunks;
private final Locator<?> exactOverride;
private SearchPlan(boolean possible, CoarseSample coarse, int strideChunks, Locator<?> exactOverride) {
this.possible = possible;
this.coarse = coarse;
this.strideChunks = strideChunks;
this.exactOverride = exactOverride;
}
public static SearchPlan impossible() {
return new SearchPlan(false, null, 1, null);
}
public static SearchPlan unpruned() {
return new SearchPlan(true, null, 1, null);
}
public static SearchPlan of(CoarseSample coarse, int strideChunks, Locator<?> exactOverride) {
return new SearchPlan(true, coarse, strideChunks, exactOverride);
}
public boolean isPossible() {
return possible;
}
public CoarseSample getCoarse() {
return coarse;
}
public int getStrideChunks() {
return strideChunks;
}
public Locator<?> getExactOverride() {
return exactOverride;
}
}
@Override
public boolean matches(Engine engine, Position2 chunk) {
return exact.matches(engine, chunk);
}
@Override
public Future<Position2> find(Engine engine, Position2 pos, long timeout, Consumer<Integer> checks) throws WrongEngineBroException {
if (engine.isClosed()) {
throw new WrongEngineBroException();
}
Locator.cancelSearch();
return MultiBurst.burst.completeValue(() -> {
AtomicBoolean stop = new AtomicBoolean(false);
LocatorCanceller.cancel = () -> stop.set(true);
try {
SearchPlan plan = planner.apply(engine);
if (!plan.isPossible()) {
return null;
}
return search(engine, plan, pos, timeout, checks, stop);
} finally {
LocatorCanceller.cancel = null;
}
});
}
private Position2 search(Engine engine, SearchPlan plan, Position2 pos, long timeout, Consumer<Integer> checks, AtomicBoolean stop) {
Locator<?> verifier = plan.getExactOverride() != null ? plan.getExactOverride() : exact;
int stride = Math.max(1, plan.getStrideChunks());
int batchTarget = IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism()) * 32;
int maxRing = Math.max(1, MAX_SAMPLE_RADIUS_CHUNKS / stride);
PrecisionStopwatch stopwatch = PrecisionStopwatch.start();
AtomicInteger covered = new AtomicInteger();
KList<Position2> batch = new KList<>();
for (int ring = 0; ring <= maxRing; ring++) {
if (stop.get() || stopwatch.getMilliseconds() >= timeout) {
return null;
}
appendRing(batch, pos, ring, stride);
if (batch.size() < batchTarget && ring < maxRing) {
continue;
}
Position2 result = processBatch(engine, plan, verifier, batch, stride, timeout, stopwatch, covered, checks, stop);
batch.clear();
if (result != null) {
return result;
}
}
return null;
}
private Position2 processBatch(Engine engine, SearchPlan plan, Locator<?> verifier, KList<Position2> batch, int stride, long timeout, PrecisionStopwatch stopwatch, AtomicInteger covered, Consumer<Integer> checks, AtomicBoolean stop) {
int size = batch.size();
boolean[] hits = new boolean[size];
boolean fine = stride <= 1;
CoarseSample coarse = plan.getCoarse();
AtomicBoolean matched = new AtomicBoolean(false);
BurstExecutor executor = MultiBurst.burst.burst(size);
for (int i = 0; i < size; i++) {
int index = i;
Position2 sample = batch.get(i);
executor.queue(() -> {
if (stop.get() || (fine && matched.get())) {
return;
}
int blockX = (sample.getX() << 4) + 8;
int blockZ = (sample.getZ() << 4) + 8;
if (coarse != null && !coarse.test(blockX, blockZ)) {
return;
}
if (fine) {
if (verifier.matches(engine, sample)) {
hits[index] = true;
matched.set(true);
}
return;
}
hits[index] = true;
});
}
executor.complete();
covered.addAndGet(size * stride * stride);
checks.accept(covered.get());
for (int i = 0; i < size; i++) {
if (!hits[i]) {
continue;
}
if (fine) {
return batch.get(i);
}
if (stop.get() || stopwatch.getMilliseconds() >= timeout) {
return null;
}
Position2 refined = refine(engine, verifier, batch.get(i), stride, timeout, stopwatch, stop);
if (refined != null) {
return refined;
}
}
return null;
}
private Position2 refine(Engine engine, Locator<?> verifier, Position2 candidate, int stride, long timeout, PrecisionStopwatch stopwatch, AtomicBoolean stop) {
KList<Position2> cells = new KList<>();
for (int ring = 0; ring <= stride; ring++) {
if (stop.get() || stopwatch.getMilliseconds() >= timeout) {
return null;
}
cells.clear();
appendRing(cells, candidate, ring, 1);
AtomicReference<Position2> found = new AtomicReference<>();
BurstExecutor executor = MultiBurst.burst.burst(cells.size());
for (Position2 cell : cells) {
executor.queue(() -> {
if (stop.get() || found.get() != null) {
return;
}
if (verifier.matches(engine, cell)) {
found.compareAndSet(null, cell);
}
});
}
executor.complete();
if (found.get() != null) {
return found.get();
}
}
return null;
}
static void appendRing(KList<Position2> batch, Position2 origin, int ring, int stride) {
if (ring == 0) {
batch.add(origin);
return;
}
int offset = ring * stride;
for (int dx = -ring; dx <= ring; dx++) {
int x = origin.getX() + (dx * stride);
batch.add(new Position2(x, origin.getZ() - offset));
batch.add(new Position2(x, origin.getZ() + offset));
}
for (int dz = -ring + 1; dz <= ring - 1; dz++) {
int z = origin.getZ() + (dz * stride);
batch.add(new Position2(origin.getX() - offset, z));
batch.add(new Position2(origin.getX() + offset, z));
}
}
public static SearchPlan biomePlan(Engine engine, String biomeKey) {
if (engine.getFocus() != null || engine.getFocusRegion() != null) {
return SearchPlan.unpruned();
}
IrisComplex complex = engine.getComplex();
BiomeSource source = new BiomeSource(engine);
KSet<String> landHosts = new KSet<>();
KSet<String> seaHosts = new KSet<>();
KSet<String> shoreHosts = new KSet<>();
KSet<String> surfaceRegions = new KSet<>();
KSet<String> caveRegions = new KSet<>();
for (IrisRegion region : engine.getDimension().getAllRegions(engine)) {
if (region == null) {
continue;
}
boolean hosted = false;
for (String root : region.getLandBiomes()) {
if (source.closureContains(root, biomeKey)) {
landHosts.add(root);
hosted = true;
}
}
for (String root : region.getSeaBiomes()) {
if (source.closureContains(root, biomeKey)) {
seaHosts.add(root);
hosted = true;
}
}
for (String root : region.getShoreBiomes()) {
if (source.closureContains(root, biomeKey)) {
shoreHosts.add(root);
hosted = true;
}
}
if (hosted) {
surfaceRegions.add(region.getLoadKey());
}
if (region.getCaveBiomes().contains(biomeKey)) {
caveRegions.add(region.getLoadKey());
}
}
if (!surfaceRegions.isEmpty()) {
return SearchPlan.of(surfaceCoarse(complex, surfaceRegions, landHosts, seaHosts, shoreHosts), BIOME_STRIDE_CHUNKS, null);
}
if (!caveRegions.isEmpty()) {
CoarseSample coarse = caveCoarse(complex, caveRegions, biomeKey);
Locator<IrisBiome> caveExact = (e, c) -> {
IrisBiome biome = e.getCaveBiome((c.getX() << 4) + 8, (c.getZ() << 4) + 8);
return biome != null && biomeKey.equals(biome.getLoadKey());
};
return SearchPlan.of(coarse, BIOME_STRIDE_CHUNKS, caveExact);
}
return SearchPlan.impossible();
}
public static SearchPlan caveBiomePlan(Engine engine, String biomeKey) {
if (engine.getFocus() != null || engine.getFocusRegion() != null) {
return SearchPlan.unpruned();
}
IrisComplex complex = engine.getComplex();
KSet<String> caveRegions = new KSet<>();
for (IrisRegion region : engine.getDimension().getAllRegions(engine)) {
if (region != null && region.getCaveBiomes().contains(biomeKey)) {
caveRegions.add(region.getLoadKey());
}
}
if (caveRegions.isEmpty()) {
return SearchPlan.impossible();
}
return SearchPlan.of(caveCoarse(complex, caveRegions, biomeKey), BIOME_STRIDE_CHUNKS, null);
}
public static SearchPlan regionPlan(Engine engine, String regionKey) {
if (engine.getFocus() != null || engine.getFocusRegion() != null) {
return SearchPlan.unpruned();
}
if (!engine.getDimension().getRegions().contains(regionKey)) {
return SearchPlan.impossible();
}
IrisComplex complex = engine.getComplex();
CoarseSample coarse = (x, z) -> {
IrisRegion region = complex.getRegionStream().get(x, z);
return region != null && regionKey.equals(region.getLoadKey());
};
return SearchPlan.of(coarse, REGION_STRIDE_CHUNKS, null);
}
public static SearchPlan objectPlan(Engine engine, String objectKey) {
if (engine.getFocus() != null || engine.getFocusRegion() != null) {
return SearchPlan.unpruned();
}
IrisComplex complex = engine.getComplex();
BiomeSource source = new BiomeSource(engine);
KSet<String> landHosts = new KSet<>();
KSet<String> seaHosts = new KSet<>();
KSet<String> shoreHosts = new KSet<>();
KSet<String> directRegions = new KSet<>();
KSet<String> hostedRegions = new KSet<>();
for (IrisRegion region : engine.getDimension().getAllRegions(engine)) {
if (region == null) {
continue;
}
if (placesObject(region.getSurfaceObjects(), objectKey)) {
directRegions.add(region.getLoadKey());
}
boolean hosted = false;
for (String root : region.getLandBiomes()) {
if (source.closurePlacesObject(root, objectKey)) {
landHosts.add(root);
hosted = true;
}
}
for (String root : region.getSeaBiomes()) {
if (source.closurePlacesObject(root, objectKey)) {
seaHosts.add(root);
hosted = true;
}
}
for (String root : region.getShoreBiomes()) {
if (source.closurePlacesObject(root, objectKey)) {
shoreHosts.add(root);
hosted = true;
}
}
if (hosted) {
hostedRegions.add(region.getLoadKey());
}
}
if (directRegions.isEmpty() && hostedRegions.isEmpty()) {
return SearchPlan.impossible();
}
CoarseSample surface = hostedRegions.isEmpty() ? null : surfaceCoarse(complex, hostedRegions, landHosts, seaHosts, shoreHosts);
CoarseSample coarse = (x, z) -> {
if (!directRegions.isEmpty()) {
IrisRegion region = complex.getRegionStream().get(x, z);
if (region != null && directRegions.contains(region.getLoadKey())) {
return true;
}
}
return surface != null && surface.test(x, z);
};
return SearchPlan.of(coarse, 1, null);
}
private static CoarseSample surfaceCoarse(IrisComplex complex, KSet<String> regions, KSet<String> landHosts, KSet<String> seaHosts, KSet<String> shoreHosts) {
return (x, z) -> {
IrisRegion region = complex.getRegionStream().get(x, z);
if (region == null || !regions.contains(region.getLoadKey())) {
return false;
}
if (!landHosts.isEmpty()) {
IrisBiome biome = complex.getLandBiomeStream().get(x, z);
if (biome != null && landHosts.contains(biome.getLoadKey())) {
return true;
}
}
if (!seaHosts.isEmpty()) {
IrisBiome biome = complex.getSeaBiomeStream().get(x, z);
if (biome != null && seaHosts.contains(biome.getLoadKey())) {
return true;
}
}
if (!shoreHosts.isEmpty()) {
IrisBiome biome = complex.getShoreBiomeStream().get(x, z);
if (biome != null && shoreHosts.contains(biome.getLoadKey())) {
return true;
}
}
return false;
};
}
private static CoarseSample caveCoarse(IrisComplex complex, KSet<String> caveRegions, String biomeKey) {
return (x, z) -> {
IrisRegion region = complex.getRegionStream().get(x, z);
if (region == null || !caveRegions.contains(region.getLoadKey())) {
return false;
}
IrisBiome cave = complex.getCaveBiomeStream().get(x, z);
return cave != null && biomeKey.equals(cave.getLoadKey());
};
}
private static boolean placesObject(KList<IrisObjectPlacement> placements, String objectKey) {
for (IrisObjectPlacement placement : placements) {
if (placement != null && placement.getPlace().contains(objectKey)) {
return true;
}
}
return false;
}
private static final class BiomeSource {
private final Engine engine;
private final KMap<String, IrisBiome> cache = new KMap<>();
private BiomeSource(Engine engine) {
this.engine = engine;
}
private IrisBiome load(String key) {
if (key == null) {
return null;
}
IrisBiome cached = cache.get(key);
if (cached != null) {
return cached;
}
IrisBiome loaded = engine.getData().getBiomeLoader().load(key);
if (loaded != null) {
cache.put(key, loaded);
}
return loaded;
}
private boolean closureContains(String rootKey, String targetKey) {
if (rootKey == null || targetKey == null) {
return false;
}
KSet<String> visited = new KSet<>();
ArrayDeque<String> queue = new ArrayDeque<>();
queue.add(rootKey);
while (!queue.isEmpty()) {
String key = queue.poll();
if (!visited.add(key)) {
continue;
}
if (key.equals(targetKey)) {
return true;
}
IrisBiome biome = load(key);
if (biome == null) {
continue;
}
for (String child : biome.getChildren()) {
if (child != null && !visited.contains(child)) {
queue.add(child);
}
}
}
return false;
}
private boolean closurePlacesObject(String rootKey, String objectKey) {
if (rootKey == null || objectKey == null) {
return false;
}
KSet<String> visited = new KSet<>();
ArrayDeque<String> queue = new ArrayDeque<>();
queue.add(rootKey);
while (!queue.isEmpty()) {
String key = queue.poll();
if (!visited.add(key)) {
continue;
}
IrisBiome biome = load(key);
if (biome == null) {
continue;
}
if (placesObject(biome.getSurfaceObjects(), objectKey)) {
return true;
}
for (String child : biome.getChildren()) {
if (child != null && !visited.contains(child)) {
queue.add(child);
}
}
}
return false;
}
}
}
@@ -19,16 +19,21 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.structure.StructureGraphCatalog;
import art.arcane.iris.engine.framework.structure.StructureGraphCompilation;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisStructure;
import art.arcane.iris.engine.object.IrisStructurePlacement;
import art.arcane.iris.engine.object.NativeStructureSuppression;
import art.arcane.iris.engine.object.ObjectPlaceMode;
import art.arcane.iris.engine.object.StructureDistribution;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import java.util.ArrayList;
import java.util.Collections;
@@ -36,6 +41,7 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Pattern;
/**
* Finds where IRIS_PLACED structures generate. A structure key matches either the iris
@@ -53,10 +59,14 @@ import java.util.Set;
*/
public final class IrisStructureLocator {
private static final int DENSITY_CANDIDATE_BUDGET = 4_096;
private static final int MAX_BURIAL_COLUMNS = 2_000_000;
private static final int UNDERGROUND_SURFACE_CLEARANCE = 1;
private static final Pattern NAMESPACED_RESOURCE_KEY = Pattern.compile("[a-z0-9_.-]+:[a-z0-9/._-]+");
private static final Cache<Engine, PlacementIndex> INDEX_CACHE = Caffeine.newBuilder().weakKeys().build();
private static final PlacementIndex EMPTY_INDEX = new PlacementIndex(
Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptyList());
Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet(),
Collections.emptyList());
private static final LocateResult NOT_FOUND_RESULT = new LocateResult(LocateStatus.NOT_FOUND, 0, 0, 0);
private static final LocateResult SEARCH_LIMIT_RESULT =
new LocateResult(LocateStatus.SEARCH_LIMIT_REACHED, 0, 0, 0);
@@ -79,14 +89,14 @@ public final class IrisStructureLocator {
PlacementIndex placementIndex = index(engine);
String normalizedKey = normalize(key);
return placementIndex.normalizedLoadKeys.contains(normalizedKey)
|| placementIndex.vanillaSources.contains(normalizedKey);
|| placementIndex.vanillaAliases.contains(normalizedKey);
}
public static boolean suppressesVanilla(Engine engine, String vanillaKey) {
if (engine == null || vanillaKey == null || vanillaKey.isEmpty()) {
return false;
}
return index(engine).vanillaSources.contains(normalize(vanillaKey));
return index(engine).suppressedVanillaSources.contains(normalize(vanillaKey));
}
public static void invalidate(Engine engine) {
@@ -117,7 +127,7 @@ public final class IrisStructureLocator {
}
SeedManager seedManager = engine.getSeedManager();
if (seedManager == null) {
return NOT_FOUND_RESULT;
throw new IllegalStateException("Iris structure locate requires a bound seed manager for '" + key + "'");
}
long seed = seedManager.getMantle();
@@ -210,18 +220,20 @@ public final class IrisStructureLocator {
return new LocateResult(LocateStatus.FOUND, resolved.originX(), resolved.baseY(), resolved.originZ());
}
public static ResolvedPlacement resolvePlacement(Engine engine, IrisStructurePlacement placement, int cx, int cz, int placementOrdinal) {
if (engine == null || placement == null || placement.getDistribution() == null
|| placement.getStructures() == null || placement.getStructures().isEmpty()
|| engine.getData() == null || engine.getSeedManager() == null) {
return null;
public static ResolvedPlacement resolvePlacement(Engine engine, IrisStructurePlacement placement, int cx, int cz) {
if (engine == null || engine.getData() == null || engine.getSeedManager() == null) {
throw new IllegalStateException("Iris structure placement requires a fully bound engine");
}
if (placement == null || placement.getDistribution() == null
|| placement.getStructures() == null || placement.getStructures().isEmpty()) {
throw new IllegalStateException("Iris structure placement is missing its distribution or structure list");
}
long seed = engine.getSeedManager().getMantle();
if (!StructurePlacementGrid.startsInChunk(placement, cx, cz, seed, placementOrdinal)) {
if (!StructurePlacementGrid.startsInChunk(placement, cx, cz, seed)) {
return null;
}
RNG rng = StructurePlacementGrid.placementRng(placement, cx, cz, seed, placementOrdinal);
RNG rng = StructurePlacementGrid.placementRng(placement, cx, cz, seed);
int originX = (cx << 4) + rng.nextInt(16);
int originZ = (cz << 4) + rng.nextInt(16);
Integer baseY = resolveBaseY(engine, placement, originX, originZ, rng);
@@ -231,26 +243,39 @@ public final class IrisStructureLocator {
String selectedKey = selectStructureKey(placement, rng);
if (selectedKey == null || selectedKey.isBlank()) {
return null;
throw new IllegalStateException("Iris structure placement selected a blank structure key");
}
IrisStructure structure = IrisData.loadAnyStructure(selectedKey, engine.getData());
IrisStructure structure = engine.getData().load(IrisStructure.class, selectedKey, false);
if (structure == null) {
return null;
throw new IllegalStateException("Iris structure placement references missing structure '"
+ selectedKey + "'");
}
StructureGraphCompilation compilation = StructureGraphCatalog.compile(engine.getData(), structure);
if (!compilation.isAssemblyViable()) {
throw new IllegalStateException("Iris structure '" + selectedKey
+ "' has no runtime-viable assembly graph");
}
StructureAssembler assembler = new StructureAssembler(engine.getData(), structure, originX, baseY, originZ);
StructureAssembler assembler = StructureAssembler.forData(
engine.getData(), structure, new IrisPosition(originX, baseY, originZ));
KList<PlacedStructurePiece> pieces = assembler.assemble(rng);
if (pieces == null || pieces.isEmpty()) {
if (!requirePlacementOutput(placement, selectedKey, cx, cz,
pieces != null && !pieces.isEmpty(), "runtime assembly produced no pieces")) {
return null;
}
pieces = alignSurfacePieces(pieces, placement, structure, baseY);
if (!requirePlacementOutput(placement, selectedKey, cx, cz,
pieces != null && !pieces.isEmpty(), "surface alignment produced no pieces")) {
return null;
}
boolean exactY = hasExactY(placement, structure, pieces);
int worldMin = engine.getMinHeight() + 1;
int worldMax = engine.getMinHeight() + engine.getHeight() - 1;
if (exactY) {
Integer verticalShift = resolveVerticalShift(pieces, placement, baseY, worldMin, worldMax);
if (verticalShift == null) {
if (!requirePlacementOutput(placement, selectedKey, cx, cz, verticalShift != null,
"assembled pieces cannot fit the configured vertical and world bounds")) {
return null;
}
if (verticalShift != 0) {
@@ -258,16 +283,49 @@ public final class IrisStructureLocator {
baseY += verticalShift;
}
}
if (placement.isUnderground()) {
Integer burialShift = resolveUndergroundBurialShift(
engine, pieces, placement, baseY, worldMin, worldMax);
if (!requirePlacementOutput(placement, selectedKey, cx, cz, burialShift != null,
"assembled pieces and carving envelope cannot remain fully buried beneath terrain")) {
return null;
}
if (burialShift != 0) {
pieces = shiftPieces(pieces, burialShift);
baseY += burialShift;
}
}
long configuredRadius = (long) Math.max(1, structure.getMaxSizeChunks()) * 16L;
int structureRadius = (int) Math.min(Integer.MAX_VALUE, configuredRadius);
if (!fitsHorizontalBounds(pieces, originX, originZ, structureRadius)
|| (exactY && !fitsVerticalBounds(pieces, worldMin, worldMax))) {
boolean withinBounds = fitsHorizontalBounds(pieces, originX, originZ, structureRadius)
&& (!exactY || fitsVerticalBounds(pieces, worldMin, worldMax));
if (!requirePlacementOutput(placement, selectedKey, cx, cz, withinBounds,
"assembled pieces exceed the configured structure or world bounds")) {
return null;
}
return new ResolvedPlacement(placement, selectedKey, structure, pieces, rng, originX, baseY, originZ, exactY);
}
public static boolean requirePlacementOutput(IrisStructurePlacement placement, String structureKey,
int chunkX, int chunkZ, boolean outputPresent,
String failureReason) {
if (outputPresent) {
return true;
}
if (placement == null
|| NativeStructureSuppression.REPLACE_SOURCE != placement.getNativeSuppression()) {
return false;
}
String key = structureKey == null || structureKey.isBlank()
? String.valueOf(placement.getStructures()) : structureKey;
String reason = failureReason == null || failureReason.isBlank()
? "placement produced no output" : failureReason;
throw new IllegalStateException("REPLACE_SOURCE placement for Iris structure '" + key
+ "' failed in chunk " + chunkX + "," + chunkZ + ": " + reason
+ ". Native generation is suppressed and will not be used as a fallback");
}
static boolean fitsWorldBounds(KList<PlacedStructurePiece> pieces, int worldMin, int worldMax,
int originX, int originZ, int structureRadius) {
int[] bounds = computeBounds(pieces);
@@ -281,12 +339,11 @@ public final class IrisStructureLocator {
private static ResolvedPlacement resolveInChunk(Engine engine, String key, int cx, int cz) {
IrisData data = engine.getData();
KList<IrisStructurePlacement> placements = placementsAt(engine, cx, cz);
for (int placementOrdinal = 0; placementOrdinal < placements.size(); placementOrdinal++) {
IrisStructurePlacement placement = placements.get(placementOrdinal);
for (IrisStructurePlacement placement : placements) {
if (!matches(placement, key, data)) {
continue;
}
ResolvedPlacement resolved = resolvePlacement(engine, placement, cx, cz, placementOrdinal);
ResolvedPlacement resolved = resolvePlacement(engine, placement, cx, cz);
if (resolved != null && matchesResolved(resolved, key)) {
return resolved;
}
@@ -337,6 +394,86 @@ public final class IrisStructureLocator {
return 0;
}
static Integer resolveUndergroundBurialShift(Engine engine, KList<PlacedStructurePiece> pieces,
IrisStructurePlacement placement, int baseY,
int worldMin, int worldMax) {
int[] bounds = computeBounds(pieces);
if (engine == null || bounds == null || placement == null || !placement.isUnderground()) {
return null;
}
int sideExtension = placement.isOverbore()
? Math.max(1, placement.getOverboreRadius())
: placement.isBore() ? Math.max(0, placement.getBorePadding()) : 0;
int topExtension = placement.isOverbore()
? (int) Math.ceil(Math.max(1D, placement.getOverboreHeight()) * 1.8D)
: placement.isBore() ? Math.max(0, placement.getBorePadding()) : 0;
int bottomExtension = placement.isOverbore() ? Math.max(0, placement.getOverboreFloor()) : 0;
int bandMin = Math.max(worldMin, Math.min(placement.getMinHeight(), placement.getMaxHeight()));
int bandMax = Math.min(worldMax, Math.max(placement.getMinHeight(), placement.getMaxHeight()));
int minimumShift = Math.max(worldMin - (bounds[1] - bottomExtension), bandMin - baseY);
int maximumShift = Math.min(0, Math.min(worldMax - (bounds[4] + topExtension), bandMax - baseY));
if (minimumShift > maximumShift) {
return null;
}
Long2IntOpenHashMap surfaceHeights = new Long2IntOpenHashMap();
surfaceHeights.defaultReturnValue(Integer.MIN_VALUE);
if (placement.isBore() && !placement.isOverbore()) {
maximumShift = resolveBurialEnvelopeShift(
engine,
bounds[0] - sideExtension,
bounds[3] + sideExtension,
bounds[2] - sideExtension,
bounds[5] + sideExtension,
bounds[4] + topExtension,
maximumShift,
surfaceHeights
);
if (maximumShift == Integer.MIN_VALUE) {
return null;
}
} else {
for (PlacedStructurePiece piece : pieces) {
maximumShift = resolveBurialEnvelopeShift(
engine,
piece.getMinX() - sideExtension,
piece.getMaxX() + sideExtension,
piece.getMinZ() - sideExtension,
piece.getMaxZ() + sideExtension,
piece.getMaxY() + topExtension,
maximumShift,
surfaceHeights
);
if (maximumShift == Integer.MIN_VALUE) {
return null;
}
}
}
return minimumShift > maximumShift ? null : maximumShift;
}
private static int resolveBurialEnvelopeShift(Engine engine, int minX, int maxX, int minZ, int maxZ,
int envelopeTopY, int maximumShift,
Long2IntOpenHashMap surfaceHeights) {
for (int x = minX; x <= maxX; x++) {
for (int z = minZ; z <= maxZ; z++) {
long columnKey = ((long) x << 32) ^ (z & 0xffffffffL);
int surfaceY = surfaceHeights.get(columnKey);
if (surfaceY == Integer.MIN_VALUE) {
if (surfaceHeights.size() >= MAX_BURIAL_COLUMNS) {
return Integer.MIN_VALUE;
}
surfaceY = engine.getHeight(x, z, true) + engine.getMinHeight();
surfaceHeights.put(columnKey, surfaceY);
}
int allowedTopY = surfaceY - UNDERGROUND_SURFACE_CLEARANCE;
maximumShift = Math.min(maximumShift, allowedTopY - envelopeTopY);
}
}
return maximumShift;
}
static String selectStructureKey(IrisStructurePlacement placement, RNG rng) {
if (placement == null || placement.getStructures() == null || placement.getStructures().isEmpty()) {
return null;
@@ -432,7 +569,8 @@ public final class IrisStructureLocator {
}
if (placement.getDistribution() == StructureDistribution.RANDOM_SPREAD) {
randomSpread.add(new RandomSpreadParameters(
Math.max(1, placement.getSpacing()), placement.getSeparation(), placement.getSalt()));
Math.max(1, placement.getSpacing()), placement.getSeparation(),
StructurePlacementGrid.placementSalt(placement)));
} else if (placement.getDistribution() == StructureDistribution.CONCENTRIC_RINGS) {
concentricRings.add(placement);
} else if (isSearchableDensityPlacement(engine, placement)) {
@@ -511,7 +649,7 @@ public final class IrisStructureLocator {
if (structureKey == null || structureKey.isBlank()) {
continue;
}
IrisStructure structure = IrisData.loadAnyStructure(structureKey, data);
IrisStructure structure = data.load(IrisStructure.class, structureKey, false);
if (structure == null) {
continue;
}
@@ -536,9 +674,12 @@ public final class IrisStructureLocator {
}
private static PlacementIndex index(Engine engine) {
if (engine == null || engine.getData() == null || engine.getDimension() == null) {
if (engine == null) {
return EMPTY_INDEX;
}
if (engine.getData() == null || engine.getDimension() == null) {
throw new IllegalStateException("Iris structure index requires a fully bound engine and dimension");
}
return INDEX_CACHE.get(engine, ignored -> build(engine));
}
@@ -546,42 +687,63 @@ public final class IrisStructureLocator {
IrisData data = engine.getData();
Set<String> loadKeys = new LinkedHashSet<>();
Set<String> normalizedLoadKeys = new LinkedHashSet<>();
Set<String> vanillaSources = new LinkedHashSet<>();
Set<String> vanillaAliases = new LinkedHashSet<>();
Set<String> suppressedVanillaSources = new LinkedHashSet<>();
List<IrisStructurePlacement> placements = new ArrayList<>();
collect(engine.getDimension().getStructures(), data, loadKeys, normalizedLoadKeys, vanillaSources, placements);
collect(engine.getDimension().getStructures(), data, loadKeys, normalizedLoadKeys, vanillaAliases,
suppressedVanillaSources, placements, true);
for (IrisRegion region : engine.getDimension().getAllRegions(engine)) {
collect(region.getStructures(), data, loadKeys, normalizedLoadKeys, vanillaSources, placements);
collect(region.getStructures(), data, loadKeys, normalizedLoadKeys, vanillaAliases,
suppressedVanillaSources, placements, false);
}
for (IrisBiome biome : engine.getDimension().getReachableBiomes(engine)) {
collect(biome.getStructures(), data, loadKeys, normalizedLoadKeys, vanillaSources, placements);
collect(biome.getStructures(), data, loadKeys, normalizedLoadKeys, vanillaAliases,
suppressedVanillaSources, placements, false);
}
return new PlacementIndex(
Collections.unmodifiableSet(loadKeys),
Collections.unmodifiableSet(normalizedLoadKeys),
Collections.unmodifiableSet(vanillaSources),
Collections.unmodifiableSet(vanillaAliases),
Collections.unmodifiableSet(suppressedVanillaSources),
List.copyOf(placements));
}
private static void collect(KList<IrisStructurePlacement> source, IrisData data, Set<String> loadKeys,
Set<String> normalizedLoadKeys, Set<String> vanillaSources,
List<IrisStructurePlacement> placements) {
Set<String> normalizedLoadKeys, Set<String> vanillaAliases,
Set<String> suppressedVanillaSources,
List<IrisStructurePlacement> placements, boolean allowNativeSuppression) {
if (source == null) {
return;
}
for (IrisStructurePlacement placement : source) {
if (placement == null || placement.getStructures() == null) {
continue;
if (placement == null) {
throw new IllegalStateException("Iris structure placement list contains a null placement");
}
if (placement.getDistribution() == null) {
throw new IllegalStateException("Iris structure placement is missing its distribution");
}
boolean replacesNative = NativeStructureSuppression.REPLACE_SOURCE == placement.getNativeSuppression();
if (replacesNative && !allowNativeSuppression) {
throw new IllegalStateException("REPLACE_SOURCE is only valid on dimension-level structure placements");
}
if (placement.getStructures() == null || placement.getStructures().isEmpty()) {
throw new IllegalStateException(replacesNative
? "REPLACE_SOURCE requires at least one Iris structure reference"
: "Iris structure placement requires at least one structure reference");
}
boolean validPlacement = false;
for (String structureKey : placement.getStructures()) {
if (structureKey == null || structureKey.isBlank()) {
continue;
throw new IllegalStateException(replacesNative
? "REPLACE_SOURCE contains a blank Iris structure reference"
: "Iris structure placement contains a blank structure reference");
}
IrisStructure structure = IrisData.loadAnyStructure(structureKey, data);
IrisStructure structure = data.load(IrisStructure.class, structureKey, false);
if (structure == null) {
continue;
throw new IllegalStateException((replacesNative
? "REPLACE_SOURCE references missing Iris structure '"
: "Iris structure placement references missing structure '")
+ structureKey + "'");
}
validPlacement = true;
loadKeys.add(structureKey);
normalizedLoadKeys.add(normalize(structureKey));
if (structure.getLoadKey() != null && !structure.getLoadKey().isBlank()) {
@@ -589,12 +751,22 @@ public final class IrisStructureLocator {
normalizedLoadKeys.add(normalize(structure.getLoadKey()));
}
if (structure.getVanillaSource() != null && !structure.getVanillaSource().isBlank()) {
vanillaSources.add(normalize(structure.getVanillaSource()));
vanillaAliases.add(normalize(structure.getVanillaSource()));
}
if (replacesNative) {
String vanillaSource = structure.getVanillaSource();
if (vanillaSource == null || !NAMESPACED_RESOURCE_KEY.matcher(vanillaSource).matches()) {
throw new IllegalStateException("REPLACE_SOURCE structure '" + structureKey
+ "' must declare a valid namespaced vanillaSource");
}
if (!StructureGraphCatalog.guaranteesRuntimeOutput(data, structure)) {
throw new IllegalStateException("REPLACE_SOURCE structure '" + structureKey
+ "' is not runtime-viable; native generation will not be used as a fallback");
}
suppressedVanillaSources.add(normalize(vanillaSource));
}
}
if (validPlacement) {
placements.add(placement);
}
placements.add(placement);
}
}
@@ -628,14 +800,17 @@ public final class IrisStructureLocator {
private static final class PlacementIndex {
private final Set<String> loadKeys;
private final Set<String> normalizedLoadKeys;
private final Set<String> vanillaSources;
private final Set<String> vanillaAliases;
private final Set<String> suppressedVanillaSources;
private final List<IrisStructurePlacement> placements;
private PlacementIndex(Set<String> loadKeys, Set<String> normalizedLoadKeys,
Set<String> vanillaSources, List<IrisStructurePlacement> placements) {
Set<String> vanillaAliases, Set<String> suppressedVanillaSources,
List<IrisStructurePlacement> placements) {
this.loadKeys = loadKeys;
this.normalizedLoadKeys = normalizedLoadKeys;
this.vanillaSources = vanillaSources;
this.vanillaAliases = vanillaAliases;
this.suppressedVanillaSources = suppressedVanillaSources;
this.placements = placements;
}
}
@@ -50,15 +50,18 @@ public interface Locator<T> {
}
static Locator<IrisRegion> region(String loadKey) {
return (e, c) -> e.getRegion((c.getX() << 4) + 8, (c.getZ() << 4) + 8).getLoadKey().equals(loadKey);
Locator<IrisRegion> exact = (e, c) -> e.getRegion((c.getX() << 4) + 8, (c.getZ() << 4) + 8).getLoadKey().equals(loadKey);
return new HintedLocator<>(exact, (engine) -> HintedLocator.regionPlan(engine, loadKey));
}
static Locator<IrisObject> object(String loadKey) {
return (e, c) -> e.getObjectsAt(c.getX(), c.getZ()).contains(loadKey);
Locator<IrisObject> exact = (e, c) -> e.getObjectsAt(c.getX(), c.getZ()).contains(loadKey);
return new HintedLocator<>(exact, (engine) -> HintedLocator.objectPlan(engine, loadKey));
}
static Locator<IrisBiome> surfaceBiome(String loadKey) {
return (e, c) -> e.getSurfaceBiome((c.getX() << 4) + 8, (c.getZ() << 4) + 8).getLoadKey().equals(loadKey);
Locator<IrisBiome> exact = (e, c) -> e.getSurfaceBiome((c.getX() << 4) + 8, (c.getZ() << 4) + 8).getLoadKey().equals(loadKey);
return new HintedLocator<>(exact, (engine) -> HintedLocator.biomePlan(engine, loadKey));
}
static Locator<art.arcane.iris.engine.object.IrisStructure> structure(String key) {
@@ -67,13 +70,14 @@ public interface Locator<T> {
static Locator<BlockPos> poi(String type) {
return (e, c) -> {
Set<Pair<String, BlockPos>> pos = e.getPOIsAt((c.getX() << 4) + 8, (c.getZ() << 4) + 8);
Set<Pair<String, BlockPos>> pos = e.getPOIsAt(c.getX(), c.getZ());
return pos.stream().anyMatch(p -> p.getA().equals(type));
};
}
static Locator<IrisBiome> caveBiome(String loadKey) {
return (e, c) -> e.getCaveBiome((c.getX() << 4) + 8, (c.getZ() << 4) + 8).getLoadKey().equals(loadKey);
Locator<IrisBiome> exact = (e, c) -> e.getCaveBiome((c.getX() << 4) + 8, (c.getZ() << 4) + 8).getLoadKey().equals(loadKey);
return new HintedLocator<>(exact, (engine) -> HintedLocator.caveBiomePlan(engine, loadKey));
}
static Locator<IrisBiome> caveOrMantleBiome(String loadKey) {
@@ -0,0 +1,299 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.framework;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisLootMode;
import art.arcane.iris.engine.object.IrisLootReference;
import art.arcane.iris.engine.object.IrisLootTable;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.ToIntFunction;
public final class LootResolver {
private static final int MAX_SCALED_SOURCE_COUNT = 256;
private static final long CONTAINER_SALT = 0x632BE59BD9B4E019L;
private static final long TABLE_SALT = 0x94D049BB133111EBL;
private static final long ENTRY_SALT = 0x9E3779B97F4A7C15L;
private LootResolver() {
}
public static RNG containerRng(long lootSeed, int x, int y, int z) {
return new RNG(containerSeed(lootSeed, x, y, z));
}
public static long containerSeed(long lootSeed, int x, int y, int z) {
long seed = mix(lootSeed ^ CONTAINER_SALT);
seed = mix(seed ^ ((long) x * 0xD6E8FEB86659FD93L));
seed = mix(seed ^ ((long) y * 0xA5A3564E27F886A7L));
return mix(seed ^ ((long) z * 0x8CB92BA72F3D8DD7L));
}
public static RNG tableRng(long lootSeed, IrisLootTable table, int x, int y, int z) {
return new RNG(tableSeed(lootSeed, table, x, y, z));
}
public static long tableSeed(long lootSeed, IrisLootTable table, int x, int y, int z) {
return mix(containerSeed(lootSeed, x, y, z) ^ stableHash(tableIdentity(table)) ^ TABLE_SALT);
}
public static boolean spatialOneIn(
long lootSeed,
IrisLootTable table,
int entryIndex,
int x,
int y,
int z,
long rarity
) {
if (rarity <= 1L) {
return true;
}
long roll = mix(tableSeed(lootSeed, table, x, y, z) ^ ((long) entryIndex * ENTRY_SALT));
return new RNG(roll).nextLong(rarity) == 0L;
}
public static long combinedRarity(int tableRarity, int entryRarity) {
long table = Math.max(1, tableRarity);
long entry = Math.max(1, entryRarity);
if (table > Long.MAX_VALUE / entry) {
return Long.MAX_VALUE;
}
return table * entry;
}
public static boolean oneIn(RNG rng, int rarity) {
int bound = Math.max(1, rarity);
return bound == 1 || rng.nextInt(bound) == 0;
}
public static int inclusive(RNG rng, int first, int second) {
int lower = Math.min(first, second);
int upper = Math.max(first, second);
if (lower == upper) {
return lower;
}
long span = (long) upper - lower + 1L;
return (int) (lower + rng.nextLong(span));
}
public static <T> T pickWeighted(List<T> choices, ToIntFunction<T> weight, RNG rng) {
long total = 0L;
for (T choice : choices) {
int value = Math.max(0, weight.applyAsInt(choice));
total = Long.MAX_VALUE - total < value ? Long.MAX_VALUE : total + value;
}
if (total <= 0L) {
return null;
}
long pull = rng.nextLong(total);
for (T choice : choices) {
pull -= Math.max(0, weight.applyAsInt(choice));
if (pull < 0L) {
return choice;
}
}
return null;
}
public static <T> void resolveEnvironmentSources(
List<T> sources,
Engine engine,
RNG rng,
int x,
int relativeY,
int z,
boolean objectLootDefined,
Function<IrisLootTable, T> mapper
) {
IrisRegion region = engine.getComplex().getRegionStream().get(x, z);
IrisBiome surfaceBiome = engine.getComplex().getTrueBiomeStream().get(x, z);
double terrainHeight = engine.getComplex().getHeightStream().get(x, z);
IrisBiome contextualBiome = relativeY < terrainHeight
? engine.getCaveBiome(x, relativeY, z)
: surfaceBiome;
if (contextualBiome == null) {
contextualBiome = surfaceBiome;
}
boolean distinctContextualBiome = !sameBiome(surfaceBiome, contextualBiome);
double multiplier = engine.getDimension().getLoot().getMultiplier()
* region.getLoot().getMultiplier()
* surfaceBiome.getLoot().getMultiplier();
if (distinctContextualBiome) {
multiplier *= contextualBiome.getLoot().getMultiplier();
}
boolean fallback = !objectLootDefined;
injectReference(sources, engine.getDimension().getLoot(), engine, mapper, fallback);
injectReference(sources, region.getLoot(), engine, mapper, fallback);
injectReference(sources, surfaceBiome.getLoot(), engine, mapper, fallback);
if (distinctContextualBiome) {
injectReference(sources, contextualBiome.getLoot(), engine, mapper, fallback);
}
scaleSources(sources, multiplier, rng);
}
public static <T> void injectSources(
List<T> sources,
List<? extends T> additions,
IrisLootMode mode,
boolean fallback
) {
if (mode == IrisLootMode.FALLBACK && !fallback) {
return;
}
if (mode == IrisLootMode.CLEAR || mode == IrisLootMode.REPLACE) {
sources.clear();
}
sources.addAll(additions);
}
public static <T> void scaleSources(List<T> sources, double multiplier, RNG rng) {
if (!Double.isFinite(multiplier) || multiplier < 0D) {
throw new IllegalArgumentException("Effective loot source multiplier must be finite and non-negative, got "
+ multiplier + ".");
}
if (sources.isEmpty()) {
return;
}
if (sources.size() > MAX_SCALED_SOURCE_COUNT) {
throw new IllegalArgumentException("Loot source scaling starts above the maximum of "
+ MAX_SCALED_SOURCE_COUNT + " sources: " + sources.size() + ".");
}
double scaledSize = sources.size() * multiplier;
if (!Double.isFinite(scaledSize) || scaledSize > MAX_SCALED_SOURCE_COUNT) {
throw new IllegalArgumentException("Loot source scaling exceeds the maximum of "
+ MAX_SCALED_SOURCE_COUNT + " sources: " + sources.size() + " * " + multiplier + ".");
}
int target = (int) Math.round(scaledSize);
if (target == sources.size()) {
return;
}
List<T> original = new ArrayList<>(sources);
int[] order = new int[original.size()];
if (target < original.size()) {
shrinkSources(sources, original, order, target, rng);
return;
}
expandSources(sources, original, order, target, rng);
}
static boolean sameBiome(IrisBiome first, IrisBiome second) {
if (first == second) {
return true;
}
if (first == null || second == null) {
return false;
}
return Objects.equals(first.getLoadKey(), second.getLoadKey());
}
static String tableIdentity(IrisLootTable table) {
if (table == null) {
return "";
}
String loadKey = table.getLoadKey();
if (loadKey != null && !loadKey.isBlank()) {
return loadKey;
}
String name = table.getName();
return name == null ? "" : name;
}
private static <T> void injectReference(
List<T> sources,
IrisLootReference reference,
Engine engine,
Function<IrisLootTable, T> mapper,
boolean fallback
) {
KList<IrisLootTable> tables = reference.getLootTables(engine.getComplex());
KList<T> additions = new KList<>();
for (IrisLootTable table : tables) {
if (table == null) {
continue;
}
T source = mapper.apply(table);
if (source != null) {
additions.add(source);
}
}
injectSources(sources, additions, reference.getMode(), fallback);
}
private static <T> void shrinkSources(List<T> sources, List<T> original, int[] order, int target, RNG rng) {
shuffleOrder(order, rng);
boolean[] selected = new boolean[original.size()];
for (int index = 0; index < target; index++) {
selected[order[index]] = true;
}
sources.clear();
for (int index = 0; index < original.size(); index++) {
if (selected[index]) {
sources.add(original.get(index));
}
}
}
private static <T> void expandSources(List<T> sources, List<T> original, int[] order, int target, RNG rng) {
while (sources.size() < target) {
shuffleOrder(order, rng);
int additions = Math.min(target - sources.size(), original.size());
for (int index = 0; index < additions; index++) {
sources.add(original.get(order[index]));
}
}
}
private static void shuffleOrder(int[] order, RNG rng) {
for (int index = 0; index < order.length; index++) {
order[index] = index;
}
for (int index = 0; index < order.length - 1; index++) {
int selected = index + rng.nextInt(order.length - index);
int previous = order[index];
order[index] = order[selected];
order[selected] = previous;
}
}
private static long stableHash(String value) {
long hash = 0xCBF29CE484222325L;
for (int i = 0; i < value.length(); i++) {
hash ^= value.charAt(i);
hash *= 0x100000001B3L;
}
return mix(hash);
}
private static long mix(long value) {
long mixed = value;
mixed = (mixed ^ (mixed >>> 30)) * 0xBF58476D1CE4E5B9L;
mixed = (mixed ^ (mixed >>> 27)) * 0x94D049BB133111EBL;
return mixed ^ (mixed >>> 31);
}
}
@@ -0,0 +1,62 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.framework;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
import java.util.Objects;
public final class NativeStructureGenerationPolicy {
private NativeStructureGenerationPolicy() {
}
public static IrisNativeStructureDecision resolve(Engine engine, String structureKey,
boolean undergroundStep) {
Engine activeEngine = Objects.requireNonNull(engine, "Native structure policy requires an engine");
IrisDimension dimension = Objects.requireNonNull(activeEngine.getDimension(),
"Native structure policy requires a bound dimension");
IrisImportedStructureControl control = Objects.requireNonNull(
dimension.getImportedStructures(),
"Dimension importedStructures must not be null");
IrisNativeStructureDecision decision = control.resolve(structureKey, undergroundStep);
if (!decision.generate()) {
return decision;
}
if (IrisStructureLocator.suppressesVanilla(activeEngine, structureKey)) {
return decision.withStatus(NativeStructureGenerationStatus.REPLACED_BY_IRIS);
}
return decision;
}
public static String generationStatusMessage(String structureKey,
NativeStructureGenerationStatus status) {
String key = structureKey == null ? "" : structureKey.trim();
return switch (Objects.requireNonNull(status, "Native structure status must not be null")) {
case GENERATE_NATIVE -> "Native structure " + key + " generates natively.";
case DISABLED_BY_PACK -> "Native structure " + key
+ " is disabled by this dimension's importedStructures settings.";
case REPLACED_BY_IRIS -> "Native structure " + key
+ " is replaced by an Iris placement in this pack and locates through that explicit replacement.";
case INVALID_REGISTRY_KEY -> "Native structure registry key is invalid: " + key;
};
}
}
@@ -41,8 +41,14 @@ public class PlacedStructurePiece {
private final int maxZ;
public boolean intersects(PlacedStructurePiece o) {
return minX < o.maxX && maxX > o.minX
&& minY < o.maxY && maxY > o.minY
&& minZ < o.maxZ && maxZ > o.minZ;
long maxExclusiveX = (long) maxX + 1L;
long maxExclusiveY = (long) maxY + 1L;
long maxExclusiveZ = (long) maxZ + 1L;
long otherMaxExclusiveX = (long) o.maxX + 1L;
long otherMaxExclusiveY = (long) o.maxY + 1L;
long otherMaxExclusiveZ = (long) o.maxZ + 1L;
return minX < otherMaxExclusiveX && maxExclusiveX > o.minX
&& minY < otherMaxExclusiveY && maxExclusiveY > o.minY
&& minZ < otherMaxExclusiveZ && maxExclusiveZ > o.minZ;
}
}
@@ -51,6 +51,7 @@ public class SeedManager {
private final long post;
private final long bodies;
private final long mode;
private final long loot;
@Setter(AccessLevel.NONE)
private long fullMixedSeed;
@@ -75,6 +76,7 @@ public class SeedManager {
post = of("post");
bodies = of("bodies");
mode = of("mode");
loot = of("loot");
}
private long of(String name) {
@@ -19,6 +19,12 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.structure.JigsawPoolSelection;
import art.arcane.iris.engine.framework.structure.StructureGraphCatalog;
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;
@@ -26,110 +32,219 @@ 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.IrisObjectRotation;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisStructure;
import art.arcane.iris.engine.object.JigsawJoint;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.volmlib.util.math.RNG;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Objects;
public final class StructureAssembler {
private static final int HARD_PIECE_CAP = 512;
private static final int MAX_DEPTH = 30;
private static final int MAX_SIZE_CHUNKS = 32;
private static final int[] NO_ROTATION = {0};
private static final int[] Y_DEGREES = {0, 90, 180, 270};
private final IrisData data;
private final StructureGraphResolver resolver;
private final IrisStructure structure;
private final int originX;
private final int originY;
private final int originZ;
private final int radius;
public StructureAssembler(IrisData data, IrisStructure structure, int originX, int originY, int originZ) {
this.data = data;
this.structure = structure;
this.originX = originX;
this.originY = originY;
this.originZ = originZ;
this.radius = Math.max(1, structure.getMaxSizeChunks()) * 16;
private StructureAssembler(AssemblyOptions options) {
this.resolver = options.resolver();
this.structure = options.structure();
this.originX = options.origin().getX();
this.originY = options.origin().getY();
this.originZ = options.origin().getZ();
this.radius = this.structure.getMaxSizeChunks() >= 1
&& this.structure.getMaxSizeChunks() <= MAX_SIZE_CHUNKS
? this.structure.getMaxSizeChunks() * 16 : 0;
}
public static StructureAssembler forData(IrisData data, IrisStructure structure, IrisPosition origin) {
IrisData activeData = Objects.requireNonNull(data, "Structure assembly data must not be null");
IrisStructure activeStructure = Objects.requireNonNull(
structure, "Structure assembly structure must not be null");
return forCompilation(StructureGraphCatalog.compile(activeData, activeStructure), origin);
}
public static StructureAssembler forResolver(StructureGraphResolver resolver, IrisStructure structure,
IrisPosition origin) {
StructureGraphResolver activeResolver = Objects.requireNonNull(
resolver, "Structure assembly resolver must not be null");
IrisStructure activeStructure = Objects.requireNonNull(
structure, "Structure assembly structure must not be null");
return forCompilation(StructureGraphCompiler.compile(activeStructure, activeResolver), origin);
}
public static StructureAssembler forCompilation(StructureGraphCompilation compilation, IrisPosition origin) {
StructureGraphCompilation activeCompilation = Objects.requireNonNull(
compilation, "Structure graph compilation must not be null");
if (activeCompilation.hasErrors()) {
throw invalidGraph(activeCompilation);
}
return new StructureAssembler(new AssemblyOptions(
StructureGraphResolver.forCompiledGraph(activeCompilation.getGraph()),
activeCompilation.getGraph().getStructure(), origin));
}
public KList<PlacedStructurePiece> assemble(RNG rng) {
IrisJigsawPool startPool = IrisData.loadAnyJigsawPool(structure.getStartPool(), data);
if (startPool == null || startPool.getPieces().isEmpty()) {
IrisLogging.warn("Structure " + structure.getLoadKey() + " has no resolvable start pool '" + structure.getStartPool() + "'");
return null;
Objects.requireNonNull(rng, "Structure assembly RNG must not be null");
if (radius == 0) {
throw new IllegalStateException("Structure '" + structure.getLoadKey()
+ "' has maxSizeChunks outside 1.." + MAX_SIZE_CHUNKS);
}
if (structure.getMaxDepth() < 1 || structure.getMaxDepth() > MAX_DEPTH) {
throw new IllegalStateException("Structure '" + structure.getLoadKey()
+ "' has maxDepth outside 1.." + MAX_DEPTH);
}
IrisJigsawPool startPool = resolver.loadPool(structure.getStartPool());
if (startPool == null || startPool.getPieces() == null || startPool.getPieces().isEmpty()) {
throw new IllegalStateException("Structure '" + structure.getLoadKey()
+ "' has no resolvable start pool '" + structure.getStartPool() + "'");
}
KList<PlacedStructurePiece> placed = new KList<>();
Deque<OpenConnector> open = new ArrayDeque<>();
IrisJigsawPiece startPiece = pickPiece(startPool, rng);
if (startPiece == null) {
return null;
IrisJigsawPieceEntry startEntry = weightedPick(startPool, rng);
if (startEntry == null) {
throw new IllegalStateException("Structure '" + structure.getLoadKey()
+ "' start pool has no positively weighted entry");
}
IrisObject startObject = IrisData.loadAnyObject(startPiece.getObject(), data);
if (startEntry.isEmpty()) {
return placed;
}
IrisJigsawPiece startPiece = resolver.loadPiece(startEntry.getPiece());
if (startPiece == null) {
throw new IllegalStateException("Structure '" + structure.getLoadKey()
+ "' start pool references missing piece '" + startEntry.getPiece() + "'");
}
IrisObject startObject = resolver.loadObject(startPiece.getObject());
if (startObject == null) {
IrisLogging.warn("Jigsaw piece references missing object '" + startPiece.getObject() + "'");
return null;
throw new IllegalStateException("Structure '" + structure.getLoadKey()
+ "' start piece references missing object '" + startPiece.getObject() + "'");
}
if (startPiece.getConnectors() == null) {
throw new IllegalStateException("Structure '" + structure.getLoadKey()
+ "' start piece has no connector list");
}
int startRotY = startPiece.isRotatable() ? Y_DEGREES[rng.i(0, 3)] : 0;
int startRotY = startPiece.isRotatable() ? Y_DEGREES[rng.i(Y_DEGREES.length)] : 0;
IrisObjectRotation startRot = IrisObjectRotation.of(0, startRotY, 0);
PlacedStructurePiece start = build(startPiece, startObject, originX, originY, originZ, startRot);
placed.add(start);
enqueueConnectors(open, start, startObject, 1, null);
enqueueConnectors(open, start, startObject, 0, null);
while (!open.isEmpty() && placed.size() < HARD_PIECE_CAP) {
OpenConnector c = open.poll();
String poolName = c.pool;
int depth = c.depth;
IrisJigsawPool pool = IrisData.loadAnyJigsawPool(poolName, data);
if (pool == null) {
if (depth > structure.getMaxDepth()) {
continue;
}
if (depth > structure.getMaxDepth()) {
if (pool.getFallback() == null || pool.getFallback().isEmpty()) {
continue;
}
pool = IrisData.loadAnyJigsawPool(pool.getFallback(), data);
if (pool == null || pool.getPieces().isEmpty()) {
continue;
}
IrisJigsawPool pool = resolver.loadPool(c.pool);
if (pool == null) {
throw new IllegalStateException("Structure '" + structure.getLoadKey()
+ "' references missing connector pool '" + c.pool + "'");
}
attachOne(open, placed, pool, c, depth, rng);
if (!attachOne(open, placed, pool, c, depth, rng)) {
return null;
}
}
if (!open.isEmpty()) {
throw assemblyFailure("exceeded the hard piece cap of " + HARD_PIECE_CAP
+ " with " + open.size() + " connector(s) unresolved");
}
return placed;
}
private void attachOne(Deque<OpenConnector> open, KList<PlacedStructurePiece> placed, IrisJigsawPool pool, OpenConnector c, int depth, RNG rng) {
for (String pieceName : weightedOrder(pool, rng)) {
IrisJigsawPiece piece = IrisData.loadAnyJigsawPiece(pieceName, data);
if (piece == null) {
continue;
private boolean attachOne(Deque<OpenConnector> open, KList<PlacedStructurePiece> placed,
IrisJigsawPool pool, OpenConnector c, int depth, RNG rng) {
if (pool.getPieces() == null) {
throw assemblyFailure("connector pool '" + c.pool + "' has no piece list");
}
if (pool.getPieces().isEmpty()) {
return true;
}
if (depth < structure.getMaxDepth() && attachFromPool(open, placed, pool, c, depth, rng)) {
return true;
}
String fallbackKey = JigsawPoolSelection.directFallbackKey(pool);
if (fallbackKey.isEmpty()) {
return depth >= structure.getMaxDepth();
}
IrisJigsawPool fallback = resolver.loadPool(fallbackKey);
if (fallback == null) {
throw assemblyFailure("references missing authored fallback pool '" + fallbackKey + "'");
}
if (fallback.getPieces() == null) {
throw assemblyFailure("authored fallback pool '" + fallbackKey + "' has no piece list");
}
if (fallback.getPieces().isEmpty()) {
return true;
}
if (attachFromPool(open, placed, fallback, c, depth, rng)) {
return true;
}
return depth >= structure.getMaxDepth();
}
private boolean attachFromPool(Deque<OpenConnector> open, KList<PlacedStructurePiece> placed, IrisJigsawPool pool,
OpenConnector c, int depth, RNG rng) {
for (IrisJigsawPieceEntry entry : weightedOrder(pool, rng)) {
if (entry.isEmpty()) {
return true;
}
IrisObject object = IrisData.loadAnyObject(piece.getObject(), data);
String pieceName = entry.getPiece();
if (pieceName == null || pieceName.isBlank()) {
throw assemblyFailure("pool contains a weighted entry without a piece key");
}
IrisJigsawPiece piece = resolver.loadPiece(pieceName);
if (piece == null) {
throw assemblyFailure("pool references missing piece '" + pieceName + "'");
}
if (piece.getObject() == null || piece.getObject().isBlank()) {
throw assemblyFailure("piece '" + pieceName + "' has no object key");
}
IrisObject object = resolver.loadObject(piece.getObject());
if (object == null) {
continue;
throw assemblyFailure("piece '" + pieceName + "' references missing object '"
+ piece.getObject() + "'");
}
if (piece.getConnectors() == null) {
throw assemblyFailure("piece '" + pieceName + "' has no connector list");
}
for (IrisJigsawConnector cb : piece.getConnectors()) {
requireConnector(cb, pieceName);
if (c.targetName == null) {
throw assemblyFailure("open connector from pool '" + c.pool + "' has no target name");
}
if (!cb.getName().equals(c.targetName)) {
continue;
}
IrisDirection needed = c.facing.reverse();
for (int yDeg : rotationCandidates(cb.getJoint(), rng)) {
int[] rotations = piece.isRotatable() ? rotationCandidates(c.joint, rng) : NO_ROTATION;
for (int yDeg : rotations) {
IrisObjectRotation rot = IrisObjectRotation.of(0, yDeg, 0);
IrisDirection rotatedFace = rot.rotate(cb.getDirection());
if (rotatedFace != needed) {
continue;
}
if (c.joint == JigsawJoint.ALIGNED && rot.rotate(cb.getTop()) != c.top) {
continue;
}
IrisPosition center = centerOf(object);
IrisPosition cr = new IrisPosition(
@@ -150,43 +265,64 @@ public final class StructureAssembler {
}
placed.add(candidate);
enqueueConnectors(open, candidate, object, depth + 1, cb);
return;
if (depth < structure.getMaxDepth()) {
enqueueConnectors(open, candidate, object, depth + 1, cb);
}
return true;
}
}
}
return false;
}
private void enqueueConnectors(Deque<OpenConnector> open, PlacedStructurePiece p, IrisObject object, int depth, IrisJigsawConnector skip) {
if (p.getPiece().getConnectors() == null) {
throw assemblyFailure("placed piece for object '" + object.getLoadKey() + "' has no connector list");
}
IrisPosition center = centerOf(object);
for (IrisJigsawConnector con : p.getPiece().getConnectors()) {
if (con == skip) {
continue;
}
requireConnector(con, object.getLoadKey());
if (con.getPool() == null || con.getPool().isBlank()) {
continue;
}
IrisPosition cr = new IrisPosition(
con.getPosition().getX() - center.getX(),
con.getPosition().getY() - center.getY(),
con.getPosition().getZ() - center.getZ());
IrisPosition rcr = p.getRotation().rotate(cr, 0, 0, 0);
IrisDirection facing = p.getRotation().rotate(con.getDirection());
IrisDirection top = p.getRotation().rotate(con.getTop());
open.add(new OpenConnector(
p.getX() + rcr.getX(),
p.getY() + rcr.getY(),
p.getZ() + rcr.getZ(),
facing, con.getPool(), con.getName(), con.getTargetName(), con.getJoint(), depth));
facing, top, con.getPool(), con.getName(), con.getTargetName(), con.getJoint(), depth));
}
}
private PlacedStructurePiece build(IrisJigsawPiece piece, IrisObject object, int x, int y, int z, IrisObjectRotation rot) {
IrisPosition rotated = rot.rotate(new IrisPosition(object.getW(), object.getH(), object.getD()), 0, 0, 0);
int rw = Math.abs(rotated.getX());
int rh = Math.abs(rotated.getY());
int rd = Math.abs(rotated.getZ());
int hx = rw / 2;
int hy = rh / 2;
int hz = rd / 2;
int width = Math.max(1, object.getW());
int height = Math.max(1, object.getH());
int depth = Math.max(1, object.getD());
int localMinX = -(width / 2);
int localMinY = -(height / 2);
int localMinZ = -(depth / 2);
int localMaxX = localMinX + width - 1;
int localMaxY = localMinY + height - 1;
int localMaxZ = localMinZ + depth - 1;
IrisPosition rotatedMin = rot.rotate(new IrisPosition(localMinX, localMinY, localMinZ), 0, 0, 0);
IrisPosition rotatedMax = rot.rotate(new IrisPosition(localMaxX, localMaxY, localMaxZ), 0, 0, 0);
int minX = Math.min(rotatedMin.getX(), rotatedMax.getX());
int minY = Math.min(rotatedMin.getY(), rotatedMax.getY());
int minZ = Math.min(rotatedMin.getZ(), rotatedMax.getZ());
int maxX = Math.max(rotatedMin.getX(), rotatedMax.getX());
int maxY = Math.max(rotatedMin.getY(), rotatedMax.getY());
int maxZ = Math.max(rotatedMin.getZ(), rotatedMax.getZ());
return new PlacedStructurePiece(piece, object, x, y, z, rot,
x - hx, y - hy, z - hz, x + hx, y + hy, z + hz);
x + minX, y + minY, z + minZ, x + maxX, y + maxY, z + maxZ);
}
private boolean withinRadius(PlacedStructurePiece p) {
@@ -209,7 +345,7 @@ public final class StructureAssembler {
}
int[] shuffled = {0, 90, 180, 270};
for (int i = shuffled.length - 1; i > 0; i--) {
int j = rng.i(0, i);
int j = rng.i(i + 1);
int t = shuffled[i];
shuffled[i] = shuffled[j];
shuffled[j] = t;
@@ -221,52 +357,99 @@ public final class StructureAssembler {
return new IrisPosition(object.getW() / 2, object.getH() / 2, object.getD() / 2);
}
private IrisJigsawPiece pickPiece(IrisJigsawPool pool, RNG rng) {
String name = weightedPick(pool, rng);
return name == null ? null : IrisData.loadAnyJigsawPiece(name, data);
}
private String weightedPick(IrisJigsawPool pool, RNG rng) {
int total = 0;
private IrisJigsawPieceEntry weightedPick(IrisJigsawPool pool, RNG rng) {
if (pool.getPieces() == null) {
throw assemblyFailure("pool has no piece list");
}
long total = 0L;
for (IrisJigsawPieceEntry e : pool.getPieces()) {
total += Math.max(1, e.getWeight());
if (e == null || e.getWeight() <= 0) {
throw assemblyFailure("pool contains a null or non-positive weighted entry");
}
total += e.getWeight();
}
if (total <= 0) {
return null;
}
int t = rng.i(0, total - 1);
long t = rng.nextLong(total);
for (IrisJigsawPieceEntry e : pool.getPieces()) {
t -= Math.max(1, e.getWeight());
t -= e.getWeight();
if (t < 0) {
return e.getPiece();
return e;
}
}
return pool.getPieces().getFirst().getPiece();
return null;
}
private KList<String> weightedOrder(IrisJigsawPool pool, RNG rng) {
KList<IrisJigsawPieceEntry> remaining = new KList<>(pool.getPieces());
KList<String> order = new KList<>();
while (!remaining.isEmpty()) {
int total = 0;
for (IrisJigsawPieceEntry e : remaining) {
total += Math.max(1, e.getWeight());
private KList<IrisJigsawPieceEntry> weightedOrder(IrisJigsawPool pool, RNG rng) {
KList<IrisJigsawPieceEntry> remaining = new KList<>();
if (pool.getPieces() != null) {
for (IrisJigsawPieceEntry entry : pool.getPieces()) {
if (entry == null || entry.getWeight() <= 0) {
throw assemblyFailure("pool contains a null or non-positive weighted entry");
}
remaining.add(entry);
}
int t = rng.i(0, total - 1);
}
KList<IrisJigsawPieceEntry> order = new KList<>();
while (!remaining.isEmpty()) {
long total = 0L;
for (IrisJigsawPieceEntry e : remaining) {
total += e.getWeight();
}
long t = rng.nextLong(total);
int idx = 0;
for (int i = 0; i < remaining.size(); i++) {
t -= Math.max(1, remaining.get(i).getWeight());
t -= remaining.get(i).getWeight();
if (t < 0) {
idx = i;
break;
}
}
order.add(remaining.remove(idx).getPiece());
order.add(remaining.remove(idx));
}
return order;
}
private record OpenConnector(int wx, int wy, int wz, IrisDirection facing, String pool, String name,
private void requireConnector(IrisJigsawConnector connector, String pieceKey) {
if (connector == null || connector.getPosition() == null || connector.getDirection() == null
|| connector.getTop() == null || connector.getName() == null
|| connector.getTargetName() == null || connector.getJoint() == null) {
throw assemblyFailure("piece '" + pieceKey + "' contains a malformed connector");
}
}
private IllegalStateException assemblyFailure(String detail) {
return new IllegalStateException("Structure '" + structure.getLoadKey() + "' assembly failed: " + detail);
}
private static IllegalStateException invalidGraph(StructureGraphCompilation compilation) {
String structureKey = compilation.getGraph().getStructureKey();
StringBuilder detail = new StringBuilder();
for (StructureGraphDiagnostic diagnostic : compilation.getDiagnostics()) {
if (diagnostic.severity() != StructureGraphDiagnostic.Severity.ERROR) {
continue;
}
if (!detail.isEmpty()) {
detail.append("; ");
}
detail.append(diagnostic.message());
}
return new IllegalStateException("Structure '" + structureKey
+ "' assembly rejected its invalid graph: " + detail);
}
private record OpenConnector(int wx, int wy, int wz, IrisDirection facing, IrisDirection top,
String pool, String name,
String targetName, JigsawJoint joint, int depth) {
}
private record AssemblyOptions(StructureGraphResolver resolver, IrisStructure structure,
IrisPosition origin) {
private AssemblyOptions {
Objects.requireNonNull(resolver, "Structure assembly resolver must not be null");
Objects.requireNonNull(structure, "Structure assembly structure must not be null");
Objects.requireNonNull(origin, "Structure assembly origin must not be null");
}
}
}
@@ -27,16 +27,17 @@ public final class StructurePlacementGrid {
private StructurePlacementGrid() {
}
public static boolean startsInChunk(IrisStructurePlacement placement, int cx, int cz, long seed, int placementOrdinal) {
public static boolean startsInChunk(IrisStructurePlacement placement, int cx, int cz, long seed) {
return switch (placement.getDistribution()) {
case RANDOM_SPREAD -> randomSpreadStart(cx, cz, placement.getSpacing(), placement.getSeparation(), placement.getSalt(), seed);
case DENSITY -> densityStart(placement, cx, cz, seed, placementOrdinal);
case RANDOM_SPREAD -> randomSpreadStart(cx, cz, placement.getSpacing(), placement.getSeparation(),
placementSalt(placement), seed);
case DENSITY -> densityStart(placement, cx, cz, seed);
case CONCENTRIC_RINGS -> concentricRingsStart(cx, cz, placement, seed);
};
}
public static RNG placementRng(IrisStructurePlacement placement, int cx, int cz, long seed, int placementOrdinal) {
return new RNG(placementSeed(placement, cx, cz, seed, placementOrdinal));
public static RNG placementRng(IrisStructurePlacement placement, int cx, int cz, long seed) {
return new RNG(placementSeed(placement, cx, cz, seed));
}
public static boolean randomSpreadStart(int cx, int cz, int spacing, int separation, int salt, long seed) {
@@ -74,7 +75,7 @@ public final class StructurePlacementGrid {
int slots = Math.min(spread, count - firstIndex);
int slot = placementIndex - firstIndex;
double slotAngle = (2.0 * Math.PI) / slots;
double offset = unitDouble(mix(seed, ring, count, placement.getSalt())) * slotAngle;
double offset = unitDouble(mix(seed, ring, count, placementSalt(placement))) * slotAngle;
double angle = offset + (slot * slotAngle);
long configuredRadius = (long) ring * distance;
if (configuredRadius > Integer.MAX_VALUE) {
@@ -106,7 +107,7 @@ public final class StructurePlacementGrid {
continue;
}
double slotAngle = (2.0 * Math.PI) / slots;
double offset = unitDouble(mix(seed, ring, count, placement.getSalt())) * slotAngle;
double offset = unitDouble(mix(seed, ring, count, placementSalt(placement))) * slotAngle;
double slotPosition = (Math.atan2(cz, cx) - offset) / slotAngle;
int nearestSlot = Math.floorMod((int) Math.round(slotPosition), slots);
for (int delta = -1; delta <= 1; delta++) {
@@ -120,7 +121,7 @@ public final class StructurePlacementGrid {
return false;
}
private static boolean densityStart(IrisStructurePlacement placement, int cx, int cz, long seed, int placementOrdinal) {
private static boolean densityStart(IrisStructurePlacement placement, int cx, int cz, long seed) {
double density = placement.getDensity();
if (density <= 0.0) {
return false;
@@ -128,24 +129,81 @@ public final class StructurePlacementGrid {
if (density >= 1.0) {
return true;
}
RNG rng = new RNG(placementSeed(placement, cx, cz, seed, placementOrdinal) ^ DENSITY_SIGNATURE);
RNG rng = new RNG(placementSeed(placement, cx, cz, seed) ^ DENSITY_SIGNATURE);
return rng.chance(density);
}
private static long placementSeed(IrisStructurePlacement placement, int cx, int cz, long seed, int placementOrdinal) {
private static long placementSeed(IrisStructurePlacement placement, int cx, int cz, long seed) {
long signature = 1469598103934665603L;
signature = (signature ^ placement.getDistribution().ordinal()) * 1099511628211L;
signature = (signature ^ placement.getSalt()) * 1099511628211L;
signature = (signature ^ placementOrdinal) * 1099511628211L;
for (String key : placement.getStructures()) {
if (key == null) {
continue;
}
for (int i = 0; i < key.length(); i++) {
signature = (signature ^ key.charAt(i)) * 1099511628211L;
signature = appendLong(signature, placement.getDistribution().ordinal());
signature = appendLong(signature, placement.getSalt());
String placementId = placement.getPlacementId();
if (placementId != null && !placementId.isBlank()) {
signature = appendLong(signature, 1L);
signature = appendSignature(signature, placementId.trim());
} else {
signature = appendLong(signature, 0L);
signature = appendLong(signature, placement.getSpacing());
signature = appendLong(signature, placement.getSeparation());
signature = appendLong(signature, Double.doubleToLongBits(placement.getDensity()));
signature = appendLong(signature, placement.getRingCount());
signature = appendLong(signature, placement.getRingDistance());
signature = appendLong(signature, placement.getRingSpread());
signature = appendLong(signature, placement.getMinHeight());
signature = appendLong(signature, placement.getMaxHeight());
signature = appendLong(signature, placement.isUnderground() ? 1L : 0L);
signature = appendLong(signature, placement.isUnderwater() ? 1L : 0L);
signature = appendLong(signature, placement.getStructures().size());
for (String key : placement.getStructures()) {
signature = appendSignature(signature, key == null ? "" : key);
}
}
return mix(seed ^ signature, cx, cz, placement.getSalt() ^ placementOrdinal);
int identitySalt = (int) (signature ^ (signature >>> 32));
return mix(seed ^ signature, cx, cz, placementSalt(placement) ^ identitySalt);
}
private static long appendSignature(long signature, String value) {
long result = appendLong(signature, value.length());
for (int index = 0; index < value.length(); index++) {
result = (result ^ value.charAt(index)) * 1099511628211L;
}
return result;
}
private static long appendLong(long signature, long value) {
long result = signature;
long remaining = value;
for (int index = 0; index < Long.BYTES; index++) {
result = (result ^ (remaining & 0xffL)) * 1099511628211L;
remaining >>>= Byte.SIZE;
}
return result;
}
static int placementSalt(IrisStructurePlacement placement) {
String placementId = placement.getPlacementId();
long identity;
if (placementId != null && !placementId.isBlank()) {
identity = appendSignature(1469598103934665603L, placementId.trim());
} else {
identity = 1469598103934665603L;
identity = appendLong(identity, placement.getDistribution().ordinal());
identity = appendLong(identity, placement.getSpacing());
identity = appendLong(identity, placement.getSeparation());
identity = appendLong(identity, Double.doubleToLongBits(placement.getDensity()));
identity = appendLong(identity, placement.getRingCount());
identity = appendLong(identity, placement.getRingDistance());
identity = appendLong(identity, placement.getRingSpread());
identity = appendLong(identity, placement.getMinHeight());
identity = appendLong(identity, placement.getMaxHeight());
identity = appendLong(identity, placement.isUnderground() ? 1L : 0L);
identity = appendLong(identity, placement.isUnderwater() ? 1L : 0L);
identity = appendLong(identity, placement.getStructures().size());
for (String key : placement.getStructures()) {
identity = appendSignature(identity, key == null ? "" : key);
}
}
return placement.getSalt() ^ (int) (identity ^ (identity >>> 32));
}
private static double unitDouble(long value) {
@@ -26,6 +26,8 @@ import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
/**
@@ -44,20 +46,16 @@ public final class StructureReachability {
}
public static Set<String> reachableKeys(Engine engine) {
if (engine == null) {
return Collections.emptySet();
Engine activeEngine = Objects.requireNonNull(engine, "Structure reachability requires an engine");
if (activeEngine.getData() == null) {
throw new IllegalStateException("Structure reachability requires a bound pack data loader");
}
if (engine.getData() == null) {
return Collections.emptySet();
}
return REACHABLE_CACHE.get(engine, ignored -> build(engine));
return REACHABLE_CACHE.get(activeEngine, ignored -> build(activeEngine));
}
public static boolean isReachable(Engine engine, String structureKey) {
if (structureKey == null || structureKey.isEmpty()) {
return false;
}
return reachableKeys(engine).contains(structureKey.toLowerCase());
String normalizedStructureKey = normalizeStructureKey(structureKey);
return reachableKeys(engine).contains(normalizedStructureKey);
}
public static void invalidate(Engine engine) {
@@ -69,14 +67,18 @@ public final class StructureReachability {
private static Set<String> build(Engine engine) {
IrisWorld world = engine.getWorld();
if (world == null || world.platformWorld() == null) {
return Collections.emptySet();
if (world == null) {
throw new IllegalStateException("Structure reachability requires a bound Iris world");
}
if (world.platformWorld() == null) {
throw new IllegalStateException("Structure reachability requires a bound platform world");
}
Set<String> reachable = new LinkedHashSet<>();
for (String key : IrisPlatforms.get().structureHooks().reachableStructureKeys(world.platformWorld())) {
if (key != null && !key.isEmpty()) {
reachable.add(key.toLowerCase());
if (key == null || key.isBlank()) {
throw new IllegalStateException("Platform structure reachability returned a blank registry key");
}
reachable.add(key.toLowerCase(Locale.ROOT));
}
return Collections.unmodifiableSet(reachable);
}
@@ -88,24 +90,38 @@ public final class StructureReachability {
*/
public static KList<String> missingBiomeKeys(Engine engine, String structureKey) {
KList<String> missing = new KList<>();
if (engine == null || structureKey == null || structureKey.isEmpty()) {
return missing;
Engine activeEngine = Objects.requireNonNull(engine, "Structure biome diagnostics require an engine");
String normalizedStructureKey = normalizeStructureKey(structureKey);
IrisWorld world = activeEngine.getWorld();
if (world == null) {
throw new IllegalStateException("Structure biome diagnostics require a bound Iris world");
}
IrisWorld world = engine.getWorld();
if (world == null || world.platformWorld() == null) {
return missing;
if (world.platformWorld() == null) {
throw new IllegalStateException("Structure biome diagnostics require a bound platform world");
}
Set<String> possible = new LinkedHashSet<>();
for (String key : IrisPlatforms.get().structureHooks().possibleBiomeKeys(world.platformWorld())) {
if (key != null) {
possible.add(key.toLowerCase());
if (key == null || key.isBlank()) {
throw new IllegalStateException("Platform structure biome query returned a blank possible-biome key");
}
possible.add(key.toLowerCase(Locale.ROOT));
}
for (String biomeKey : IrisPlatforms.get().structureHooks().structureBiomeKeys(structureKey)) {
if (biomeKey != null && !possible.contains(biomeKey.toLowerCase())) {
for (String biomeKey : IrisPlatforms.get().structureHooks().structureBiomeKeys(normalizedStructureKey)) {
if (biomeKey == null || biomeKey.isBlank()) {
throw new IllegalStateException("Platform structure biome query returned a blank required-biome key for "
+ normalizedStructureKey);
}
if (!possible.contains(biomeKey.toLowerCase(Locale.ROOT))) {
missing.add(biomeKey);
}
}
return missing;
}
private static String normalizeStructureKey(String structureKey) {
if (structureKey == null || structureKey.isBlank()) {
throw new IllegalArgumentException("Registered structure key must not be blank");
}
return structureKey.trim().toLowerCase(Locale.ROOT);
}
}
@@ -4,11 +4,12 @@ import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.LootResolver;
import art.arcane.iris.core.events.IrisLootEvent;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.InventorySlotType;
import art.arcane.iris.engine.object.IrisLootTable;
@@ -16,6 +17,7 @@ import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.platform.bukkit.BukkitBlockState;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.volmlib.util.math.RNG;
@@ -37,7 +39,7 @@ public class WorldObjectPlacer implements IObjectPlacer {
private final EngineMantle mantle;
public WorldObjectPlacer(World world) {
var a = IrisToolbelt.access(world);
PlatformChunkGenerator a = IrisToolbelt.access(world);
if (a == null || a.getEngine() == null) throw new IllegalStateException(world.getName() + " is not an Iris World!");
this.world = world;
this.engine = a.getEngine();
@@ -62,34 +64,37 @@ public class WorldObjectPlacer implements IObjectPlacer {
Block block = world.getBlockAt(x, worldY, z);
if (block.getType() == Material.BEDROCK) return;
InventorySlotType slot = null;
if (BukkitBlockResolution.isStorageChest(d)) {
slot = InventorySlotType.STORAGE;
}
boolean storageChest = BukkitBlockResolution.isStorageChest(d);
if (d instanceof IrisCustomData data) {
block.setBlockData(data.getBase(), false);
IrisLogging.warn("Tried to place custom block at " + x + ", " + y + ", " + z + " which is not supported!");
} else block.setBlockData(d, false);
if (slot != null) {
RNG rx = new RNG(Cache.key(x, z));
KList<IrisLootTable> tables = EngineBukkitOps.getLootTables(engine, rx, block);
if (storageChest && !J.runRegion(world, x >> 4, z >> 4, () -> fillLoot(block), 1)) {
IrisLogging.warn("Failed to schedule loot resolution at " + x + ", " + worldY + ", " + z);
}
}
try {
Bukkit.getPluginManager().callEvent(new IrisLootEvent(engine, block, slot, tables));
private void fillLoot(Block block) {
if (!BukkitBlockResolution.isStorageChest(block.getBlockData()) || !EngineBukkitOps.isCanonicalContainer(block)) {
return;
}
int x = block.getX();
int y = block.getY();
int z = block.getZ();
RNG rng = LootResolver.containerRng(engine.getSeedManager().getLoot(), x, y, z);
KList<IrisLootTable> tables = EngineBukkitOps.getLootTables(engine, rng, block);
if (!tables.isEmpty()){
IrisLogging.debug("IrisLootEvent has been accessed");
}
if (tables.isEmpty())
return;
InventoryHolder m = (InventoryHolder) block.getState();
EngineBukkitOps.addItems(engine, false, m.getInventory(), rx, tables, slot, world, x, y, z, 15);
} catch (Throwable e) {
IrisLogging.reportError(e);
try {
Bukkit.getPluginManager().callEvent(new IrisLootEvent(engine, block, InventorySlotType.STORAGE, tables));
if (tables.isEmpty()) {
return;
}
InventoryHolder holder = (InventoryHolder) block.getState();
EngineBukkitOps.addItems(engine, false, holder.getInventory(), tables, InventorySlotType.STORAGE, world, x, y, z);
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
@@ -137,10 +142,17 @@ public class WorldObjectPlacer implements IObjectPlacer {
@Override
public <T> void setData(int xx, int yy, int zz, T data) {
if (data == null || yy < 0 || yy >= engine.getHeight()) {
return;
}
mantle.getMantle().set(xx, yy, zz, data);
}
@Override
public <T> T getData(int xx, int yy, int zz, Class<T> t) {
return null;
if (yy < 0 || yy >= engine.getHeight()) {
return null;
}
return mantle.getMantle().get(xx, yy, zz, t);
}
}
@@ -0,0 +1,101 @@
package art.arcane.iris.engine.framework.structure;
import art.arcane.iris.engine.object.IrisJigsawPiece;
import art.arcane.iris.engine.object.IrisJigsawPool;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisStructure;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
public final class CompiledStructureGraph {
private final IrisStructure structure;
private final Map<String, IrisJigsawPool> pools;
private final Map<String, IrisJigsawPiece> pieces;
private final Map<String, IrisObject> objects;
private final Set<String> reachablePools;
private final Set<String> reachablePieces;
private CompiledStructureGraph(Builder builder) {
structure = builder.structure;
pools = Collections.unmodifiableMap(new LinkedHashMap<>(builder.pools));
pieces = Collections.unmodifiableMap(new LinkedHashMap<>(builder.pieces));
objects = Collections.unmodifiableMap(new LinkedHashMap<>(builder.objects));
reachablePools = Collections.unmodifiableSet(new LinkedHashSet<>(builder.reachablePools));
reachablePieces = Collections.unmodifiableSet(new LinkedHashSet<>(builder.reachablePieces));
}
static Builder builder(IrisStructure structure) {
return new Builder(structure);
}
public IrisStructure getStructure() {
return structure;
}
public String getStructureKey() {
String key = structure.getLoadKey();
return key == null || key.isBlank() ? "<unloaded>" : key;
}
public Map<String, IrisJigsawPool> getPools() {
return pools;
}
public Map<String, IrisJigsawPiece> getPieces() {
return pieces;
}
public Map<String, IrisObject> getObjects() {
return objects;
}
public Set<String> getReachablePools() {
return reachablePools;
}
public Set<String> getReachablePieces() {
return reachablePieces;
}
static final class Builder {
private final IrisStructure structure;
private final Map<String, IrisJigsawPool> pools = new LinkedHashMap<>();
private final Map<String, IrisJigsawPiece> pieces = new LinkedHashMap<>();
private final Map<String, IrisObject> objects = new LinkedHashMap<>();
private final Set<String> reachablePools = new LinkedHashSet<>();
private final Set<String> reachablePieces = new LinkedHashSet<>();
private Builder(IrisStructure structure) {
this.structure = Objects.requireNonNull(structure);
}
Map<String, IrisJigsawPool> pools() {
return pools;
}
Map<String, IrisJigsawPiece> pieces() {
return pieces;
}
Map<String, IrisObject> objects() {
return objects;
}
Set<String> reachablePools() {
return reachablePools;
}
Set<String> reachablePieces() {
return reachablePieces;
}
CompiledStructureGraph build() {
return new CompiledStructureGraph(this);
}
}
}
@@ -0,0 +1,386 @@
package art.arcane.iris.engine.framework.structure;
import art.arcane.iris.engine.object.IrisObject;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;
public final class IrisObjectFrameReader {
private static final String HEADER = "Iris V2 IOB;";
private static final int MAX_FILE_BYTES = 64 * 1024 * 1024;
private static final int MAX_TILE_COUNT = 65_536;
private static final int MAX_CANDIDATE_OFFSETS = 1_024;
private static final long MAX_TILE_PARSE_STATES = 262_144L;
private static final int MAX_LEGACY_SIGN_COLOR_INDEX = 15;
private IrisObjectFrameReader() {
}
public static IrisObject readBounds(InputStream stream, String resourceName) throws IOException {
String activeResourceName = resourceName == null || resourceName.isBlank()
? "<unknown>" : resourceName;
try {
byte[] content = readLimitedContent(Objects.requireNonNull(stream), activeResourceName);
return readBounds(content, activeResourceName);
} catch (IOException e) {
if (e.getMessage() != null && e.getMessage().startsWith("Malformed Iris object resource ")) {
throw e;
}
throw malformed(activeResourceName, e.getMessage() == null
? e.getClass().getSimpleName() : e.getMessage());
} catch (RuntimeException e) {
throw malformed(activeResourceName, e.getMessage() == null
? e.getClass().getSimpleName() : e.getMessage());
}
}
private static IrisObject readBounds(byte[] content, String resourceName) throws IOException {
ByteArrayInputStream bytes = new ByteArrayInputStream(content);
DataInputStream input = new DataInputStream(bytes);
int width = input.readInt();
int height = input.readInt();
int depth = input.readInt();
long volume = requireDimensions(resourceName, width, height, depth);
if (!HEADER.equals(input.readUTF())) {
throw malformed(resourceName, "invalid header");
}
int paletteSize = input.readShort();
if (paletteSize < 0) {
throw malformed(resourceName, "palette exceeds the signed IOB limit");
}
for (int index = 0; index < paletteSize; index++) {
if (input.readUTF().isBlank()) {
throw malformed(resourceName, "palette entry " + index + " is blank");
}
}
int blockCount = input.readInt();
if (blockCount < 0 || blockCount > volume || blockCount > 0 && paletteSize == 0) {
throw malformed(resourceName, "invalid block count " + blockCount);
}
for (int index = 0; index < blockCount; index++) {
int x = input.readShort();
int y = input.readShort();
int z = input.readShort();
int paletteIndex = input.readShort();
requirePosition(resourceName, "block", index, x, y, z, width, height, depth);
if (paletteIndex < 0 || paletteIndex >= paletteSize) {
throw malformed(resourceName, "block " + index
+ " references invalid palette index " + paletteIndex);
}
}
int tileCount = input.readInt();
if (tileCount < 0 || tileCount > volume) {
throw malformed(resourceName, "invalid tile count " + tileCount);
}
if (tileCount > MAX_TILE_COUNT) {
throw malformed(resourceName, "tile count " + tileCount
+ " exceeds limit " + MAX_TILE_COUNT);
}
int tileStart = content.length - bytes.available();
requireCompleteTileFraming(content, tileStart, tileCount, width, height, depth, resourceName);
return new IrisObject(width, height, depth);
}
private static void requireCompleteTileFraming(
byte[] content,
int tileStart,
int tileCount,
int width,
int height,
int depth,
String resourceName
) throws IOException {
Set<Integer> offsets = Set.of(tileStart);
long parseStates = 0L;
for (int tileIndex = 0; tileIndex < tileCount; tileIndex++) {
Set<Integer> nextOffsets = new LinkedHashSet<>();
TileFailureTracker failures = new TileFailureTracker();
for (int offset : offsets) {
parseStates++;
requireParseStateBudget(resourceName, tileIndex, parseStates);
TilePrefix prefix = readTilePrefix(
content, offset, width, height, depth, tileIndex, failures);
if (prefix == null) {
continue;
}
int modernEnd = readModernTileEnd(
content, prefix.payloadOffset(), tileIndex, failures);
if (modernEnd >= 0) {
addCandidateOffset(resourceName, tileIndex, nextOffsets, modernEnd);
}
for (int legacyEnd : readLegacyTileEnds(
content, prefix.payloadOffset(), tileIndex, failures)) {
addCandidateOffset(resourceName, tileIndex, nextOffsets, legacyEnd);
}
}
if (nextOffsets.isEmpty()) {
throw malformed(resourceName, failures.detailOr(
"tile " + tileIndex + " is truncated or malformed"));
}
offsets = nextOffsets;
}
if (!offsets.contains(content.length)) {
throw malformed(resourceName, "tile records leave trailing or incomplete data");
}
}
private static TilePrefix readTilePrefix(
byte[] content,
int offset,
int width,
int height,
int depth,
int tileIndex,
TileFailureTracker failures
) {
try {
ByteArrayInputStream bytes = slice(content, offset);
DataInputStream input = new DataInputStream(bytes);
int x = input.readShort();
int y = input.readShort();
int z = input.readShort();
if (!withinSignedObjectAxis(x, width) || !withinSignedObjectAxis(y, height)
|| !withinSignedObjectAxis(z, depth)) {
failures.record("tile " + tileIndex + " position " + x + "," + y + "," + z
+ " is outside object bounds", 20);
return null;
}
return new TilePrefix(content.length - bytes.available());
} catch (IOException | RuntimeException e) {
return null;
}
}
private static int readModernTileEnd(
byte[] content,
int offset,
int tileIndex,
TileFailureTracker failures
) {
try {
ByteArrayInputStream bytes = slice(content, offset);
DataInputStream input = new DataInputStream(bytes);
String material = input.readUTF();
if (material.isBlank()) {
failures.record("tile " + tileIndex + " modern material is blank", 40);
return -1;
}
JsonElement properties = JsonParser.parseString(input.readUTF());
if (!properties.isJsonObject()) {
failures.record("tile " + tileIndex
+ " modern properties are not a JSON object", 30);
return -1;
}
return content.length - bytes.available();
} catch (IOException | RuntimeException e) {
return -1;
}
}
private static Set<Integer> readLegacyTileEnds(
byte[] content,
int offset,
int tileIndex,
TileFailureTracker failures
) {
try {
ByteArrayInputStream bytes = slice(content, offset);
DataInputStream input = new DataInputStream(bytes);
int id = input.readShort();
return switch (id) {
case 0 -> singletonEnd(content, readLegacySign(input, bytes, tileIndex, failures));
case 1 -> readLegacySpawnerEnds(content, content.length - bytes.available());
case 2 -> readLegacyBannerEnds(content, input, bytes);
case 3 -> singletonEnd(content, readLegacyLootable(input, bytes));
default -> Set.of();
};
} catch (IOException | RuntimeException e) {
return Set.of();
}
}
private static int readLegacySign(
DataInputStream input,
ByteArrayInputStream bytes,
int tileIndex,
TileFailureTracker failures
) throws IOException {
input.readUTF();
input.readUTF();
input.readUTF();
input.readUTF();
int colorIndex = input.readByte();
if (colorIndex < 0 || colorIndex > MAX_LEGACY_SIGN_COLOR_INDEX) {
failures.record("tile " + tileIndex + " legacy sign color index " + colorIndex
+ " is outside 0.." + MAX_LEGACY_SIGN_COLOR_INDEX, 50);
throw new IOException("Invalid legacy sign color index");
}
return bytes.available();
}
private static Set<Integer> readLegacySpawnerEnds(byte[] content, int payloadOffset) {
Set<Integer> ends = new LinkedHashSet<>();
try {
ByteArrayInputStream keyedBytes = slice(content, payloadOffset);
new DataInputStream(keyedBytes).readUTF();
ends.add(content.length - keyedBytes.available());
} catch (IOException | RuntimeException ignored) {
}
if (payloadOffset <= content.length - Short.BYTES) {
ends.add(payloadOffset + Short.BYTES);
}
return Set.copyOf(ends);
}
private static Set<Integer> readLegacyBannerEnds(
byte[] content,
DataInputStream input,
ByteArrayInputStream bytes
) throws IOException {
input.readUnsignedByte();
int patternCount = input.readUnsignedByte();
int patternsOffset = content.length - bytes.available();
Set<Integer> ends = new LinkedHashSet<>();
try {
ByteArrayInputStream keyedBytes = slice(content, patternsOffset);
DataInputStream keyedInput = new DataInputStream(keyedBytes);
for (int index = 0; index < patternCount; index++) {
keyedInput.readUnsignedByte();
keyedInput.readUTF();
}
ends.add(content.length - keyedBytes.available());
} catch (IOException | RuntimeException ignored) {
}
long legacyEnd = (long) patternsOffset + (long) patternCount * 2L;
if (legacyEnd <= content.length) {
ends.add((int) legacyEnd);
}
return Set.copyOf(ends);
}
private static int readLegacyLootable(DataInputStream input, ByteArrayInputStream bytes) throws IOException {
input.readUTF();
input.readUTF();
input.readLong();
return bytes.available();
}
private static Set<Integer> singletonEnd(byte[] content, int remaining) {
return Set.of(content.length - remaining);
}
private static byte[] readLimitedContent(InputStream stream, String resourceName) throws IOException {
return readLimitedContent(stream, resourceName, MAX_FILE_BYTES);
}
static byte[] readLimitedContent(
InputStream stream,
String resourceName,
int maximumBytes
) throws IOException {
byte[] content = stream.readNBytes(maximumBytes + 1);
if (content.length > maximumBytes) {
throw malformed(resourceName, "file exceeds " + maximumBytes + "-byte limit");
}
return content;
}
private static void addCandidateOffset(
String resourceName,
int tileIndex,
Set<Integer> offsets,
int offset
) throws IOException {
offsets.add(offset);
if (offsets.size() > MAX_CANDIDATE_OFFSETS) {
throw malformed(resourceName, "tile " + tileIndex + " exceeds candidate-offset limit "
+ MAX_CANDIDATE_OFFSETS);
}
}
private static void requireParseStateBudget(
String resourceName,
int tileIndex,
long parseStates
) throws IOException {
if (parseStates > MAX_TILE_PARSE_STATES) {
throw malformed(resourceName, "tile " + tileIndex + " exceeds parse-state limit "
+ MAX_TILE_PARSE_STATES);
}
}
private static ByteArrayInputStream slice(byte[] content, int offset) {
if (offset < 0 || offset > content.length) {
throw new IllegalArgumentException("Invalid IOB frame offset " + offset);
}
return new ByteArrayInputStream(content, offset, content.length - offset);
}
private static long requireDimensions(String resourceName, int width, int height, int depth) throws IOException {
if (width < 1 || height < 1 || depth < 1) {
throw malformed(resourceName, "invalid dimensions " + width + "x" + height + "x" + depth);
}
try {
return Math.multiplyExact(Math.multiplyExact((long) width, height), depth);
} catch (ArithmeticException e) {
throw malformed(resourceName, "dimensions overflow the IOB volume limit");
}
}
private static void requirePosition(
String resourceName,
String type,
int index,
int x,
int y,
int z,
int width,
int height,
int depth
) throws IOException {
if (!withinSignedObjectAxis(x, width) || !withinSignedObjectAxis(y, height)
|| !withinSignedObjectAxis(z, depth)) {
throw malformed(resourceName, type + " " + index + " position "
+ x + "," + y + "," + z + " is outside object bounds");
}
}
private static boolean withinSignedObjectAxis(int coordinate, int size) {
int center = size / 2;
return coordinate >= -center && coordinate < size - center;
}
private static IOException malformed(String resourceName, String detail) {
return new IOException("Malformed Iris object resource " + resourceName + ": " + detail);
}
private record TilePrefix(int payloadOffset) {
}
private static final class TileFailureTracker {
private String detail;
private int priority = Integer.MIN_VALUE;
private void record(String candidateDetail, int candidatePriority) {
if (candidatePriority <= priority) {
return;
}
detail = candidateDetail;
priority = candidatePriority;
}
private String detailOr(String fallback) {
return detail == null ? fallback : detail;
}
}
}
@@ -0,0 +1,39 @@
package art.arcane.iris.engine.framework.structure;
import art.arcane.iris.engine.object.IrisJigsawPool;
import java.util.List;
import java.util.Objects;
public final class JigsawPoolSelection {
private JigsawPoolSelection() {
}
public static String directFallbackKey(IrisJigsawPool pool) {
Objects.requireNonNull(pool);
return normalize(pool.getFallback());
}
public static List<String> candidatePoolKeys(
String primaryKey,
IrisJigsawPool primaryPool,
boolean includePrimary
) {
Objects.requireNonNull(primaryPool);
String normalizedPrimary = normalize(primaryKey);
String directFallback = directFallbackKey(primaryPool);
if (!includePrimary) {
return directFallback.isEmpty() ? List.of() : List.of(directFallback);
}
if (normalizedPrimary.isEmpty()) {
return List.of();
}
return directFallback.isEmpty()
? List.of(normalizedPrimary)
: List.of(normalizedPrimary, directFallback);
}
private static String normalize(String value) {
return value == null ? "" : value.trim();
}
}
@@ -0,0 +1,24 @@
package art.arcane.iris.engine.framework.structure;
import java.util.List;
public record StructureGraphAssemblySample(long seed, Outcome outcome) {
public record Outcome(List<String> pieceKeys, int unresolvedConnectorCount, boolean pieceCapReached,
boolean intentionalEmpty) {
public Outcome {
pieceKeys = List.copyOf(pieceKeys);
}
public Outcome(List<String> pieceKeys, int unresolvedConnectorCount, boolean pieceCapReached) {
this(pieceKeys, unresolvedConnectorCount, pieceCapReached, false);
}
public boolean isViable() {
return (intentionalEmpty || !pieceKeys.isEmpty()) && !pieceCapReached;
}
public boolean isComplete() {
return isViable() && unresolvedConnectorCount == 0;
}
}
}
@@ -0,0 +1,102 @@
package art.arcane.iris.engine.framework.structure;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.PlacedStructurePiece;
import art.arcane.iris.engine.framework.StructureAssembler;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisStructure;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public final class StructureGraphCatalog {
private static final Cache<IrisData, ConcurrentMap<String, StructureGraphCompilation>> CACHE =
Caffeine.newBuilder().weakKeys().build();
private static final Cache<IrisData, ConcurrentMap<String, Boolean>> RUNTIME_OUTPUT =
Caffeine.newBuilder().weakKeys().build();
private static final List<Long> RUNTIME_SAMPLE_SEEDS = List.of(
0L, 1L, 2L, 3L, 5L, 8L, 13L, 21L, 34L, 55L, 89L, 144L, 233L, 377L, 610L, 987L);
private StructureGraphCatalog() {
}
public static StructureGraphCompilation compile(IrisData data, IrisStructure structure) {
IrisData activeData = Objects.requireNonNull(data);
IrisStructure activeStructure = Objects.requireNonNull(structure);
String structureKey = activeStructure.getLoadKey();
if (structureKey == null || structureKey.isBlank()) {
return compileAndReport(activeData, activeStructure);
}
ConcurrentMap<String, StructureGraphCompilation> catalog = CACHE.get(
activeData, ignored -> new ConcurrentHashMap<>());
return catalog.computeIfAbsent(structureKey, ignored -> compileAndReport(activeData, activeStructure));
}
public static void invalidate(IrisData data) {
if (data != null) {
CACHE.invalidate(data);
RUNTIME_OUTPUT.invalidate(data);
}
}
public static boolean guaranteesRuntimeOutput(IrisData data, IrisStructure structure) {
IrisData activeData = Objects.requireNonNull(data);
IrisStructure activeStructure = Objects.requireNonNull(structure);
if (!compile(activeData, activeStructure).guaranteesAssemblyOutput()) {
return false;
}
String structureKey = activeStructure.getLoadKey();
if (structureKey == null || structureKey.isBlank()) {
return sampleRuntimeAssembly(activeData, activeStructure);
}
ConcurrentMap<String, Boolean> catalog = RUNTIME_OUTPUT.get(
activeData, ignored -> new ConcurrentHashMap<>());
return catalog.computeIfAbsent(
structureKey, ignored -> sampleRuntimeAssembly(activeData, activeStructure));
}
private static StructureGraphCompilation compileAndReport(IrisData data, IrisStructure structure) {
StructureGraphCompilation compilation = StructureGraphCompiler.compile(
structure, StructureGraphResolver.forData(data));
String structureKey = structure.getLoadKey() == null || structure.getLoadKey().isBlank()
? "<unkeyed>" : structure.getLoadKey();
for (StructureGraphDiagnostic diagnostic : compilation.getDiagnostics()) {
String message = "[StructureGraph:" + structureKey + "] " + diagnostic.message();
if (diagnostic.severity() == StructureGraphDiagnostic.Severity.ERROR) {
IrisLogging.error(message);
} else {
IrisLogging.warn(message);
}
}
if (!compilation.isAssemblyViable()) {
IrisLogging.error("[StructureGraph:" + structureKey
+ "] Graph rejected because deterministic assembly did not complete.");
}
return compilation;
}
private static boolean sampleRuntimeAssembly(IrisData data, IrisStructure structure) {
for (long seed : RUNTIME_SAMPLE_SEEDS) {
try {
StructureAssembler assembler = StructureAssembler.forData(
data, structure, new IrisPosition(0, 64, 0));
KList<PlacedStructurePiece> pieces = assembler.assemble(new RNG(seed));
if (pieces == null || pieces.isEmpty()) {
return false;
}
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
return false;
}
}
return true;
}
}
@@ -0,0 +1,183 @@
package art.arcane.iris.engine.framework.structure;
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 java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public final class StructureGraphCompilation {
private final CompiledStructureGraph graph;
private final List<StructureGraphDiagnostic> diagnostics;
private final List<StructureGraphAssemblySample> assemblySamples;
StructureGraphCompilation(Builder builder) {
graph = Objects.requireNonNull(builder.graph);
diagnostics = Collections.unmodifiableList(new ArrayList<>(builder.diagnostics));
assemblySamples = Collections.unmodifiableList(new ArrayList<>(builder.assemblySamples));
}
static Builder builder(CompiledStructureGraph graph) {
return new Builder(graph);
}
public CompiledStructureGraph getGraph() {
return graph;
}
public List<StructureGraphDiagnostic> getDiagnostics() {
return diagnostics;
}
public List<StructureGraphAssemblySample> getAssemblySamples() {
return assemblySamples;
}
public boolean hasErrors() {
for (StructureGraphDiagnostic diagnostic : diagnostics) {
if (diagnostic.severity() == StructureGraphDiagnostic.Severity.ERROR) {
return true;
}
}
return false;
}
public boolean isAssemblyViable() {
if (hasErrors() || assemblySamples.isEmpty()) {
return false;
}
for (StructureGraphAssemblySample sample : assemblySamples) {
StructureGraphAssemblySample.Outcome outcome = sample.outcome();
if (!outcome.isComplete() && !isGeometryBoundedCandidate(outcome)) {
return false;
}
}
return true;
}
public boolean guaranteesAssemblyOutput() {
if (!isAssemblyViable()) {
return false;
}
String startPoolKey = graph.getStructure().getStartPool();
if (startPoolKey == null || startPoolKey.isBlank()) {
return false;
}
IrisJigsawPool startPool = graph.getPools().get(startPoolKey.trim());
if (startPool == null || startPool.getPieces() == null || startPool.getPieces().isEmpty()) {
return false;
}
for (IrisJigsawPieceEntry entry : startPool.getPieces()) {
if (entry == null || entry.getWeight() <= 0 || entry.isEmpty()) {
return false;
}
IrisJigsawPiece piece = graph.getPieces().get(normalize(entry.getPiece()));
if (piece == null) {
return false;
}
IrisObject object = graph.getObjects().get(normalize(piece.getObject()));
if (object == null || !fitsStartRadius(object)) {
return false;
}
}
return reachableBranchesCanTerminate();
}
private boolean reachableBranchesCanTerminate() {
for (String pieceKey : graph.getReachablePieces()) {
IrisJigsawPiece piece = graph.getPieces().get(pieceKey);
if (piece == null || piece.getConnectors() == null) {
return false;
}
for (IrisJigsawConnector connector : piece.getConnectors()) {
if (connector == null) {
return false;
}
IrisJigsawPool pool = graph.getPools().get(normalize(connector.getPool()));
if (!canTerminateWithoutPlacement(pool)) {
return false;
}
}
}
return true;
}
private boolean canTerminateWithoutPlacement(IrisJigsawPool pool) {
if (hasEmptyChoice(pool)) {
return true;
}
if (pool == null) {
return false;
}
String fallbackKey = JigsawPoolSelection.directFallbackKey(pool);
return !fallbackKey.isEmpty() && hasEmptyChoice(graph.getPools().get(fallbackKey));
}
private boolean hasEmptyChoice(IrisJigsawPool pool) {
if (pool == null || pool.getPieces() == null) {
return false;
}
if (pool.getPieces().isEmpty()) {
return true;
}
for (IrisJigsawPieceEntry entry : pool.getPieces()) {
if (entry != null && entry.getWeight() > 0 && entry.isEmpty()) {
return true;
}
}
return false;
}
private boolean fitsStartRadius(IrisObject object) {
long radius = (long) graph.getStructure().getMaxSizeChunks() * 16L;
return axisFitsRadius(object.getW(), radius) && axisFitsRadius(object.getD(), radius);
}
private boolean axisFitsRadius(int size, long radius) {
if (size < 1 || radius < 1L) {
return false;
}
long minimum = -(size / 2L);
long maximum = minimum + size - 1L;
return minimum >= -radius && maximum <= radius;
}
private String normalize(String value) {
return value == null ? "" : value.trim();
}
private boolean isGeometryBoundedCandidate(StructureGraphAssemblySample.Outcome outcome) {
return outcome.pieceCapReached()
&& !outcome.pieceKeys().isEmpty()
&& outcome.unresolvedConnectorCount() == 0;
}
static final class Builder {
private final CompiledStructureGraph graph;
private final List<StructureGraphDiagnostic> diagnostics = new ArrayList<>();
private final List<StructureGraphAssemblySample> assemblySamples = new ArrayList<>();
private Builder(CompiledStructureGraph graph) {
this.graph = Objects.requireNonNull(graph);
}
Builder diagnostics(List<StructureGraphDiagnostic> values) {
diagnostics.addAll(values);
return this;
}
Builder assemblySamples(List<StructureGraphAssemblySample> values) {
assemblySamples.addAll(values);
return this;
}
StructureGraphCompilation build() {
return new StructureGraphCompilation(this);
}
}
}
@@ -0,0 +1,871 @@
package art.arcane.iris.engine.framework.structure;
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 java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
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.SplittableRandom;
public final class StructureGraphCompiler {
private static final int ASSEMBLY_PIECE_CAP = 512;
private static final int MAX_DEPTH = 30;
private static final int MAX_SIZE_CHUNKS = 32;
private static final int[] NO_ROTATION = {0};
private static final int[] Y_ROTATIONS = {0, 90, 180, 270};
private static final List<Long> ASSEMBLY_SEEDS = List.of(0L, 1L, 2L, 3L, 5L, 8L, 13L, 21L);
private StructureGraphCompiler() {
}
public static StructureGraphCompilation compile(IrisStructure structure, StructureGraphResolver resolver) {
CompilationState state = new CompilationState(
Objects.requireNonNull(structure), Objects.requireNonNull(resolver));
state.loadClosure();
state.detectFallbackCycles();
state.analyzeReachability();
List<StructureGraphAssemblySample> samples = state.sampleAssemblies();
state.reportPieceCapSamples(samples);
CompiledStructureGraph graph = state.buildGraph();
return StructureGraphCompilation.builder(graph)
.diagnostics(state.diagnostics())
.assemblySamples(samples)
.build();
}
private static final class CompilationState {
private final IrisStructure structure;
private final StructureGraphResolver resolver;
private final CompiledStructureGraph.Builder graph;
private final Deque<String> pendingPools = new ArrayDeque<>();
private final Map<String, List<PoolReference>> poolReferences = new LinkedHashMap<>();
private final Map<String, List<PoolEntryReference>> poolEntries = new LinkedHashMap<>();
private final Map<String, List<ConnectorReference>> pieceConnectors = new LinkedHashMap<>();
private final Map<String, String> pieceObjects = new LinkedHashMap<>();
private final Set<String> processedPools = new HashSet<>();
private final Set<String> missingPools = new HashSet<>();
private final Set<String> attemptedPieces = new HashSet<>();
private final Set<String> missingPieces = new HashSet<>();
private final Set<String> attemptedObjects = new HashSet<>();
private final Set<String> missingObjects = new HashSet<>();
private final Set<String> reachableEntries = new LinkedHashSet<>();
private final Set<PieceReachState> reachablePieceStates = new LinkedHashSet<>();
private final Map<String, List<OrientedConnector>> activeConnectors = new LinkedHashMap<>();
private final List<StructureGraphDiagnostic> diagnostics = new ArrayList<>();
private final Set<String> diagnosticKeys = new HashSet<>();
private CompilationState(IrisStructure structure, StructureGraphResolver resolver) {
this.structure = structure;
this.resolver = resolver;
this.graph = CompiledStructureGraph.builder(structure);
}
private void loadClosure() {
validateStructureLimits();
String startPool = normalize(structure.getStartPool());
if (startPool.isEmpty()) {
addDiagnostic(StructureGraphDiagnostic.Code.MISSING_START_POOL,
"Structure '" + structureKey() + "' does not declare a start pool.",
"structure:start-pool:empty");
return;
}
requestPool(startPool, new PoolReference(
"Structure '" + structureKey() + "' references missing start pool '" + startPool + "'.",
StructureGraphDiagnostic.Code.MISSING_START_POOL));
while (!pendingPools.isEmpty()) {
processPool(pendingPools.removeFirst());
}
}
private void validateStructureLimits() {
if (structure.getMaxDepth() < 1 || structure.getMaxDepth() > MAX_DEPTH) {
addDiagnostic(StructureGraphDiagnostic.Code.INVALID_MAX_DEPTH,
"Structure '" + structureKey() + "' has maxDepth " + structure.getMaxDepth()
+ "; it must be between 1 and " + MAX_DEPTH + ".",
"structure:max-depth");
}
if (structure.getMaxSizeChunks() < 1 || structure.getMaxSizeChunks() > MAX_SIZE_CHUNKS) {
addDiagnostic(StructureGraphDiagnostic.Code.INVALID_MAX_SIZE,
"Structure '" + structureKey() + "' has maxSizeChunks " + structure.getMaxSizeChunks()
+ "; it must be between 1 and " + MAX_SIZE_CHUNKS + ".",
"structure:max-size");
}
}
private void requestPool(String rawKey, PoolReference reference) {
String key = normalize(rawKey);
if (key.isEmpty()) {
return;
}
poolReferences.computeIfAbsent(key, ignored -> new ArrayList<>()).add(reference);
if (missingPools.contains(key)) {
addDiagnostic(reference.code(), reference.message(), "missing-pool:" + key + ":" + reference.message());
return;
}
if (!processedPools.contains(key) && !pendingPools.contains(key)) {
pendingPools.addLast(key);
}
}
private void processPool(String key) {
if (!processedPools.add(key)) {
return;
}
IrisJigsawPool pool = resolver.loadPool(key);
if (pool == null) {
missingPools.add(key);
for (PoolReference reference : poolReferences.getOrDefault(key, List.of())) {
addDiagnostic(reference.code(), reference.message(),
"missing-pool:" + key + ":" + reference.message());
}
return;
}
graph.pools().put(key, pool);
List<PoolEntryReference> entries = new ArrayList<>();
poolEntries.put(key, entries);
KList<IrisJigsawPieceEntry> configuredEntries = pool.getPieces();
if (configuredEntries == null || configuredEntries.isEmpty()) {
if (key.equals(normalize(structure.getStartPool()))) {
addDiagnostic(StructureGraphDiagnostic.Code.EMPTY_START_POOL,
"Jigsaw pool '" + key + "' is empty.",
"empty-pool:" + key);
}
} else {
for (int index = 0; index < configuredEntries.size(); index++) {
scanPoolEntry(key, index, configuredEntries.get(index), entries);
}
}
String fallback = JigsawPoolSelection.directFallbackKey(pool);
if (!fallback.isEmpty()) {
requestPool(fallback, new PoolReference(
"Jigsaw pool '" + key + "' references missing fallback pool '" + fallback + "'.",
StructureGraphDiagnostic.Code.MISSING_POOL));
}
}
private void scanPoolEntry(String poolKey, int index, IrisJigsawPieceEntry entry,
List<PoolEntryReference> entries) {
if (entry == null) {
addDiagnostic(StructureGraphDiagnostic.Code.INVALID_POOL_ENTRY,
"Jigsaw pool '" + poolKey + "' pieces[" + index + "] is null.",
"pool-entry:null:" + poolKey + ":" + index);
return;
}
String pieceKey = normalize(entry.getPiece());
PoolEntryReference entryReference = new PoolEntryReference(
poolKey, index, pieceKey, entry.getWeight(), entry.isEmpty());
entries.add(entryReference);
if (entry.getWeight() <= 0) {
addDiagnostic(StructureGraphDiagnostic.Code.INVALID_WEIGHT,
"Jigsaw pool '" + poolKey + "' pieces[" + index + "] has non-positive weight "
+ entry.getWeight() + ".",
"pool-entry:weight:" + poolKey + ":" + index);
}
if (entry.isEmpty()) {
if (!pieceKey.isEmpty()) {
addDiagnostic(StructureGraphDiagnostic.Code.INVALID_POOL_ENTRY,
"Jigsaw pool '" + poolKey + "' pieces[" + index
+ "] declares both empty=true and a piece.",
"pool-entry:ambiguous-empty:" + poolKey + ":" + index);
}
return;
}
if (pieceKey.isEmpty()) {
addDiagnostic(StructureGraphDiagnostic.Code.MISSING_PIECE,
"Jigsaw pool '" + poolKey + "' pieces[" + index + "] does not declare a piece.",
"pool-entry:piece-empty:" + poolKey + ":" + index);
return;
}
loadPiece(pieceKey, "Jigsaw pool '" + poolKey + "' pieces[" + index
+ "] references missing piece '" + pieceKey + "'.");
}
private void loadPiece(String key, String missingMessage) {
if (missingPieces.contains(key)) {
addDiagnostic(StructureGraphDiagnostic.Code.MISSING_PIECE, missingMessage,
"missing-piece:" + key + ":" + missingMessage);
return;
}
if (!attemptedPieces.add(key)) {
return;
}
IrisJigsawPiece piece = resolver.loadPiece(key);
if (piece == null) {
missingPieces.add(key);
addDiagnostic(StructureGraphDiagnostic.Code.MISSING_PIECE, missingMessage,
"missing-piece:" + key + ":" + missingMessage);
return;
}
graph.pieces().put(key, piece);
IrisObject object = loadPieceObject(key, piece);
scanConnectors(key, piece, object);
}
private IrisObject loadPieceObject(String pieceKey, IrisJigsawPiece piece) {
String objectKey = normalize(piece.getObject());
pieceObjects.put(pieceKey, objectKey);
if (objectKey.isEmpty()) {
addDiagnostic(StructureGraphDiagnostic.Code.MISSING_OBJECT,
"Jigsaw piece '" + pieceKey + "' does not declare an object.",
"piece-object:empty:" + pieceKey);
return null;
}
if (missingObjects.contains(objectKey)) {
addDiagnostic(StructureGraphDiagnostic.Code.MISSING_OBJECT,
"Jigsaw piece '" + pieceKey + "' references missing object '" + objectKey + "'.",
"missing-object:" + objectKey + ":" + pieceKey);
return null;
}
if (!attemptedObjects.add(objectKey)) {
return graph.objects().get(objectKey);
}
IrisObject object = resolver.loadObject(objectKey);
if (object == null) {
missingObjects.add(objectKey);
addDiagnostic(StructureGraphDiagnostic.Code.MISSING_OBJECT,
"Jigsaw piece '" + pieceKey + "' references missing object '" + objectKey + "'.",
"missing-object:" + objectKey + ":" + pieceKey);
return null;
}
graph.objects().put(objectKey, object);
if (object.getW() < 1 || object.getH() < 1 || object.getD() < 1) {
addDiagnostic(StructureGraphDiagnostic.Code.INVALID_OBJECT_BOUNDS,
"Object '" + objectKey + "' used by jigsaw piece '" + pieceKey
+ "' has invalid dimensions " + object.getW() + "x" + object.getH() + "x"
+ object.getD() + ".",
"object-bounds:" + objectKey);
}
return object;
}
private void scanConnectors(String pieceKey, IrisJigsawPiece piece, IrisObject object) {
List<ConnectorReference> references = new ArrayList<>();
pieceConnectors.put(pieceKey, references);
KList<IrisJigsawConnector> connectors = piece.getConnectors();
if (connectors == null) {
addDiagnostic(StructureGraphDiagnostic.Code.INVALID_CONNECTOR,
"Jigsaw piece '" + pieceKey + "' declares a null connector list.",
"connector-list:null:" + pieceKey);
return;
}
for (int index = 0; index < connectors.size(); index++) {
IrisJigsawConnector connector = connectors.get(index);
if (connector == null) {
addDiagnostic(StructureGraphDiagnostic.Code.INVALID_CONNECTOR,
"Jigsaw piece '" + pieceKey + "' connectors[" + index + "] is null.",
"connector:null:" + pieceKey + ":" + index);
continue;
}
boolean validDirection = connector.getDirection() != null;
if (!validDirection) {
addDiagnostic(StructureGraphDiagnostic.Code.INVALID_CONNECTOR_DIRECTION,
"Jigsaw piece '" + pieceKey + "' connectors[" + index
+ "] does not declare a direction.",
"connector:direction:" + pieceKey + ":" + index);
}
boolean validTop = connector.getTop() != null;
if (!validTop) {
addDiagnostic(StructureGraphDiagnostic.Code.INVALID_CONNECTOR_DIRECTION,
"Jigsaw piece '" + pieceKey + "' connectors[" + index
+ "] does not declare a top direction.",
"connector:top:" + pieceKey + ":" + index);
}
boolean validJoint = connector.getJoint() != null;
if (!validJoint) {
addDiagnostic(StructureGraphDiagnostic.Code.INVALID_CONNECTOR,
"Jigsaw piece '" + pieceKey + "' connectors[" + index
+ "] does not declare a joint type.",
"connector:joint:" + pieceKey + ":" + index);
}
boolean validPosition = isValidPosition(connector.getPosition(), object);
if (connector.getPosition() == null || object != null && !validPosition) {
addDiagnostic(StructureGraphDiagnostic.Code.INVALID_CONNECTOR_POSITION,
invalidPositionMessage(pieceKey, index, connector.getPosition(), object),
"connector:position:" + pieceKey + ":" + index);
}
boolean validNames = connector.getName() != null && connector.getTargetName() != null;
if (!validNames) {
addDiagnostic(StructureGraphDiagnostic.Code.INVALID_CONNECTOR,
"Jigsaw piece '" + pieceKey + "' connectors[" + index
+ "] must declare non-null name and targetName values.",
"connector:names:" + pieceKey + ":" + index);
}
String poolKey = normalize(connector.getPool());
if (poolKey.isEmpty()) {
addDiagnostic(StructureGraphDiagnostic.Code.MISSING_CONNECTOR_POOL,
"Jigsaw piece '" + pieceKey + "' connectors[" + index
+ "] does not declare a target pool.",
"connector:pool-empty:" + pieceKey + ":" + index);
} else {
requestPool(poolKey, new PoolReference(
"Jigsaw piece '" + pieceKey + "' connectors[" + index
+ "] references missing pool '" + poolKey + "'.",
StructureGraphDiagnostic.Code.MISSING_POOL));
}
references.add(new ConnectorReference(
pieceKey, index, connector, validPosition, validDirection, validTop, validJoint, validNames));
}
}
private String invalidPositionMessage(String pieceKey, int index, IrisPosition position, IrisObject object) {
if (position == null) {
return "Jigsaw piece '" + pieceKey + "' connectors[" + index
+ "] does not declare a position.";
}
if (object == null) {
return "Jigsaw piece '" + pieceKey + "' connectors[" + index
+ "] position " + position + " cannot be checked because its object is missing.";
}
return "Jigsaw piece '" + pieceKey + "' connectors[" + index + "] position " + position
+ " is outside object bounds 0.." + (object.getW() - 1) + ", 0.." + (object.getH() - 1)
+ ", 0.." + (object.getD() - 1) + ".";
}
private boolean isValidPosition(IrisPosition position, IrisObject object) {
if (position == null || object == null) {
return false;
}
return position.getX() >= 0 && position.getX() < object.getW()
&& position.getY() >= 0 && position.getY() < object.getH()
&& position.getZ() >= 0 && position.getZ() < object.getD();
}
private void detectFallbackCycles() {
Map<String, Integer> states = new HashMap<>();
for (String poolKey : graph.pools().keySet()) {
detectFallbackCycle(poolKey, states);
}
}
private void detectFallbackCycle(String startPool, Map<String, Integer> states) {
if (states.getOrDefault(startPool, 0) == 2) {
return;
}
List<String> path = new ArrayList<>();
Map<String, Integer> pathIndexes = new HashMap<>();
String poolKey = startPool;
while (!poolKey.isEmpty() && graph.pools().containsKey(poolKey)
&& states.getOrDefault(poolKey, 0) != 2) {
Integer cycleStart = pathIndexes.get(poolKey);
if (cycleStart != null) {
List<String> cycle = new ArrayList<>(path.subList(cycleStart, path.size()));
cycle.add(poolKey);
String cyclePath = String.join(" -> ", cycle);
addDiagnostic(StructureGraphDiagnostic.Code.FALLBACK_CYCLE,
"Jigsaw pool fallback cycle detected: " + cyclePath + ".",
"fallback-cycle:" + cyclePath);
break;
}
pathIndexes.put(poolKey, path.size());
path.add(poolKey);
states.put(poolKey, 1);
IrisJigsawPool pool = graph.pools().get(poolKey);
poolKey = pool == null ? "" : JigsawPoolSelection.directFallbackKey(pool);
}
for (String visitedPool : path) {
states.put(visitedPool, 2);
}
}
private void analyzeReachability() {
String startPool = normalize(structure.getStartPool());
if (!graph.pools().containsKey(startPool)) {
return;
}
graph.reachablePools().add(startPool);
Deque<PieceReachState> pendingPieces = new ArrayDeque<>();
for (PoolEntryReference entry : poolEntries.getOrDefault(startPool, List.of())) {
markReachableEntry(entry);
if (entry.empty()) {
continue;
}
IrisJigsawPiece piece = graph.pieces().get(entry.pieceKey());
if (piece == null) {
continue;
}
for (int rotation : rotationsFor(piece)) {
markReachablePieceState(new PieceReachState(entry.pieceKey(), -1, rotation), pendingPieces);
}
}
if (pendingPieces.isEmpty()) {
addDiagnostic(StructureGraphDiagnostic.Code.NO_VIABLE_START_PIECE,
"Structure '" + structureKey() + "' has no start-pool entry with a positive weight,"
+ " resolvable piece, and resolvable object.",
"structure:no-viable-start");
}
while (!pendingPieces.isEmpty()) {
PieceReachState pieceState = pendingPieces.removeFirst();
for (ConnectorReference source : pieceConnectors.getOrDefault(pieceState.pieceKey(), List.of())) {
if (source.index() == pieceState.skippedConnectorIndex() || !source.canSource()) {
continue;
}
OrientedConnector orientedSource = new OrientedConnector(source, pieceState.rotation());
activeConnectors.computeIfAbsent(source.id(), ignored -> new ArrayList<>())
.add(orientedSource);
List<String> candidatePools = candidatePools(normalize(source.connector().getPool()), true);
for (String candidatePool : candidatePools) {
graph.reachablePools().add(candidatePool);
for (PoolEntryReference entry : poolEntries.getOrDefault(candidatePool, List.of())) {
if (!entryIsViable(entry)) {
continue;
}
List<ConnectorMatch> compatible = findCompatibleConnectors(
orientedSource, entry.pieceKey());
if (!compatible.isEmpty()) {
markReachableEntry(entry);
for (ConnectorMatch target : compatible) {
markReachablePieceState(
new PieceReachState(entry.pieceKey(), target.connector().index(),
target.rotation()),
pendingPieces);
}
}
}
}
}
}
reportUnmatchedReachableConnectors();
reportUnreachableResources();
}
private void markReachableEntry(PoolEntryReference entry) {
if (!entryIsViable(entry)) {
return;
}
reachableEntries.add(entry.id());
if (!entry.empty()) {
graph.reachablePieces().add(entry.pieceKey());
}
}
private void markReachablePieceState(PieceReachState state, Deque<PieceReachState> pendingPieces) {
if (!graph.reachablePieces().contains(state.pieceKey())) {
return;
}
if (reachablePieceStates.add(state)) {
pendingPieces.addLast(state);
}
}
private boolean entryIsViable(PoolEntryReference entry) {
if (entry.weight() <= 0) {
return false;
}
if (entry.empty()) {
return entry.pieceKey().isEmpty();
}
if (entry.pieceKey().isEmpty() || !graph.pieces().containsKey(entry.pieceKey())) {
return false;
}
String objectKey = pieceObjects.getOrDefault(entry.pieceKey(), "");
return !objectKey.isEmpty() && graph.objects().containsKey(objectKey);
}
private void reportUnmatchedReachableConnectors() {
for (List<OrientedConnector> orientations : activeConnectors.values()) {
OrientedConnector representative = orientations.getFirst();
List<String> candidates = candidatePools(
normalize(representative.definition().getPool()), true);
boolean hasViableEntry = false;
boolean hasCompatibleEntry = false;
for (String candidatePool : candidates) {
for (PoolEntryReference entry : poolEntries.getOrDefault(candidatePool, List.of())) {
if (!entryIsViable(entry)) {
continue;
}
hasViableEntry = true;
if (entry.empty()) {
hasCompatibleEntry = true;
break;
}
for (OrientedConnector source : orientations) {
if (!findCompatibleConnectors(source, entry.pieceKey()).isEmpty()) {
hasCompatibleEntry = true;
break;
}
}
if (hasCompatibleEntry) {
break;
}
}
if (hasCompatibleEntry) {
break;
}
}
if (hasViableEntry && !hasCompatibleEntry) {
addDiagnostic(StructureGraphDiagnostic.Code.NO_COMPATIBLE_CONNECTOR,
"Jigsaw piece '" + representative.connector().pieceKey() + "' connectors["
+ representative.connector().index() + "] targets pool '"
+ normalize(representative.definition().getPool())
+ "', but no reachable candidate exposes the requested target name with a"
+ " compatible direction.",
"connector:no-match:" + representative.connector().pieceKey() + ":"
+ representative.connector().index());
}
}
}
private void reportUnreachableResources() {
for (String poolKey : graph.pools().keySet()) {
if (!graph.reachablePools().contains(poolKey)) {
addDiagnostic(StructureGraphDiagnostic.Code.UNREACHABLE_POOL,
"Jigsaw pool '" + poolKey + "' is in the structure closure but cannot be reached"
+ " from the start pool.",
"unreachable-pool:" + poolKey);
continue;
}
for (PoolEntryReference entry : poolEntries.getOrDefault(poolKey, List.of())) {
if (!entry.empty() && entryIsViable(entry) && !reachableEntries.contains(entry.id())) {
addDiagnostic(StructureGraphDiagnostic.Code.UNREACHABLE_PIECE,
"Jigsaw pool '" + poolKey + "' pieces[" + entry.index() + "] references piece '"
+ entry.pieceKey() + "', but no reachable connector can attach it.",
"unreachable-piece:" + entry.id());
}
}
}
}
private List<ConnectorMatch> findCompatibleConnectors(OrientedConnector source,
String candidatePieceKey) {
List<ConnectorMatch> compatible = new ArrayList<>();
IrisJigsawPiece candidatePiece = graph.pieces().get(candidatePieceKey);
if (candidatePiece == null) {
return compatible;
}
for (ConnectorReference candidate : pieceConnectors.getOrDefault(candidatePieceKey, List.of())) {
for (int rotation : rotationsFor(candidatePiece)) {
if (connectorsCompatible(source, candidate, rotation)) {
compatible.add(new ConnectorMatch(candidate, rotation));
}
}
}
return compatible;
}
private boolean connectorsCompatible(OrientedConnector source, ConnectorReference candidate,
int candidateRotation) {
if (!source.connector().canSource() || !candidate.canTarget()) {
return false;
}
IrisJigsawConnector sourceConnector = source.definition();
IrisJigsawConnector candidateConnector = candidate.connector();
if (!normalize(sourceConnector.getTargetName()).equals(normalize(candidateConnector.getName()))) {
return false;
}
IrisDirection sourceDirection = rotateDirection(
sourceConnector.getDirection(), source.rotation());
IrisDirection candidateDirection = rotateDirection(
candidateConnector.getDirection(), candidateRotation);
if (candidateDirection != sourceDirection.reverse()) {
return false;
}
if (sourceConnector.getJoint() != JigsawJoint.ALIGNED) {
return true;
}
IrisDirection sourceTop = rotateDirection(sourceConnector.getTop(), source.rotation());
IrisDirection candidateTop = rotateDirection(candidateConnector.getTop(), candidateRotation);
return candidateTop == sourceTop;
}
private List<String> candidatePools(String firstPool, boolean includeFirst) {
List<String> result = new ArrayList<>();
IrisJigsawPool pool = graph.pools().get(firstPool);
if (pool == null) {
return result;
}
for (String candidate : JigsawPoolSelection.candidatePoolKeys(firstPool, pool, includeFirst)) {
if (graph.pools().containsKey(candidate)) {
result.add(candidate);
}
}
return result;
}
private List<StructureGraphAssemblySample> sampleAssemblies() {
List<StructureGraphAssemblySample> samples = new ArrayList<>(ASSEMBLY_SEEDS.size());
for (long seed : ASSEMBLY_SEEDS) {
samples.add(sampleAssembly(seed));
}
return samples;
}
private StructureGraphAssemblySample sampleAssembly(long seed) {
SplittableRandom random = new SplittableRandom(seed);
List<String> pieces = new ArrayList<>();
Deque<SampleConnector> open = new ArrayDeque<>();
String startPool = normalize(structure.getStartPool());
PoolEntryReference start = weightedPick(viableEntries(startPool), random);
if (start == null) {
return new StructureGraphAssemblySample(seed,
new StructureGraphAssemblySample.Outcome(pieces, 1, false));
}
if (start.empty()) {
return new StructureGraphAssemblySample(seed,
new StructureGraphAssemblySample.Outcome(pieces, 0, false, true));
}
IrisJigsawPiece startPiece = graph.pieces().get(start.pieceKey());
int startRotation = startPiece != null && startPiece.isRotatable()
? Y_ROTATIONS[random.nextInt(Y_ROTATIONS.length)]
: 0;
addSamplePiece(start.pieceKey(), -1, 0, startRotation, pieces, open);
int unresolvedConnectors = 0;
while (!open.isEmpty() && pieces.size() < ASSEMBLY_PIECE_CAP) {
SampleConnector source = open.removeFirst();
AttachmentCandidate candidate = sampleCandidate(source, random);
if (candidate == null) {
if (!isIntentionalTermination(source)) {
unresolvedConnectors++;
}
continue;
}
if (candidate.entry().empty()) {
continue;
}
if (source.depth() < Math.max(1, structure.getMaxDepth())) {
addSamplePiece(candidate.entry().pieceKey(), candidate.match().connector().index(),
source.depth() + 1, candidate.match().rotation(), pieces, open);
} else {
pieces.add(candidate.entry().pieceKey());
}
}
boolean capped = !open.isEmpty();
return new StructureGraphAssemblySample(seed,
new StructureGraphAssemblySample.Outcome(pieces, unresolvedConnectors, capped));
}
private AttachmentCandidate sampleCandidate(SampleConnector source, SplittableRandom random) {
String primaryPool = normalize(source.connector().definition().getPool());
boolean withinDepth = source.depth() < Math.max(1, structure.getMaxDepth());
List<String> candidates = candidatePools(primaryPool, withinDepth);
for (String candidatePool : candidates) {
List<AttachmentCandidate> attachments = new ArrayList<>();
for (PoolEntryReference entry : viableEntries(candidatePool)) {
if (entry.empty()) {
attachments.add(new AttachmentCandidate(entry, null));
continue;
}
List<ConnectorMatch> matching = findCompatibleConnectors(
source.connector(), entry.pieceKey());
if (!matching.isEmpty()) {
attachments.add(new AttachmentCandidate(entry, matching.getFirst()));
}
}
AttachmentCandidate selected = weightedPickAttachments(attachments, random);
if (selected != null) {
return selected;
}
}
return null;
}
private boolean isIntentionalTermination(SampleConnector source) {
String poolKey = normalize(source.connector().definition().getPool());
IrisJigsawPool pool = graph.pools().get(poolKey);
if (pool == null) {
return false;
}
boolean includePrimary = source.depth() < Math.max(1, structure.getMaxDepth());
for (String candidate : candidatePools(poolKey, includePrimary)) {
IrisJigsawPool candidatePool = graph.pools().get(candidate);
if (candidatePool != null && candidatePool.getPieces() != null
&& candidatePool.getPieces().isEmpty()) {
return true;
}
}
return !includePrimary;
}
private void addSamplePiece(String pieceKey, int skippedConnector, int connectorDepth, int rotation,
List<String> pieces, Deque<SampleConnector> open) {
pieces.add(pieceKey);
for (ConnectorReference connector : pieceConnectors.getOrDefault(pieceKey, List.of())) {
if (connector.index() != skippedConnector && connector.canSource()) {
open.addLast(new SampleConnector(
new OrientedConnector(connector, rotation), connectorDepth));
}
}
}
private List<PoolEntryReference> viableEntries(String poolKey) {
List<PoolEntryReference> result = new ArrayList<>();
for (PoolEntryReference entry : poolEntries.getOrDefault(poolKey, List.of())) {
if (entryIsViable(entry)) {
result.add(entry);
}
}
return result;
}
private PoolEntryReference weightedPick(List<PoolEntryReference> entries, SplittableRandom random) {
long totalWeight = 0L;
for (PoolEntryReference entry : entries) {
totalWeight += entry.weight();
}
if (totalWeight <= 0L) {
return null;
}
long target = random.nextLong(totalWeight);
for (PoolEntryReference entry : entries) {
target -= entry.weight();
if (target < 0L) {
return entry;
}
}
return entries.getLast();
}
private AttachmentCandidate weightedPickAttachments(List<AttachmentCandidate> attachments,
SplittableRandom random) {
long totalWeight = 0L;
for (AttachmentCandidate attachment : attachments) {
totalWeight += attachment.entry().weight();
}
if (totalWeight <= 0L) {
return null;
}
long target = random.nextLong(totalWeight);
for (AttachmentCandidate attachment : attachments) {
target -= attachment.entry().weight();
if (target < 0L) {
return attachment;
}
}
return attachments.getLast();
}
private void reportPieceCapSamples(List<StructureGraphAssemblySample> samples) {
List<String> cappedSeeds = new ArrayList<>();
for (StructureGraphAssemblySample sample : samples) {
if (sample.outcome().pieceCapReached()) {
cappedSeeds.add(Long.toString(sample.seed()));
}
}
if (!cappedSeeds.isEmpty()) {
addDiagnostic(StructureGraphDiagnostic.Code.ASSEMBLY_PIECE_CAP_REACHED,
"Deterministic structural assembly reached the " + ASSEMBLY_PIECE_CAP
+ "-piece safety cap for seeds " + String.join(", ", cappedSeeds) + ".",
"assembly:piece-cap");
}
}
private CompiledStructureGraph buildGraph() {
return graph.build();
}
private List<StructureGraphDiagnostic> diagnostics() {
return diagnostics;
}
private void addDiagnostic(StructureGraphDiagnostic.Code code, String message, String deduplicationKey) {
if (diagnosticKeys.add(code.name() + ":" + deduplicationKey)) {
diagnostics.add(new StructureGraphDiagnostic(code, message));
}
}
private String structureKey() {
String key = structure.getLoadKey();
return key == null || key.isBlank() ? "<unloaded>" : key;
}
}
private static String normalize(String value) {
return value == null ? "" : value.trim();
}
private static int[] rotationsFor(IrisJigsawPiece piece) {
return piece.isRotatable() ? Y_ROTATIONS : NO_ROTATION;
}
private static IrisDirection rotateDirection(IrisDirection direction, int degrees) {
if (direction.isVertical()) {
return direction;
}
int turns = Math.floorMod(degrees, 360) / 90;
IrisDirection rotated = direction;
for (int turn = 0; turn < turns; turn++) {
rotated = switch (rotated) {
case NORTH_NEGATIVE_Z -> IrisDirection.WEST_NEGATIVE_X;
case WEST_NEGATIVE_X -> IrisDirection.SOUTH_POSITIVE_Z;
case SOUTH_POSITIVE_Z -> IrisDirection.EAST_POSITIVE_X;
case EAST_POSITIVE_X -> IrisDirection.NORTH_NEGATIVE_Z;
case UP_POSITIVE_Y, DOWN_NEGATIVE_Y -> rotated;
};
}
return rotated;
}
private record PoolReference(String message, StructureGraphDiagnostic.Code code) {
}
private record PoolEntryReference(String poolKey, int index, String pieceKey, int weight, boolean empty) {
private String id() {
return poolKey + "#" + index;
}
}
private record ConnectorReference(String pieceKey, int index, IrisJigsawConnector connector,
boolean validPosition, boolean validDirection, boolean validTop,
boolean validJoint, boolean validNames) {
private String id() {
return pieceKey + "#" + index;
}
private boolean canSource() {
return canTarget() && !normalize(connector.getPool()).isEmpty();
}
private boolean canTarget() {
return connector != null && validPosition && validDirection && validTop && validJoint && validNames;
}
}
private record PieceReachState(String pieceKey, int skippedConnectorIndex, int rotation) {
}
private record OrientedConnector(ConnectorReference connector, int rotation) {
private IrisJigsawConnector definition() {
return connector.connector();
}
}
private record ConnectorMatch(ConnectorReference connector, int rotation) {
}
private record SampleConnector(OrientedConnector connector, int depth) {
}
private record AttachmentCandidate(PoolEntryReference entry, ConnectorMatch match) {
}
}
@@ -0,0 +1,52 @@
package art.arcane.iris.engine.framework.structure;
import java.util.Objects;
public record StructureGraphDiagnostic(Code code, String message) {
public StructureGraphDiagnostic {
Objects.requireNonNull(code);
Objects.requireNonNull(message);
}
public Severity severity() {
return code.severity();
}
public enum Severity {
ERROR,
WARNING
}
public enum Code {
MISSING_START_POOL(Severity.ERROR),
EMPTY_START_POOL(Severity.ERROR),
MISSING_POOL(Severity.ERROR),
INVALID_MAX_DEPTH(Severity.ERROR),
INVALID_MAX_SIZE(Severity.ERROR),
INVALID_POOL_ENTRY(Severity.ERROR),
INVALID_WEIGHT(Severity.ERROR),
MISSING_PIECE(Severity.ERROR),
MISSING_OBJECT(Severity.ERROR),
INVALID_OBJECT_BOUNDS(Severity.ERROR),
INVALID_CONNECTOR(Severity.ERROR),
INVALID_CONNECTOR_POSITION(Severity.ERROR),
INVALID_CONNECTOR_DIRECTION(Severity.ERROR),
MISSING_CONNECTOR_POOL(Severity.ERROR),
FALLBACK_CYCLE(Severity.ERROR),
NO_VIABLE_START_PIECE(Severity.WARNING),
NO_COMPATIBLE_CONNECTOR(Severity.WARNING),
UNREACHABLE_POOL(Severity.WARNING),
UNREACHABLE_PIECE(Severity.WARNING),
ASSEMBLY_PIECE_CAP_REACHED(Severity.WARNING);
private final Severity severity;
Code(Severity severity) {
this.severity = severity;
}
public Severity severity() {
return severity;
}
}
}
@@ -0,0 +1,74 @@
package art.arcane.iris.engine.framework.structure;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.IrisJigsawPiece;
import art.arcane.iris.engine.object.IrisJigsawPool;
import art.arcane.iris.engine.object.IrisObject;
import java.util.Objects;
public interface StructureGraphResolver {
IrisJigsawPool loadPool(String key);
IrisJigsawPiece loadPiece(String key);
IrisObject loadObject(String key);
static StructureGraphResolver forData(IrisData data) {
return new IrisDataStructureGraphResolver(Objects.requireNonNull(data));
}
static StructureGraphResolver forCompiledGraph(CompiledStructureGraph graph) {
return new CompiledStructureGraphResolver(Objects.requireNonNull(graph));
}
}
final class IrisDataStructureGraphResolver implements StructureGraphResolver {
private final IrisData data;
IrisDataStructureGraphResolver(IrisData data) {
this.data = data;
}
@Override
public IrisJigsawPool loadPool(String key) {
return data.load(IrisJigsawPool.class, key, false);
}
@Override
public IrisJigsawPiece loadPiece(String key) {
return data.load(IrisJigsawPiece.class, key, false);
}
@Override
public IrisObject loadObject(String key) {
return data.load(IrisObject.class, key, false);
}
}
final class CompiledStructureGraphResolver implements StructureGraphResolver {
private final CompiledStructureGraph graph;
CompiledStructureGraphResolver(CompiledStructureGraph graph) {
this.graph = graph;
}
@Override
public IrisJigsawPool loadPool(String key) {
return graph.getPools().get(normalize(key));
}
@Override
public IrisJigsawPiece loadPiece(String key) {
return graph.getPieces().get(normalize(key));
}
@Override
public IrisObject loadObject(String key) {
return graph.getObjects().get(normalize(key));
}
private static String normalize(String key) {
return key == null ? "" : key.trim();
}
}
@@ -0,0 +1,191 @@
/*
* 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.structure;
import art.arcane.iris.core.structure.authoring.StructureResourceBundle;
import art.arcane.iris.engine.framework.PlacedStructurePiece;
import art.arcane.iris.engine.framework.StructureAssembler;
import art.arcane.iris.engine.object.IrisJigsawPiece;
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.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public final class StructureResourceBundleGraphCompiler {
private static final Gson GSON = new GsonBuilder().create();
private static final List<Long> ASSEMBLY_SEEDS = List.of(
0L, 1L, 2L, 3L, 5L, 8L, 13L, 21L, 34L, 55L, 89L, 144L, 233L, 377L, 610L, 987L);
private StructureResourceBundleGraphCompiler() {
}
public static List<StructureGraphCompilation> compile(StructureResourceBundle bundle) {
BundleGraph graph = parse(Objects.requireNonNull(bundle));
List<StructureGraphCompilation> compilations = new ArrayList<>(graph.structures().size());
for (IrisStructure structure : graph.structures().values()) {
compilations.add(StructureGraphCompiler.compile(structure, graph.resolver()));
}
return List.copyOf(compilations);
}
public static void requireViable(StructureResourceBundle bundle) {
BundleGraph graph = parse(Objects.requireNonNull(bundle));
if (graph.structures().isEmpty()) {
throw new IllegalArgumentException("Structure bundle does not contain a root structure resource");
}
for (IrisStructure structure : graph.structures().values()) {
StructureGraphCompilation compilation = StructureGraphCompiler.compile(structure, graph.resolver());
if (!compilation.isAssemblyViable()) {
String diagnostic = compilation.getDiagnostics().isEmpty()
? "deterministic assembly did not complete"
: compilation.getDiagnostics().getFirst().message();
throw new IllegalArgumentException("Structure bundle graph is not viable: " + diagnostic);
}
requireGeometryViable(compilation);
}
}
private static BundleGraph parse(StructureResourceBundle activeBundle) {
Map<String, IrisStructure> structures = new LinkedHashMap<>();
Map<String, IrisJigsawPool> pools = new LinkedHashMap<>();
Map<String, IrisJigsawPiece> pieces = new LinkedHashMap<>();
Map<String, IrisObject> objects = new LinkedHashMap<>();
for (StructureResourceBundle.Resource resource : activeBundle.resources().values()) {
String path = resource.relativePath();
if (matches(path, "structures/", ".json")) {
IrisStructure structure = readJson(resource, IrisStructure.class);
String key = key(path, "structures/", ".json");
structure.setLoadKey(key);
structures.put(key, structure);
} else if (matches(path, "jigsaw-pools/", ".json")) {
pools.put(key(path, "jigsaw-pools/", ".json"), readJson(resource, IrisJigsawPool.class));
} else if (matches(path, "jigsaw-pieces/", ".json")) {
pieces.put(key(path, "jigsaw-pieces/", ".json"), readJson(resource, IrisJigsawPiece.class));
} else if (matches(path, "objects/", ".iob")) {
objects.put(key(path, "objects/", ".iob"), readObjectBounds(resource));
}
}
return new BundleGraph(structures, new BundleResolver(pools, pieces, objects));
}
private static void requireGeometryViable(StructureGraphCompilation compilation) {
String structureKey = compilation.getGraph().getStructureKey();
for (long seed : ASSEMBLY_SEEDS) {
try {
StructureAssembler assembler = StructureAssembler.forCompilation(
compilation, new IrisPosition(0, 64, 0));
KList<PlacedStructurePiece> pieces = assembler.assemble(new RNG(seed));
if (pieces == null) {
throw new IllegalArgumentException("Structure bundle graph '" + structureKey
+ "' fails sampled runtime geometry assembly at seed " + seed);
}
} catch (RuntimeException e) {
if (e instanceof IllegalArgumentException
&& e.getMessage() != null
&& e.getMessage().startsWith("Structure bundle graph '")) {
throw e;
}
throw new IllegalArgumentException("Structure bundle graph '" + structureKey
+ "' fails sampled runtime geometry assembly at seed " + seed + ": "
+ e.getClass().getSimpleName() + ": " + failureMessage(e), e);
}
}
}
private static String failureMessage(RuntimeException exception) {
return exception.getMessage() == null || exception.getMessage().isBlank()
? "no failure detail" : exception.getMessage();
}
private static boolean matches(String path, String prefix, String suffix) {
return path.startsWith(prefix) && path.endsWith(suffix) && path.length() > prefix.length() + suffix.length();
}
private static String key(String path, String prefix, String suffix) {
return path.substring(prefix.length(), path.length() - suffix.length());
}
private static <T> T readJson(StructureResourceBundle.Resource resource, Class<T> type) {
T value;
try {
value = GSON.fromJson(new String(resource.content(), StandardCharsets.UTF_8), type);
} catch (RuntimeException e) {
throw new IllegalArgumentException("Malformed structure resource " + resource.relativePath(), e);
}
if (value == null) {
throw new IllegalArgumentException("Empty structure resource " + resource.relativePath());
}
return value;
}
private static IrisObject readObjectBounds(StructureResourceBundle.Resource resource) {
try (ByteArrayInputStream input = new ByteArrayInputStream(resource.content())) {
return IrisObjectFrameReader.readBounds(input, resource.relativePath());
} catch (IOException e) {
throw new IllegalArgumentException(e.getMessage(), e);
}
}
private record BundleResolver(
Map<String, IrisJigsawPool> pools,
Map<String, IrisJigsawPiece> pieces,
Map<String, IrisObject> objects
) implements StructureGraphResolver {
private BundleResolver {
pools = Map.copyOf(pools);
pieces = Map.copyOf(pieces);
objects = Map.copyOf(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);
}
}
private record BundleGraph(Map<String, IrisStructure> structures, BundleResolver resolver) {
private BundleGraph {
structures = Map.copyOf(structures);
Objects.requireNonNull(resolver);
}
}
}
@@ -0,0 +1,281 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.mantle.components;
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.TileData;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.B;
import art.arcane.volmlib.util.collection.KList;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
final class CaveObjectPlacementTransaction implements IObjectPlacer {
private static final int CACHE_MISS = Integer.MIN_VALUE;
private final IObjectPlacer delegate;
private final Engine engine;
private final int anchorY;
private final int minDepthBelowSurface;
private final int worldHeight;
private final KList<BufferedMutation> mutations;
private final Map<PositionKey, PlatformBlockState> bufferedBlocks;
private final Long2IntOpenHashMap surfaceHeights;
private final Long2IntOpenHashMap caveCeilings;
private int blockWrites;
CaveObjectPlacementTransaction(IObjectPlacer delegate, int anchorY, int minDepthBelowSurface) {
this.delegate = delegate;
this.engine = delegate.getEngine();
this.anchorY = anchorY;
this.minDepthBelowSurface = Math.max(0, minDepthBelowSurface);
this.worldHeight = engine == null ? 0 : engine.getHeight();
this.mutations = new KList<>();
this.bufferedBlocks = new HashMap<>();
this.surfaceHeights = new Long2IntOpenHashMap();
this.caveCeilings = new Long2IntOpenHashMap();
this.surfaceHeights.defaultReturnValue(CACHE_MISS);
this.caveCeilings.defaultReturnValue(CACHE_MISS);
}
CommitResult commit() {
if (blockWrites == 0) {
discard();
return CommitResult.EMPTY;
}
for (BufferedMutation mutation : mutations) {
if (!isWithinBounds(mutation.x(), mutation.y(), mutation.z())) {
discard();
return CommitResult.REJECTED_BOUNDS;
}
}
for (BufferedMutation mutation : mutations) {
mutation.apply(delegate);
}
discard();
return CommitResult.COMMITTED;
}
void discard() {
mutations.clear();
bufferedBlocks.clear();
blockWrites = 0;
}
int getCaveCeiling(int x, int z) {
long key = Cache.key(x, z);
int cached = caveCeilings.get(key);
if (cached != CACHE_MISS) {
return cached;
}
int ceiling = findCaveCeiling(x, z);
caveCeilings.put(key, ceiling);
return ceiling;
}
static int maxBuriedY(int worldHeight, int surfaceY, int minDepthBelowSurface) {
return Math.min(worldHeight - 1, surfaceY - Math.max(0, minDepthBelowSurface));
}
@Override
public int getHighest(int x, int z, IrisData data) {
return delegate.getHighest(x, z, data);
}
@Override
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
return delegate.getHighest(x, z, data, ignoreFluid);
}
@Override
public void set(int x, int y, int z, PlatformBlockState state) {
if (state == null) {
return;
}
mutations.add(new BlockMutation(x, y, z, state));
bufferedBlocks.put(new PositionKey(x, y, z), state);
blockWrites++;
}
@Override
public PlatformBlockState get(int x, int y, int z) {
PlatformBlockState buffered = bufferedBlocks.get(new PositionKey(x, y, z));
return buffered == null ? delegate.get(x, y, z) : buffered;
}
@Override
public boolean isPreventingDecay() {
return delegate.isPreventingDecay();
}
@Override
public boolean isCarved(int x, int y, int z) {
return delegate.isCarved(x, y, z);
}
@Override
public boolean isSolid(int x, int y, int z) {
PlatformBlockState buffered = bufferedBlocks.get(new PositionKey(x, y, z));
return buffered == null ? delegate.isSolid(x, y, z) : B.isSolid(buffered);
}
@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 void setTile(int x, int y, int z, TileData tile) {
if (tile != null) {
mutations.add(new TileMutation(x, y, z, tile));
}
}
@Override
public <T> void setData(int x, int y, int z, T data) {
if (data == null) {
return;
}
mutations.add(new DataMutation(x, y, z, data));
if (data instanceof PlatformBlockState state) {
bufferedBlocks.put(new PositionKey(x, y, z), state);
blockWrites++;
}
}
@Override
public <T> @Nullable T getData(int x, int y, int z, Class<T> type) {
for (int i = mutations.size() - 1; i >= 0; i--) {
BufferedMutation mutation = mutations.get(i);
if (mutation.x() != x || mutation.y() != y || mutation.z() != z) {
continue;
}
Object value = mutation.value();
if (type.isInstance(value)) {
return type.cast(value);
}
}
return delegate.getData(x, y, z, type);
}
@Override
public Engine getEngine() {
return engine;
}
private boolean isWithinBounds(int x, int y, int z) {
if (engine == null || y < 0 || y >= worldHeight) {
return false;
}
int surfaceY = getSurfaceHeight(x, z);
if (y > maxBuriedY(worldHeight, surfaceY, minDepthBelowSurface)) {
return false;
}
return y < getCaveCeiling(x, z);
}
private int getSurfaceHeight(int x, int z) {
long key = Cache.key(x, z);
int cached = surfaceHeights.get(key);
if (cached != CACHE_MISS) {
return cached;
}
int surfaceY = engine.getHeight(x, z, true);
surfaceHeights.put(key, surfaceY);
return surfaceY;
}
private int findCaveCeiling(int x, int z) {
if (worldHeight <= 0 || anchorY < 0 || anchorY >= worldHeight) {
return 0;
}
if (!delegate.isCarved(x, anchorY, z)) {
return Math.min(worldHeight, anchorY + 1);
}
int scanLimit = Math.min(worldHeight - 1, Math.max(anchorY, getSurfaceHeight(x, z)));
for (int y = anchorY + 1; y <= scanLimit; y++) {
if (!delegate.isCarved(x, y, z)) {
return y;
}
}
return Math.min(worldHeight, scanLimit + 1);
}
enum CommitResult {
COMMITTED,
EMPTY,
REJECTED_BOUNDS
}
private interface BufferedMutation {
int x();
int y();
int z();
Object value();
void apply(IObjectPlacer placer);
}
private record BlockMutation(int x, int y, int z, PlatformBlockState value) implements BufferedMutation {
@Override
public void apply(IObjectPlacer placer) {
placer.set(x, y, z, value);
}
}
private record TileMutation(int x, int y, int z, TileData value) implements BufferedMutation {
@Override
public void apply(IObjectPlacer placer) {
placer.setTile(x, y, z, value);
}
}
private record DataMutation(int x, int y, int z, Object value) implements BufferedMutation {
@Override
public void apply(IObjectPlacer placer) {
placer.setData(x, y, z, value);
}
}
private record PositionKey(int x, int y, int z) {
}
}
@@ -0,0 +1,74 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.mantle.components;
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.mantle.EngineMantle;
import art.arcane.iris.engine.modifier.IrisFloatingChildBiomeModifier;
import art.arcane.iris.engine.object.FloatingIslandBoundarySampler;
import art.arcane.iris.engine.object.FloatingIslandSample;
import art.arcane.iris.engine.object.IrisBiome;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import org.jetbrains.annotations.Nullable;
final class FloatingIslandSampleResolver implements IslandObjectPlacer.SampleProvider {
private final IrisData data;
private final Engine engine;
private final int chunkHeight;
private final long baseSeed;
private final FloatingIslandBoundarySampler boundarySampler;
private final Long2ObjectOpenHashMap<FloatingIslandSample> sampleCache;
private final LongOpenHashSet resolvedSamples;
FloatingIslandSampleResolver(EngineMantle engineMantle, FloatingIslandBoundarySampler boundarySampler) {
this.data = engineMantle.getData();
this.engine = engineMantle.getEngine();
this.chunkHeight = engine.getHeight();
this.baseSeed = engine.getSeedManager().getTerrain() ^ IrisFloatingChildBiomeModifier.FLOATING_BASE_SEED_SALT;
this.boundarySampler = boundarySampler;
this.sampleCache = new Long2ObjectOpenHashMap<>();
this.resolvedSamples = new LongOpenHashSet();
}
@Override
public @Nullable FloatingIslandSample sample(int x, int z) {
long key = Cache.key(x, z);
if (resolvedSamples.contains(key)) {
return sampleCache.get(key);
}
IrisBiome parent = parent(x, z);
if (parent == null || parent.getFloatingChildBiomes() == null || parent.getFloatingChildBiomes().isEmpty()) {
resolvedSamples.add(key);
return null;
}
FloatingIslandSample sample = FloatingIslandSample.sample(parent, x, z, chunkHeight, baseSeed, data, engine, boundarySampler);
resolvedSamples.add(key);
if (sample != null) {
sampleCache.put(key, sample);
}
return sample;
}
@Nullable IrisBiome parent(int x, int z) {
return boundarySampler.parent(x, z);
}
}
@@ -0,0 +1,206 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <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.PlatformBlockState;
import art.arcane.iris.util.common.data.B;
import art.arcane.volmlib.util.collection.KList;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
final class FloatingObjectPlacementTransaction implements IObjectPlacer {
private final IslandObjectPlacer delegate;
private final KList<BufferedMutation> mutations;
private final Map<PositionKey, PlatformBlockState> bufferedBlocks;
private int blockWrites;
FloatingObjectPlacementTransaction(IslandObjectPlacer delegate) {
this.delegate = delegate;
this.mutations = new KList<>();
this.bufferedBlocks = new HashMap<>();
}
CommitResult commit() {
if (blockWrites == 0) {
discard();
return CommitResult.EMPTY;
}
for (BufferedMutation mutation : mutations) {
if (!delegate.canWriteObjectBlock(mutation.x(), mutation.y(), mutation.z())) {
discard();
return CommitResult.REJECTED_SUPPORT;
}
}
for (BufferedMutation mutation : mutations) {
mutation.apply(delegate);
}
discard();
return CommitResult.COMMITTED;
}
void discard() {
mutations.clear();
bufferedBlocks.clear();
blockWrites = 0;
}
@Override
public int getHighest(int x, int z, IrisData data) {
return delegate.getHighest(x, z, data);
}
@Override
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
return delegate.getHighest(x, z, data, ignoreFluid);
}
@Override
public void set(int x, int y, int z, PlatformBlockState state) {
if (state == null) {
return;
}
mutations.add(new BlockMutation(x, y, z, state));
bufferedBlocks.put(new PositionKey(x, y, z), state);
blockWrites++;
}
@Override
public PlatformBlockState get(int x, int y, int z) {
PlatformBlockState state = bufferedBlocks.get(new PositionKey(x, y, z));
return state == null ? delegate.get(x, y, z) : state;
}
@Override
public boolean isPreventingDecay() {
return delegate.isPreventingDecay();
}
@Override
public boolean isCarved(int x, int y, int z) {
return delegate.isCarved(x, y, z);
}
@Override
public boolean isSolid(int x, int y, int z) {
PlatformBlockState state = bufferedBlocks.get(new PositionKey(x, y, z));
return state == null ? delegate.isSolid(x, y, z) : B.isSolid(state);
}
@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 void setTile(int x, int y, int z, TileData tile) {
if (tile != null) {
mutations.add(new TileMutation(x, y, z, tile));
}
}
@Override
public <T> void setData(int x, int y, int z, T data) {
if (data == null) {
return;
}
mutations.add(new DataMutation(x, y, z, data));
if (data instanceof PlatformBlockState state) {
bufferedBlocks.put(new PositionKey(x, y, z), state);
blockWrites++;
}
}
@Override
public <T> @Nullable T getData(int x, int y, int z, Class<T> type) {
for (int i = mutations.size() - 1; i >= 0; i--) {
BufferedMutation mutation = mutations.get(i);
if (mutation.x() != x || mutation.y() != y || mutation.z() != z) {
continue;
}
Object value = mutation.value();
if (type.isInstance(value)) {
return type.cast(value);
}
}
return delegate.getData(x, y, z, type);
}
@Override
public Engine getEngine() {
return delegate.getEngine();
}
enum CommitResult {
COMMITTED,
EMPTY,
REJECTED_SUPPORT
}
private interface BufferedMutation {
int x();
int y();
int z();
Object value();
void apply(IObjectPlacer placer);
}
private record BlockMutation(int x, int y, int z, PlatformBlockState value) implements BufferedMutation {
@Override
public void apply(IObjectPlacer placer) {
placer.set(x, y, z, value);
}
}
private record TileMutation(int x, int y, int z, TileData value) implements BufferedMutation {
@Override
public void apply(IObjectPlacer placer) {
placer.setTile(x, y, z, value);
}
}
private record DataMutation(int x, int y, int z, Object value) implements BufferedMutation {
@Override
public void apply(IObjectPlacer placer) {
placer.setData(x, y, z, value);
}
}
private record PositionKey(int x, int y, int z) {
}
}
@@ -35,10 +35,14 @@ import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
public class IrisCaveCarver3D {
private static final byte LIQUID_AIR = 0;
private static final byte LIQUID_WATER = 1;
private static final byte LIQUID_LAVA = 2;
private static final byte LIQUID_FORCED_AIR = 3;
private static final int ADAPTIVE_MIN_PLANE_COLUMNS = 16;
@@ -58,6 +62,7 @@ public class IrisCaveCarver3D {
private final ModuleState[] modules;
private final double inverseNormalization;
private final MatterCavern carveAir;
private final MatterCavern carveWater;
private final MatterCavern carveLava;
private final MatterCavern carveForcedAir;
private final double normalizationFactor;
@@ -76,6 +81,7 @@ public class IrisCaveCarver3D {
this.data = engine.getData();
this.profile = profile;
this.carveAir = new MatterCavern(true, "", LIQUID_AIR);
this.carveWater = new MatterCavern(true, "", LIQUID_WATER);
this.carveLava = new MatterCavern(true, "", LIQUID_LAVA);
this.carveForcedAir = new MatterCavern(true, "", LIQUID_FORCED_AIR);
List<ModuleState> moduleStates = new ArrayList<>();
@@ -166,6 +172,25 @@ public class IrisCaveCarver3D {
IrisRange worldYRange,
int[] precomputedSurfaceHeights,
IrisRange overrideVerticalRange
) {
WaterSupportPlan waterSupportPlan = new WaterSupportPlan();
int carved = carve(writer, chunkX, chunkZ, columnWeights, minWeight, thresholdPenalty,
worldYRange, precomputedSurfaceHeights, overrideVerticalRange, waterSupportPlan);
waterSupportPlan.resolve(writer.acquireChunk(chunkX, chunkZ));
return carved;
}
int carve(
MantleWriter writer,
int chunkX,
int chunkZ,
double[] columnWeights,
double minWeight,
double thresholdPenalty,
IrisRange worldYRange,
int[] precomputedSurfaceHeights,
IrisRange overrideVerticalRange,
WaterSupportPlan waterSupportPlan
) {
PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start();
try {
@@ -213,6 +238,7 @@ public class IrisCaveCarver3D {
int x0 = PowerOfTwoCoordinates.chunkToBlock(chunkX);
int z0 = PowerOfTwoCoordinates.chunkToBlock(chunkZ);
int[] columnMaxY = scratch.columnMaxY;
int[] waterMaxY = scratch.waterMaxY;
int[] surfaceBreakFloorY = scratch.surfaceBreakFloorY;
boolean[] surfaceBreakColumn = scratch.surfaceBreakColumn;
double[] columnThreshold = scratch.columnThreshold;
@@ -240,6 +266,7 @@ public class IrisCaveCarver3D {
: clearanceTopY;
columnMaxY[index] = columnTopY;
waterMaxY[index] = resolveWaterMaxY(columnSurfaceY);
surfaceBreakFloorY[index] = Math.max(minY, columnSurfaceY - surfaceBreakDepth);
surfaceBreakColumn[index] = breakColumn;
columnThreshold[index] = profile.getDensityThreshold().get(thresholdRng, x, z, data) - profile.getThresholdBias();
@@ -260,12 +287,14 @@ public class IrisCaveCarver3D {
adaptiveThresholdMargin,
surfaceBreakThresholdBoost,
columnMaxY,
waterMaxY,
surfaceBreakFloorY,
surfaceBreakColumn,
columnThreshold,
clampedWeights,
verticalEdgeFade,
matterByY,
profile.isWaterRequiresFloor() ? waterSupportPlan : null,
resolvedMinWeight,
resolvedThresholdPenalty,
0D,
@@ -280,12 +309,14 @@ public class IrisCaveCarver3D {
maxY,
surfaceBreakThresholdBoost,
columnMaxY,
waterMaxY,
surfaceBreakFloorY,
surfaceBreakColumn,
columnThreshold,
clampedWeights,
verticalEdgeFade,
matterByY,
profile.isWaterRequiresFloor() ? waterSupportPlan : null,
resolvedMinWeight,
resolvedThresholdPenalty,
0D,
@@ -303,12 +334,14 @@ public class IrisCaveCarver3D {
latticeStep,
surfaceBreakThresholdBoost,
columnMaxY,
waterMaxY,
surfaceBreakFloorY,
surfaceBreakColumn,
columnThreshold,
clampedWeights,
verticalEdgeFade,
matterByY,
profile.isWaterRequiresFloor() ? waterSupportPlan : null,
resolvedMinWeight,
resolvedThresholdPenalty,
0D,
@@ -324,12 +357,14 @@ public class IrisCaveCarver3D {
sampleStep,
surfaceBreakThresholdBoost,
columnMaxY,
waterMaxY,
surfaceBreakFloorY,
surfaceBreakColumn,
columnThreshold,
clampedWeights,
verticalEdgeFade,
matterByY,
profile.isWaterRequiresFloor() ? waterSupportPlan : null,
resolvedMinWeight,
resolvedThresholdPenalty,
0D,
@@ -352,12 +387,14 @@ public class IrisCaveCarver3D {
int maxY,
double surfaceBreakThresholdBoost,
int[] columnMaxY,
int[] waterMaxY,
int[] surfaceBreakFloorY,
boolean[] surfaceBreakColumn,
double[] columnThreshold,
double[] clampedWeights,
double[] verticalEdgeFade,
MatterCavern[] matterByY,
WaterSupportPlan waterSupportPlan,
double minWeight,
double thresholdPenalty,
double thresholdBoost,
@@ -423,7 +460,7 @@ public class IrisCaveCarver3D {
classifyDensityPlane(x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve);
int fadeIndex = y - minY;
int localY = y & 15;
MatterCavern matter = matterByY[fadeIndex];
MatterCavern verticalMatter = matterByY[fadeIndex];
if (skipExistingCarved) {
for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) {
@@ -438,7 +475,10 @@ public class IrisCaveCarver3D {
continue;
}
cavernSlice.set(localX, localY, localZ, matter);
double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization;
MatterCavern matter = resolveMatter(verticalMatter, x0 + localX, y, z0 + localZ,
columnIndex, waterMaxY, localThreshold);
writeCavern(cavernSlice, localX, y, localZ, matter, waterSupportPlan);
carved++;
}
continue;
@@ -452,7 +492,10 @@ public class IrisCaveCarver3D {
int columnIndex = planeColumnIndices[planeIndex];
int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
int localZ = columnIndex & 15;
cavernSlice.set(localX, localY, localZ, matter);
double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization;
MatterCavern matter = resolveMatter(verticalMatter, x0 + localX, y, z0 + localZ,
columnIndex, waterMaxY, localThreshold);
writeCavern(cavernSlice, localX, y, localZ, matter, waterSupportPlan);
carved++;
}
}
@@ -471,12 +514,14 @@ public class IrisCaveCarver3D {
double adaptiveThresholdMargin,
double surfaceBreakThresholdBoost,
int[] columnMaxY,
int[] waterMaxY,
int[] surfaceBreakFloorY,
boolean[] surfaceBreakColumn,
double[] columnThreshold,
double[] clampedWeights,
double[] verticalEdgeFade,
MatterCavern[] matterByY,
WaterSupportPlan waterSupportPlan,
double minWeight,
double thresholdPenalty,
double thresholdBoost,
@@ -558,7 +603,7 @@ public class IrisCaveCarver3D {
);
int fadeIndex = y - minY;
int localY = y & 15;
MatterCavern matter = matterByY[fadeIndex];
MatterCavern verticalMatter = matterByY[fadeIndex];
if (skipExistingCarved) {
for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) {
@@ -573,7 +618,10 @@ public class IrisCaveCarver3D {
continue;
}
cavernSlice.set(localX, localY, localZ, matter);
double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization;
MatterCavern matter = resolveMatter(verticalMatter, x0 + localX, y, z0 + localZ,
columnIndex, waterMaxY, localThreshold);
writeCavern(cavernSlice, localX, y, localZ, matter, waterSupportPlan);
carved++;
}
continue;
@@ -587,7 +635,10 @@ public class IrisCaveCarver3D {
int columnIndex = planeColumnIndices[planeIndex];
int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
int localZ = columnIndex & 15;
cavernSlice.set(localX, localY, localZ, matter);
double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization;
MatterCavern matter = resolveMatter(verticalMatter, x0 + localX, y, z0 + localZ,
columnIndex, waterMaxY, localThreshold);
writeCavern(cavernSlice, localX, y, localZ, matter, waterSupportPlan);
carved++;
}
}
@@ -627,12 +678,14 @@ public class IrisCaveCarver3D {
int latticeStep,
double surfaceBreakThresholdBoost,
int[] columnMaxY,
int[] waterMaxY,
int[] surfaceBreakFloorY,
boolean[] surfaceBreakColumn,
double[] columnThreshold,
double[] clampedWeights,
double[] verticalEdgeFade,
MatterCavern[] matterByY,
WaterSupportPlan waterSupportPlan,
double minWeight,
double thresholdPenalty,
double thresholdBoost,
@@ -717,7 +770,7 @@ public class IrisCaveCarver3D {
double density = sampleDensityOptimized(x, y, z);
int stampMaxY = Math.min(maxY, y + 1);
for (int yy = y; yy <= stampMaxY; yy++) {
MatterCavern matter = matterByY[yy - minY];
MatterCavern verticalMatter = matterByY[yy - minY];
MatterSlice<MatterCavern> cavernSlice = resolveCavernSlice(scratch, chunk, PowerOfTwoCoordinates.floorDivPow2(yy, 4));
int localY = yy & 15;
int fadeIndex = yy - minY;
@@ -738,15 +791,19 @@ public class IrisCaveCarver3D {
int localX = tileLocalX[columnIndex];
int localZ = tileLocalZ[columnIndex];
int worldX = x0 + localX;
int worldZ = z0 + localZ;
MatterCavern matter = resolveMatter(verticalMatter, worldX, yy, worldZ,
index, waterMaxY, localThreshold);
if (skipExistingCarved) {
if (cavernSlice.get(localX, localY, localZ) == null) {
cavernSlice.set(localX, localY, localZ, matter);
writeCavern(cavernSlice, localX, yy, localZ, matter, waterSupportPlan);
carved++;
}
continue;
}
cavernSlice.set(localX, localY, localZ, matter);
writeCavern(cavernSlice, localX, yy, localZ, matter, waterSupportPlan);
carved++;
}
}
@@ -766,12 +823,14 @@ public class IrisCaveCarver3D {
int sampleStep,
double surfaceBreakThresholdBoost,
int[] columnMaxY,
int[] waterMaxY,
int[] surfaceBreakFloorY,
boolean[] surfaceBreakColumn,
double[] columnThreshold,
double[] clampedWeights,
double[] verticalEdgeFade,
MatterCavern[] matterByY,
WaterSupportPlan waterSupportPlan,
double minWeight,
double thresholdPenalty,
double thresholdBoost,
@@ -812,18 +871,20 @@ public class IrisCaveCarver3D {
int carveMaxY = Math.min(columnTopY, y + sampleStep - 1);
for (int yy = y; yy <= carveMaxY; yy++) {
MatterCavern matter = matterByY[yy - minY];
MatterCavern verticalMatter = matterByY[yy - minY];
MatterCavern matter = resolveMatter(verticalMatter, x, yy, z,
index, waterMaxY, localThreshold);
MatterSlice<MatterCavern> cavernSlice = resolveCavernSlice(scratch, chunk, PowerOfTwoCoordinates.floorDivPow2(yy, 4));
int localY = yy & 15;
if (skipExistingCarved) {
if (cavernSlice.get(lx, localY, lz) == null) {
cavernSlice.set(lx, localY, lz, matter);
writeCavern(cavernSlice, lx, yy, lz, matter, waterSupportPlan);
carved++;
}
continue;
}
cavernSlice.set(lx, localY, lz, matter);
writeCavern(cavernSlice, lx, yy, lz, matter, waterSupportPlan);
carved++;
}
}
@@ -2046,9 +2107,7 @@ public class IrisCaveCarver3D {
MatterCavern[] matterByY = scratch.matterByY;
boolean allowLava = profile.isAllowLava();
boolean allowWater = profile.isAllowWater();
int lavaHeight = engine.getDimension().getCaveLavaHeight();
int fluidHeight = engine.getDimension().getFluidHeight();
for (int y = minY; y <= maxY; y++) {
int offset = y - minY;
@@ -2056,10 +2115,6 @@ public class IrisCaveCarver3D {
matterByY[offset] = carveLava;
continue;
}
if (allowWater && y <= fluidHeight) {
matterByY[offset] = carveAir;
continue;
}
if (!allowLava && y <= lavaHeight) {
matterByY[offset] = carveForcedAir;
continue;
@@ -2071,6 +2126,77 @@ public class IrisCaveCarver3D {
return matterByY;
}
private int resolveWaterMaxY(int columnSurfaceY) {
if (!profile.isAllowWater()) {
return Integer.MIN_VALUE;
}
int minDepth = Math.max(0, profile.getWaterMinDepthBelowSurface());
return Math.min(engine.getDimension().getFluidHeight(), columnSurfaceY - minDepth);
}
private MatterCavern resolveMatter(MatterCavern verticalMatter, int x, int y, int z,
int columnIndex, int[] waterMaxY, double localThreshold) {
if (verticalMatter != carveLava
&& y <= waterMaxY[columnIndex]
&& isAquiferCandidate(x, y, z, localThreshold)) {
return carveWater;
}
return verticalMatter;
}
private boolean isAquiferCandidate(int x, int y, int z, double localThreshold) {
int fluidHeight = engine.getDimension().getFluidHeight();
double depthFactor = Math.max(0D, Math.min(1.5D, (fluidHeight - y) / 48D));
double cutoff = 0.35D + (depthFactor * 0.2D);
if (detailDensity.noiseFastSigned3D(x, y * 0.5D, z) <= cutoff) {
return false;
}
return !profile.isWaterRequiresFloor() || hasAquiferCupSupport(x, y, z, localThreshold);
}
private boolean hasAquiferCupSupport(int x, int y, int z, double threshold) {
int floorY = Math.max(0, y - 1);
int deepFloorY = Math.max(0, y - 2);
int aboveY = Math.min(engine.getHeight() - 1, y + 1);
if (!isDensitySolid(x, floorY, z, threshold)) {
return false;
}
if (!isDensitySolid(x, deepFloorY, z, threshold - 0.05D)) {
return false;
}
int support = 0;
if (isDensitySolid(x + 1, y, z, threshold)) {
support++;
}
if (isDensitySolid(x - 1, y, z, threshold)) {
support++;
}
if (isDensitySolid(x, y, z + 1, threshold)) {
support++;
}
if (isDensitySolid(x, y, z - 1, threshold)) {
support++;
}
if (isDensitySolid(x, aboveY, z, threshold)) {
support++;
}
return support >= 4;
}
private boolean isDensitySolid(int x, int y, int z, double threshold) {
return sampleDensityOptimized(x, y, z) > threshold;
}
private void writeCavern(MatterSlice<MatterCavern> cavernSlice, int localX, int y, int localZ,
MatterCavern matter, WaterSupportPlan waterSupportPlan) {
cavernSlice.set(localX, y & 15, localZ, matter);
if (waterSupportPlan != null && matter == carveWater) {
waterSupportPlan.add(localX, y, localZ, carveWater, carveAir);
}
}
private void prepareSectionCaches(Scratch scratch, int minY, int maxY) {
int minSection = Math.max(0, PowerOfTwoCoordinates.floorDivPow2(minY, 4));
int maxSection = Math.max(minSection, PowerOfTwoCoordinates.floorDivPow2(maxY, 4));
@@ -2139,6 +2265,97 @@ public class IrisCaveCarver3D {
return verticalEdgeFade;
}
static final class WaterSupportPlan {
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;
}
}
private static final class ModuleState {
private final CNG density;
private final int minY;
@@ -2176,6 +2393,7 @@ public class IrisCaveCarver3D {
private static final class Scratch {
private final int[] columnMaxY = new int[256];
private final int[] waterMaxY = new int[256];
private final int[] surfaceBreakFloorY = new int[256];
private final boolean[] surfaceBreakColumn = new boolean[256];
private final double[] columnThreshold = new double[256];
@@ -19,6 +19,7 @@
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.framework.IrisStructureLocator;
@@ -30,12 +31,14 @@ import art.arcane.iris.engine.mantle.IrisMantleComponent;
import art.arcane.iris.engine.mantle.MantleWriter;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.ObjectPlaceMode;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisStructure;
import art.arcane.iris.engine.object.IrisStructurePlacement;
import art.arcane.iris.engine.object.IrisStructureStiltSettings;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.noise.CNG;
@@ -45,8 +48,11 @@ import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import art.arcane.volmlib.util.math.RNG;
import it.unimi.dsi.fastutil.longs.Long2IntMap;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
@ComponentFlag(ReservedFlag.JIGSAW)
@@ -81,15 +87,15 @@ public class IrisStructureComponent extends IrisMantleComponent {
}
placements.addAll(getDimension().getStructures());
for (int placementOrdinal = 0; placementOrdinal < placements.size(); placementOrdinal++) {
placeFromPlacement(writer, placements.get(placementOrdinal), x, z, placementOrdinal);
for (IrisStructurePlacement placement : placements) {
placeFromPlacement(writer, placement, x, z);
}
}
@ChunkCoordinates
private void placeFromPlacement(MantleWriter writer, IrisStructurePlacement placement, int cx, int cz, int placementOrdinal) {
private void placeFromPlacement(MantleWriter writer, IrisStructurePlacement placement, int cx, int cz) {
IrisStructureLocator.ResolvedPlacement resolved = IrisStructureLocator.resolvePlacement(
getEngineMantle().getEngine(), placement, cx, cz, placementOrdinal);
getEngineMantle().getEngine(), placement, cx, cz);
if (resolved == null) {
return;
}
@@ -102,7 +108,10 @@ public class IrisStructureComponent extends IrisMantleComponent {
String key = resolved.structureKey();
IrisStructure structure = resolved.structure();
KList<PlacedStructurePiece> pieces = resolved.pieces();
KList<PlacedStructurePiece> pieces = resolvedPiecesOrNull(resolved, cx, cz);
if (pieces == null) {
return;
}
RNG rng = resolved.rng();
int baseY = resolved.baseY();
if (trace) {
@@ -120,23 +129,108 @@ public class IrisStructureComponent extends IrisMantleComponent {
}
ObjectPlaceMode mode = structure.getPlaceMode();
int failedPieces = 0;
Long2IntOpenHashMap foundationColumns = placement.getStilt() == null
? null : new Long2IntOpenHashMap();
if (placement.isUnderground()) {
ObjectPlaceMode undergroundMode = (mode == ObjectPlaceMode.ORGANIC_STILT || mode == ObjectPlaceMode.CEILING_HANG)
? mode : ObjectPlaceMode.STRUCTURE_PIECE;
for (PlacedStructurePiece p : pieces) {
placeObject(writer, structure, p, undergroundMode, p.getY(), rng);
if (placeObject(writer, structure, p, undergroundMode, p.getY(), rng, foundationColumns) == -1) {
failedPieces++;
}
}
} else if (mode == ObjectPlaceMode.STRUCTURE_PIECE || mode == ObjectPlaceMode.FLOATING) {
for (PlacedStructurePiece p : pieces) {
placeObject(writer, structure, p, ObjectPlaceMode.STRUCTURE_PIECE, p.getY(), rng);
if (placeObject(writer, structure, p, ObjectPlaceMode.STRUCTURE_PIECE, p.getY(), rng, foundationColumns) == -1) {
failedPieces++;
}
}
} else if (pieces.size() == 1) {
placeObject(writer, structure, pieces.getFirst(), mode, -1, rng);
if (placeObject(writer, structure, pieces.getFirst(), mode, -1, rng, foundationColumns) == -1) {
failedPieces++;
}
} else {
for (PlacedStructurePiece p : pieces) {
placeObject(writer, structure, p, ObjectPlaceMode.STRUCTURE_PIECE, p.getY(), rng);
if (placeObject(writer, structure, p, ObjectPlaceMode.STRUCTURE_PIECE, p.getY(), rng, foundationColumns) == -1) {
failedPieces++;
}
}
}
requireAppliedPieces(resolved, cx, cz, failedPieces);
if (failedPieces == 0 && placement.getStilt() != null) {
placeFoundation(writer, foundationColumns, placement.getStilt(), rng);
}
}
private void placeFoundation(MantleWriter writer, Long2IntOpenHashMap columns,
IrisStructureStiltSettings settings, RNG rng) {
IrisMaterialPalette palette = Objects.requireNonNull(
settings.getPalette(), "Structure stilt palette must not be null");
int mantleOffset = getEngineMantle().getEngine().getMinHeight();
int maxDepth = Math.max(1, settings.getMaxDepth());
for (Long2IntMap.Entry column : columns.long2IntEntrySet()) {
int worldX = StructureFoundationPlanner.unpackX(column.getLongKey());
int worldZ = StructureFoundationPlanner.unpackZ(column.getLongKey());
int foundationY = column.getIntValue();
PlatformBlockState foundationState = writer.getDataIfPresent(
worldX, foundationY, worldZ, PlatformBlockState.class);
if (foundationState == null || !foundationState.isSolid()) {
continue;
}
int terrainHeight = getEngineMantle().trueHeight(worldX, worldZ);
int groundY = StructureFoundationPlanner.findGroundY(
foundationY, maxDepth, 0,
y -> StructureFoundationPlanner.isGroundSolid(
writer.getDataIfPresent(worldX, y, worldZ, PlatformBlockState.class),
writer.getDataIfPresent(worldX, y, worldZ, MatterCavern.class) != null,
y, terrainHeight));
if (groundY == StructureFoundationPlanner.NO_GROUND) {
continue;
}
StructureFoundationPlanner.fillSupportColumn(foundationY, groundY, y -> {
writeFoundationSupport(
writer, palette, rng, getData(), worldX, y, worldZ, mantleOffset);
});
}
}
static void writeFoundationSupport(MantleWriter writer, IrisMaterialPalette palette, RNG rng,
IrisData data, int worldX, int mantleY, int worldZ,
int mantleOffset) {
int worldY = mantleY + mantleOffset;
PlatformBlockState support = palette.get(rng, worldX, worldY, worldZ, data);
if (support == null) {
throw new IllegalStateException("Structure stilt palette resolved no block at "
+ worldX + "," + worldY + "," + worldZ);
}
writer.clearData(worldX, mantleY, worldZ, MatterCavern.class);
writer.set(worldX, mantleY, worldZ, support);
}
static KList<PlacedStructurePiece> resolvedPiecesOrNull(
IrisStructureLocator.ResolvedPlacement resolved,
int chunkX,
int chunkZ
) {
if (resolved == null) {
return null;
}
KList<PlacedStructurePiece> pieces = resolved.pieces();
if (!IrisStructureLocator.requirePlacementOutput(
resolved.placement(), resolved.structureKey(), chunkX, chunkZ,
pieces != null && !pieces.isEmpty(), "placement application received no assembled pieces")) {
return null;
}
return pieces;
}
static void requireAppliedPieces(IrisStructureLocator.ResolvedPlacement resolved,
int chunkX, int chunkZ, int failedPieces) {
IrisStructureLocator.requirePlacementOutput(
resolved.placement(), resolved.structureKey(), chunkX, chunkZ, failedPieces == 0,
"object placement rejected " + failedPieces + " of " + resolved.pieces().size()
+ " assembled piece(s)");
}
private void boreStructure(MantleWriter writer, KList<PlacedStructurePiece> pieces, int padding) {
@@ -377,7 +471,8 @@ public class IrisStructureComponent extends IrisMantleComponent {
return new int[]{minX, minY, minZ, maxX, maxY, maxZ};
}
private void placeObject(MantleWriter writer, IrisStructure structure, PlacedStructurePiece p, ObjectPlaceMode mode, int y, RNG rng) {
private int placeObject(MantleWriter writer, IrisStructure structure, PlacedStructurePiece p,
ObjectPlaceMode mode, int y, RNG rng, Long2IntOpenHashMap foundationColumns) {
IrisObject object = p.getObject();
String objectKey = object.getLoadKey();
IrisObjectPlacement config = structure.createLootPlacement(objectKey);
@@ -391,7 +486,9 @@ public class IrisStructureComponent extends IrisMantleComponent {
}
int placeY = (y == -1) ? -1 : y - getEngineMantle().getEngine().getMinHeight();
String marker = structurePlacementMarker(structure, p, objectKey);
object.place(p.getX(), placeY, p.getZ(), writer, config, rng, (position, state) -> {
return object.place(p.getX(), placeY, p.getZ(), writer, config, rng, (position, state) -> {
StructureFoundationPlanner.recordBaseCell(
foundationColumns, position.getX(), position.getY(), position.getZ(), state);
if (marker != null && shouldWriteStructureMarker(state)) {
writer.setData(position.getX(), position.getY(), position.getZ(), marker);
}
@@ -443,7 +540,7 @@ public class IrisStructureComponent extends IrisMantleComponent {
int carvePadding = placement.isOverbore() ? Math.max(0, placement.getOverboreRadius())
: placement.isBore() ? Math.max(0, placement.getBorePadding()) : 0;
for (String key : placement.getStructures()) {
IrisStructure structure = art.arcane.iris.core.loader.IrisData.loadAnyStructure(key, getData());
IrisStructure structure = getData().load(IrisStructure.class, key, false);
if (structure != null) {
max = Math.max(max, Math.max(1, structure.getMaxSizeChunks()) * 16 + carvePadding);
}
@@ -23,156 +23,54 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.mantle.MantleWriter;
import art.arcane.iris.engine.object.FloatingIslandSample;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.IrisFloatingChildBiomes;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.spi.PlatformBlockState;
import org.jetbrains.annotations.Nullable;
public class IslandObjectPlacer implements IObjectPlacer {
public final class IslandObjectPlacer implements IObjectPlacer {
private static final int OVERHANG_RADIUS = 2;
public enum AnchorFace { TOP, BOTTOM }
private static final int OVERHANG_HEIGHT_TOLERANCE = 4;
private final MantleWriter wrapped;
private final FloatingIslandSample[] samples;
private final boolean[] overhangAllowed;
private final int minX;
private final int minZ;
private final int chunkMaxIslandTopY;
private final int chunkMinIslandBottomY;
private final SampleProvider samples;
private final IrisFloatingChildBiomes entry;
private final int anchorY;
private final AnchorFace face;
public IslandObjectPlacer(MantleWriter wrapped, FloatingIslandSample[] samples, int minX, int minZ, int anchorTopY) {
this(wrapped, samples, minX, minZ, anchorTopY, AnchorFace.TOP);
}
public IslandObjectPlacer(MantleWriter wrapped, FloatingIslandSample[] samples, int minX, int minZ, int anchorY, AnchorFace face) {
private IslandObjectPlacer(MantleWriter wrapped, AnchorSettings settings) {
this.wrapped = wrapped;
this.samples = samples;
this.minX = minX;
this.minZ = minZ;
this.anchorY = anchorY;
this.face = face;
int maxTopY = -1;
int minBottomY = Integer.MAX_VALUE;
for (FloatingIslandSample s : samples) {
if (s != null) {
int ty = s.topY();
if (ty > maxTopY) {
maxTopY = ty;
}
int by = s.bottomY();
if (by >= 0 && by < minBottomY) {
minBottomY = by;
}
}
}
this.chunkMaxIslandTopY = maxTopY;
this.chunkMinIslandBottomY = (minBottomY == Integer.MAX_VALUE) ? -1 : minBottomY;
this.overhangAllowed = buildOverhangMask(samples);
this.samples = settings.samples();
this.entry = settings.entry();
this.anchorY = settings.anchorY();
this.face = settings.face();
}
private static boolean[] buildOverhangMask(FloatingIslandSample[] samples) {
boolean[] mask = new boolean[256];
for (int zf = 0; zf < 16; zf++) {
for (int xf = 0; xf < 16; xf++) {
int idx = (zf << 4) | xf;
if (samples[idx] != null) {
mask[idx] = true;
continue;
}
boolean touchedEdge = false;
boolean found = false;
for (int dz = -OVERHANG_RADIUS; dz <= OVERHANG_RADIUS && !found; dz++) {
int nzf = zf + dz;
for (int dx = -OVERHANG_RADIUS; dx <= OVERHANG_RADIUS; dx++) {
int nxf = xf + dx;
if (nxf < 0 || nxf >= 16 || nzf < 0 || nzf >= 16) {
touchedEdge = true;
continue;
}
if (samples[(nzf << 4) | nxf] != null) {
found = true;
break;
}
}
}
mask[idx] = found || touchedEdge;
}
}
return mask;
public static IslandObjectPlacer top(MantleWriter wrapped, SampleProvider samples,
IrisFloatingChildBiomes entry, int anchorY) {
return new IslandObjectPlacer(wrapped, new AnchorSettings(samples, entry, anchorY, AnchorFace.TOP));
}
private boolean shouldSkipAirColumn(int x, int y, int z) {
int xf = x - minX;
int zf = z - minZ;
if (xf >= 0 && xf < 16 && zf >= 0 && zf < 16) {
int idx = (zf << 4) | xf;
if (samples[idx] != null) {
if (face == AnchorFace.TOP) {
return false;
}
if (y >= anchorY) {
return true;
}
return false;
}
if (face == AnchorFace.TOP) {
if (y <= anchorY) {
return true;
}
if (!overhangAllowed[idx]) {
return true;
}
} else {
if (y >= anchorY) {
return true;
}
if (!overhangAllowed[idx]) {
return true;
}
}
return false;
}
if (face == AnchorFace.TOP) {
if (y <= anchorY) {
return true;
}
} else {
if (y >= anchorY) {
return true;
}
}
return true;
public static IslandObjectPlacer bottom(MantleWriter wrapped, SampleProvider samples,
IrisFloatingChildBiomes entry, int anchorY) {
return new IslandObjectPlacer(wrapped, new AnchorSettings(samples, entry, anchorY, AnchorFace.BOTTOM));
}
public boolean canWriteObjectBlock(int x, int y, int z) {
return !shouldSkipAirColumn(x, y, z);
}
private @Nullable FloatingIslandSample sampleAt(int x, int z) {
int xf = x - minX;
int zf = z - minZ;
if (xf < 0 || xf >= 16 || zf < 0 || zf >= 16) {
return null;
}
return samples[(zf << 4) | xf];
}
@Override
public int getHighest(int x, int z, IrisData data) {
FloatingIslandSample s = sampleAt(x, z);
FloatingIslandSample sample = samples.sample(x, z);
if (face == AnchorFace.TOP) {
if (s != null) {
return s.topY();
}
return chunkMaxIslandTopY;
return sample == null ? anchorY : sample.topY();
}
if (s != null) {
int by = s.bottomY();
return (by >= 0) ? by : chunkMinIslandBottomY;
if (sample == null) {
return anchorY;
}
return chunkMinIslandBottomY;
int bottomY = sample.bottomY();
return bottomY < 0 ? anchorY : bottomY;
}
@Override
@@ -187,11 +85,11 @@ public class IslandObjectPlacer implements IObjectPlacer {
@Override
public boolean isSolid(int x, int y, int z) {
FloatingIslandSample s = sampleAt(x, z);
if (s != null) {
int idx = y - s.islandBaseY;
if (idx >= 0 && idx < s.solidMask.length) {
return s.solidMask[idx];
FloatingIslandSample sample = samples.sample(x, z);
if (sample != null) {
int index = y - sample.islandBaseY;
if (index >= 0 && index < sample.solidMask.length) {
return sample.solidMask[index];
}
return false;
}
@@ -204,11 +102,10 @@ public class IslandObjectPlacer implements IObjectPlacer {
}
@Override
public void set(int x, int y, int z, PlatformBlockState d) {
if (shouldSkipAirColumn(x, y, z)) {
return;
public void set(int x, int y, int z, PlatformBlockState state) {
if (!shouldSkipAirColumn(x, y, z)) {
wrapped.set(x, y, z, state);
}
wrapped.set(x, y, z, d);
}
@Override
@@ -232,28 +129,87 @@ public class IslandObjectPlacer implements IObjectPlacer {
}
@Override
public void setTile(int xx, int yy, int zz, TileData tile) {
if (shouldSkipAirColumn(xx, yy, zz)) {
return;
public void setTile(int x, int y, int z, TileData tile) {
if (!shouldSkipAirColumn(x, y, z)) {
wrapped.setTile(x, y, z, tile);
}
wrapped.setTile(xx, yy, zz, tile);
}
@Override
public <T> void setData(int xx, int yy, int zz, T data) {
if (shouldSkipAirColumn(xx, yy, zz)) {
return;
public <T> void setData(int x, int y, int z, T data) {
if (!shouldSkipAirColumn(x, y, z)) {
wrapped.setData(x, y, z, data);
}
wrapped.setData(xx, yy, zz, data);
}
@Override
public <T> @Nullable T getData(int xx, int yy, int zz, Class<T> t) {
return wrapped.getData(xx, yy, zz, t);
public <T> @Nullable T getData(int x, int y, int z, Class<T> type) {
return wrapped.getData(x, y, z, type);
}
@Override
public Engine getEngine() {
return wrapped.getEngine();
return wrapped == null ? null : wrapped.getEngine();
}
static boolean matchesAnchor(FloatingIslandSample sample, IrisFloatingChildBiomes entry, AnchorFace face) {
if (sample == null) {
return false;
}
return face == AnchorFace.TOP ? sample.entry == entry : sample.bottomEntry() == entry;
}
private boolean shouldSkipAirColumn(int x, int y, int z) {
Engine engine = getEngine();
if (engine != null && (y < 0 || y >= engine.getHeight())) {
return true;
}
FloatingIslandSample sample = samples.sample(x, z);
if (matchesAnchor(sample, entry, face) && isNearAnchorHeight(sample)) {
return face == AnchorFace.BOTTOM && y >= anchorY;
}
if (face == AnchorFace.TOP && y <= anchorY) {
return true;
}
if (face == AnchorFace.BOTTOM && y >= anchorY) {
return true;
}
return !hasNearbySupport(x, z);
}
private boolean hasNearbySupport(int x, int z) {
for (int dz = -OVERHANG_RADIUS; dz <= OVERHANG_RADIUS; dz++) {
for (int dx = -OVERHANG_RADIUS; dx <= OVERHANG_RADIUS; dx++) {
FloatingIslandSample sample = samples.sample(x + dx, z + dz);
if (matchesAnchor(sample, entry, face) && isNearAnchorHeight(sample)) {
return true;
}
}
}
return false;
}
private boolean isNearAnchorHeight(FloatingIslandSample sample) {
int faceY = face == AnchorFace.TOP ? sample.topY() : sample.bottomY();
return faceY >= 0 && Math.abs(faceY - anchorY) <= OVERHANG_HEIGHT_TOLERANCE;
}
public enum AnchorFace {
TOP,
BOTTOM
}
@FunctionalInterface
public interface SampleProvider {
@Nullable FloatingIslandSample sample(int x, int z);
}
private record AnchorSettings(
SampleProvider samples,
IrisFloatingChildBiomes entry,
int anchorY,
AnchorFace face
) {
}
}
@@ -90,23 +90,29 @@ 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();
for (WeightedProfile weightedProfile : weightedProfiles) {
carveProfile(weightedProfile, writer, x, z, chunkSurfaceHeights);
carveProfile(weightedProfile, writer, x, z, chunkSurfaceHeights, waterSupportPlan);
}
UpperDimensionContext upperCtx = getEngineMantle().getEngine().getUpperContext();
if (upperCtx != null && getDimension().isUpperDimensionCarving()) {
carveUpperTerrain(upperCtx, weightedProfiles, writer, x, z, chunkSurfaceHeights);
carveUpperTerrain(upperCtx, weightedProfiles, writer, x, z, chunkSurfaceHeights, waterSupportPlan);
}
waterSupportPlan.resolve(writer.acquireChunk(x, z));
}
@ChunkCoordinates
private void carveProfile(WeightedProfile weightedProfile, MantleWriter writer, int cx, int cz, int[] chunkSurfaceHeights) {
private void carveProfile(WeightedProfile weightedProfile, MantleWriter writer, int cx, int cz,
int[] chunkSurfaceHeights, IrisCaveCarver3D.WaterSupportPlan waterSupportPlan) {
IrisCaveCarver3D carver = getCarver(weightedProfile.profile);
carver.carve(writer, cx, cz, weightedProfile.columnWeights, MIN_WEIGHT, THRESHOLD_PENALTY, weightedProfile.worldYRange, chunkSurfaceHeights);
carver.carve(writer, cx, cz, weightedProfile.columnWeights, MIN_WEIGHT, THRESHOLD_PENALTY,
weightedProfile.worldYRange, chunkSurfaceHeights, null, waterSupportPlan);
}
private void carveUpperTerrain(UpperDimensionContext upperCtx, List<WeightedProfile> normalProfiles, MantleWriter writer, int cx, int cz, int[] lowerSurfaceHeights) {
private void carveUpperTerrain(UpperDimensionContext upperCtx, List<WeightedProfile> normalProfiles,
MantleWriter writer, int cx, int cz, int[] lowerSurfaceHeights,
IrisCaveCarver3D.WaterSupportPlan waterSupportPlan) {
int chunkHeight = getEngineMantle().getEngine().getHeight();
int worldMinHeight = getEngineMantle().getEngine().getWorld().minHeight();
int gap = getDimension().getUpperDimensionGap();
@@ -154,7 +160,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
}
IrisCaveCarver3D carver = getCarver(weightedProfile.profile);
carver.carve(writer, cx, cz, weightedProfile.columnWeights, MIN_WEIGHT, THRESHOLD_PENALTY,
constrainedRange, ceilingSurfaceHeights, fullVerticalRange);
constrainedRange, ceilingSurfaceHeights, fullVerticalRange, waterSupportPlan);
}
}
@@ -18,8 +18,6 @@
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.data.cache.Cache;
@@ -27,7 +25,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.engine.modifier.IrisFloatingChildBiomeModifier;
import art.arcane.iris.engine.object.FloatingIslandSample;
import art.arcane.iris.engine.object.FloatingObjectFootprint;
import art.arcane.iris.engine.object.IObjectPlacer;
@@ -40,16 +37,17 @@ import art.arcane.iris.engine.object.IrisObjectTranslate;
import art.arcane.iris.engine.object.IrisProceduralBlocks;
import art.arcane.iris.engine.object.ObjectPlaceMode;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.common.math.IrisBlockVector;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.context.ChunkedDataCache;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.spi.PlatformBlockState;
import java.io.File;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Map;
@@ -57,7 +55,6 @@ import java.util.Set;
@ComponentFlag(ReservedFlag.FLOATING_OBJECT)
public class MantleFloatingObjectComponent extends IrisMantleComponent {
private static final int MIN_FOOTPRINT_CELLS_CHECKED = 3;
private static final int INVERTED_PICK_ATTEMPTS = 8;
private static final IrisObjectRotation ROTATION_NONE = IrisObjectRotation.of(0, 0, 0);
@@ -69,12 +66,10 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
IrisComplex complex = context.getComplex();
IrisData data = getData();
int chunkHeight = getEngineMantle().getEngine().getHeight();
int minX = x << 4;
int minZ = z << 4;
long baseSeed = getEngineMantle().getEngine().getSeedManager().getTerrain() ^ IrisFloatingChildBiomeModifier.FLOATING_BASE_SEED_SALT;
RNG chunkRng = new RNG(Cache.key(x, z) + seed() + 0x0FA710BEL);
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
FloatingIslandSampleResolver sampleResolver = new FloatingIslandSampleResolver(getEngineMantle(), context.getFloatingIslandBoundarySampler());
FloatingIslandSample.clearChunkMemo();
@@ -83,11 +78,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
for (int zf = 0; zf < 16; zf++) {
int wx = minX + xf;
int wz = minZ + zf;
IrisBiome parent = biomeCache.get(xf, zf);
if (parent == null || parent.getFloatingChildBiomes() == null || parent.getFloatingChildBiomes().isEmpty()) {
continue;
}
FloatingIslandSample sample = FloatingIslandSample.sampleMemoized(parent, wx, wz, chunkHeight, baseSeed, data, getEngineMantle().getEngine());
FloatingIslandSample sample = sampleResolver.sample(wx, wz);
if (sample != null) {
samples[(zf << 4) | xf] = sample;
}
@@ -115,13 +106,14 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
continue;
}
IrisBiome parent = biomeCache.get(columns.get(0) & 15, columns.get(0) >> 4);
int firstKey = columns.get(0);
IrisBiome parent = sampleResolver.parent(minX + (firstKey & 15), minZ + (firstKey >> 4));
IrisBiome target = entry.getRealBiome(parent, data);
KList<IrisObjectPlacement> floating = entry.getFloatingObjects();
if (floating != null && !floating.isEmpty()) {
for (IrisObjectPlacement placement : floating) {
tryPlaceFloatingChunk(writer, complex, chunkRng, data, placement, samples, columns, minX, minZ, entry);
tryPlaceFloatingChunk(writer, complex, chunkRng, data, placement, columns, minX, minZ, entry);
}
}
@@ -131,15 +123,15 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
boolean hasExtras = extras != null && !extras.isEmpty();
KList<Integer> interior = null;
if (hasSurface || hasExtras) {
interior = interiorColumns(samples, columns);
interior = interiorColumns(sampleResolver, columns, minX, minZ, entry, IslandObjectPlacer.AnchorFace.TOP);
if (hasSurface) {
for (IrisObjectPlacement placement : surface) {
tryPlaceAnchoredChunk(writer, complex, chunkRng, data, placement, samples, columns, interior, minX, minZ, entry);
tryPlaceAnchoredChunk(writer, complex, chunkRng, data, placement, samples, sampleResolver, columns, interior, minX, minZ, entry);
}
}
if (hasExtras) {
for (IrisObjectPlacement placement : extras) {
tryPlaceAnchoredChunk(writer, complex, chunkRng, data, placement, samples, columns, interior, minX, minZ, entry);
tryPlaceAnchoredChunk(writer, complex, chunkRng, data, placement, samples, sampleResolver, columns, interior, minX, minZ, entry);
}
}
}
@@ -152,20 +144,21 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
continue;
}
IrisBiome parent = biomeCache.get(columns.get(0) & 15, columns.get(0) >> 4);
int firstKey = columns.get(0);
IrisBiome parent = sampleResolver.parent(minX + (firstKey & 15), minZ + (firstKey >> 4));
IrisBiome target = entry.getRealBiome(parent, data);
KList<IrisObjectPlacement> bottom = target != null ? entry.resolveBottomObjects(target) : null;
if (bottom != null && !bottom.isEmpty()) {
KList<Integer> interior = interiorColumns(samples, columns);
KList<Integer> interior = interiorColumns(sampleResolver, columns, minX, minZ, entry, IslandObjectPlacer.AnchorFace.BOTTOM);
for (IrisObjectPlacement placement : bottom) {
tryPlaceInvertedChunk(writer, complex, chunkRng, data, placement, samples, columns, interior, minX, minZ, entry);
tryPlaceInvertedChunk(writer, complex, chunkRng, data, placement, samples, sampleResolver, columns, interior, minX, minZ, entry);
}
}
}
}
@ChunkCoordinates
private void tryPlaceFloatingChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, FloatingIslandSample[] samples, KList<Integer> columns, int minX, int minZ, IrisFloatingChildBiomes entry) {
private void tryPlaceFloatingChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, KList<Integer> columns, int minX, int minZ, IrisFloatingChildBiomes entry) {
if (placement == null || columns == null || columns.isEmpty()) {
return;
}
@@ -191,7 +184,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
}
final IrisObject obj = obj0;
int key = columns.get(rng.i(0, columns.size() - 1));
int key = columns.get(rng.i(columns.size()));
int xx = minX + (key & 15);
int zz = minZ + (key >> 4);
IrisObjectPlacement floatingPlacement = placement.toPlacement(obj.getLoadKey());
@@ -211,7 +204,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
}
@ChunkCoordinates
private void tryPlaceAnchoredChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, FloatingIslandSample[] samples, KList<Integer> columns, KList<Integer> interior, int minX, int minZ, IrisFloatingChildBiomes entry) {
private void tryPlaceAnchoredChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, FloatingIslandSample[] samples, IslandObjectPlacer.SampleProvider sampleProvider, KList<Integer> columns, KList<Integer> interior, int minX, int minZ, IrisFloatingChildBiomes entry) {
if (placement == null || columns.isEmpty()) {
return;
}
@@ -243,7 +236,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
KList<Integer> pool = interior.isEmpty() ? columns : interior;
int pickedKey = pool.get(rng.i(0, pool.size() - 1));
int pickedKey = pool.get(rng.i(pool.size()));
int pickedXf = pickedKey & 15;
int pickedZf = pickedKey >> 4;
FloatingIslandSample pickedSample = samples[(pickedZf << 4) | pickedXf];
@@ -252,8 +245,10 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
}
int pickTopY = pickedSample.topY();
if (!isFootprintFlat(fp, pickedXf, pickedZf, pickTopY, samples, 2)) {
if (!isFootprintFlat(fp, pickedXf, pickedZf, pickTopY, samples, 4)) {
int pickedX = minX + pickedXf;
int pickedZ = minZ + pickedZf;
if (!isFootprintFlat(fp, pickedX, pickedZ, pickTopY, sampleProvider, entry, 2)) {
if (!isFootprintFlat(fp, pickedX, pickedZ, pickTopY, sampleProvider, entry, 4)) {
continue;
}
}
@@ -270,26 +265,31 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
int yv = pickTopY + 1 - fp.getLowestSolidKeyY();
IslandObjectPlacer islandPlacer = new IslandObjectPlacer(writer, samples, minX, minZ, pickTopY);
IslandObjectPlacer islandPlacer = IslandObjectPlacer.top(writer, sampleProvider, entry, pickTopY);
FloatingObjectPlacementTransaction transaction = new FloatingObjectPlacementTransaction(islandPlacer);
int id = rng.i(0, Integer.MAX_VALUE);
try {
obj.place(wx, yv, wz, islandPlacer, anchored, rng, (b, bd) -> {
int resultY = obj.place(wx, yv, wz, transaction, anchored, rng, (b, bd) -> {
String marker = placementMarker(obj, id);
if (marker != null
&& islandPlacer.canWriteObjectBlock(b.getX(), b.getY(), b.getZ())
&& shouldWritePlacementMarker(islandPlacer, bd, b.getX(), b.getY(), b.getZ())) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
if (marker != null && shouldWritePlacementMarker(transaction, bd, b.getX(), b.getY(), b.getZ())) {
transaction.setData(b.getX(), b.getY(), b.getZ(), marker);
}
}, null, data);
if (resultY < 0) {
transaction.discard();
} else {
transaction.commit();
}
} catch (Throwable e) {
transaction.discard();
IrisLogging.reportError(e);
}
}
}
@ChunkCoordinates
private void tryPlaceInvertedChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, FloatingIslandSample[] samples, KList<Integer> columns, KList<Integer> interior, int minX, int minZ, IrisFloatingChildBiomes entry) {
private void tryPlaceInvertedChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, FloatingIslandSample[] samples, IslandObjectPlacer.SampleProvider sampleProvider, KList<Integer> columns, KList<Integer> interior, int minX, int minZ, IrisFloatingChildBiomes entry) {
if (placement == null || columns.isEmpty()) {
return;
}
@@ -318,7 +318,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
final IrisObject obj = obj0;
FloatingObjectFootprint fp = FloatingObjectFootprint.compute(obj);
int invertedYRotation = rng.i(0, 3) * 90;
int invertedYRotation = rng.i(4) * 90;
IrisObjectRotation invertedRotation = IrisObjectRotation.xFlip180WithY(invertedYRotation);
KList<Integer> pool = interior.isEmpty() ? columns : interior;
@@ -328,7 +328,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
int pickBottomY = -1;
boolean foundBottomAnchor = false;
for (int attempt = 0; attempt < INVERTED_PICK_ATTEMPTS; attempt++) {
int pickedKey = pool.get(rng.i(0, pool.size() - 1));
int pickedKey = pool.get(rng.i(pool.size()));
int candidateXf = pickedKey & 15;
int candidateZf = pickedKey >> 4;
FloatingIslandSample candidateSample = samples[(candidateZf << 4) | candidateXf];
@@ -339,8 +339,10 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
if (candidateBottomY < 0) {
continue;
}
if (!isFootprintFlatBottom(fp, invertedRotation, candidateXf, candidateZf, candidateBottomY, samples, 2)
&& !isFootprintFlatBottom(fp, invertedRotation, candidateXf, candidateZf, candidateBottomY, samples, 4)) {
int candidateX = minX + candidateXf;
int candidateZ = minZ + candidateZf;
if (!isFootprintFlatBottom(fp, invertedRotation, candidateX, candidateZ, candidateBottomY, sampleProvider, entry, 2)
&& !isFootprintFlatBottom(fp, invertedRotation, candidateX, candidateZ, candidateBottomY, sampleProvider, entry, 4)) {
continue;
}
pickedXf = candidateXf;
@@ -365,54 +367,55 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
int yv = invertedBaseY(pickBottomY, fp, invertedRotation);
IslandObjectPlacer islandPlacer = new IslandObjectPlacer(writer, samples, minX, minZ, pickBottomY, IslandObjectPlacer.AnchorFace.BOTTOM);
IslandObjectPlacer islandPlacer = IslandObjectPlacer.bottom(writer, sampleProvider, entry, pickBottomY);
FloatingObjectPlacementTransaction transaction = new FloatingObjectPlacementTransaction(islandPlacer);
int id = rng.i(0, Integer.MAX_VALUE);
try {
obj.place(wx, yv, wz, islandPlacer, inverted, rng, (b, bd) -> {
int resultY = obj.place(wx, yv, wz, transaction, inverted, rng, (b, bd) -> {
String marker = placementMarker(obj, id);
if (marker != null
&& islandPlacer.canWriteObjectBlock(b.getX(), b.getY(), b.getZ())
&& shouldWritePlacementMarker(islandPlacer, bd, b.getX(), b.getY(), b.getZ())) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
if (marker != null && shouldWritePlacementMarker(transaction, bd, b.getX(), b.getY(), b.getZ())) {
transaction.setData(b.getX(), b.getY(), b.getZ(), marker);
}
}, null, data);
if (resultY < 0) {
transaction.discard();
} else {
transaction.commit();
}
} catch (Throwable e) {
transaction.discard();
IrisLogging.reportError(e);
}
}
}
private static boolean isFootprintFlatBottom(FloatingObjectFootprint fp, IrisObjectRotation rotation, int pickedXf, int pickedZf, int pickBottomY, FloatingIslandSample[] samples, int tolerance) {
static boolean isFootprintFlatBottom(FloatingObjectFootprint fp, IrisObjectRotation rotation,
int pickedX, int pickedZ, int pickBottomY,
IslandObjectPlacer.SampleProvider samples,
IrisFloatingChildBiomes entry, int tolerance) {
IrisBlockVector anchor = invertedFootprintAnchor(fp, rotation);
int checked = 0;
boolean touchedChunkEdge = false;
long[] cells = fp.footprintXZ();
if (cells.length == 0) {
return false;
}
for (int i = 0, n = cells.length; i < n; i++) {
long encoded = cells[i];
int kx = (int) (encoded >> 32);
int kz = (int) (encoded & 0xFFFFFFFFL);
IrisBlockVector cell = rotation.rotate(new IrisBlockVector(kx, 0, kz), 0, 0, 0);
int colXf = pickedXf + cell.getBlockX() - anchor.getBlockX();
int colZf = pickedZf + cell.getBlockZ() - anchor.getBlockZ();
if (colXf < 0 || colXf >= 16 || colZf < 0 || colZf >= 16) {
touchedChunkEdge = true;
continue;
}
FloatingIslandSample s = samples[(colZf << 4) | colXf];
if (s == null) {
int columnX = pickedX + cell.getBlockX() - anchor.getBlockX();
int columnZ = pickedZ + cell.getBlockZ() - anchor.getBlockZ();
FloatingIslandSample sample = samples.sample(columnX, columnZ);
if (!IslandObjectPlacer.matchesAnchor(sample, entry, IslandObjectPlacer.AnchorFace.BOTTOM)) {
return false;
}
int by = s.bottomY();
if (by < 0 || Math.abs(by - pickBottomY) > tolerance) {
int bottomY = sample.bottomY();
if (bottomY < 0 || Math.abs(bottomY - pickBottomY) > tolerance) {
return false;
}
checked++;
}
if (checked >= MIN_FOOTPRINT_CELLS_CHECKED) {
return true;
}
return touchedChunkEdge;
return true;
}
static int invertedBaseX(int minX, int pickedXf, FloatingObjectFootprint fp) {
@@ -458,52 +461,48 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
return state.isCustom() || placesBlock;
}
private static boolean isFootprintFlat(FloatingObjectFootprint fp, int pickedXf, int pickedZf, int pickTopY, FloatingIslandSample[] samples, int tolerance) {
static boolean isFootprintFlat(FloatingObjectFootprint fp, int pickedX, int pickedZ, int pickTopY,
IslandObjectPlacer.SampleProvider samples,
IrisFloatingChildBiomes entry, int tolerance) {
int tallestKx = fp.getTallestKx();
int tallestKz = fp.getTallestKz();
int checked = 0;
boolean touchedChunkEdge = false;
long[] cells = fp.footprintXZ();
if (cells.length == 0) {
return false;
}
for (int i = 0, n = cells.length; i < n; i++) {
long encoded = cells[i];
int kx = (int) (encoded >> 32);
int kz = (int) (encoded & 0xFFFFFFFFL);
int colXf = pickedXf + (kx - tallestKx);
int colZf = pickedZf + (kz - tallestKz);
if (colXf < 0 || colXf >= 16 || colZf < 0 || colZf >= 16) {
touchedChunkEdge = true;
continue;
}
FloatingIslandSample s = samples[(colZf << 4) | colXf];
if (s == null || Math.abs(s.topY() - pickTopY) > tolerance) {
int columnX = pickedX + (kx - tallestKx);
int columnZ = pickedZ + (kz - tallestKz);
FloatingIslandSample sample = samples.sample(columnX, columnZ);
if (!IslandObjectPlacer.matchesAnchor(sample, entry, IslandObjectPlacer.AnchorFace.TOP)
|| Math.abs(sample.topY() - pickTopY) > tolerance) {
return false;
}
checked++;
}
if (checked >= MIN_FOOTPRINT_CELLS_CHECKED) {
return true;
}
return touchedChunkEdge;
return true;
}
private static KList<Integer> interiorColumns(FloatingIslandSample[] samples, KList<Integer> columns) {
private static KList<Integer> interiorColumns(IslandObjectPlacer.SampleProvider samples,
KList<Integer> columns, int minX, int minZ,
IrisFloatingChildBiomes entry,
IslandObjectPlacer.AnchorFace face) {
KList<Integer> interior = new KList<>();
for (int key : columns) {
int xf = key & 15;
int zf = key >> 4;
if (xf <= 0 || xf >= 15 || zf <= 0 || zf >= 15) {
int x = minX + (key & 15);
int z = minZ + (key >> 4);
if (!IslandObjectPlacer.matchesAnchor(samples.sample(x + 1, z), entry, face)) {
continue;
}
if (samples[(zf << 4) | (xf + 1)] == null) {
if (!IslandObjectPlacer.matchesAnchor(samples.sample(x - 1, z), entry, face)) {
continue;
}
if (samples[(zf << 4) | (xf - 1)] == null) {
if (!IslandObjectPlacer.matchesAnchor(samples.sample(x, z + 1), entry, face)) {
continue;
}
if (samples[((zf + 1) << 4) | xf] == null) {
continue;
}
if (samples[((zf - 1) << 4) | xf] == null) {
if (!IslandObjectPlacer.matchesAnchor(samples.sample(x, z - 1), entry, face)) {
continue;
}
interior.add(key);
@@ -537,8 +536,9 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
@Override
protected int computeRadius() {
int maxObjectExtent = 0;
Set<String> objectKeys = new HashSet<>();
int maxObjectExtent = 16;
Map<String, IrisBlockVector> sizeCache = new HashMap<>();
Set<String> warnedLargeObjects = new HashSet<>();
try {
IrisData data = getData();
for (IrisBiome biome : getDimension().getReachableBiomes(this::getData)) {
@@ -547,48 +547,59 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
continue;
}
for (IrisFloatingChildBiomes entry : entries) {
collectPlacementKeys(entry.getFloatingObjects(), objectKeys);
collectPlacementKeys(entry.getExtraObjects(), objectKeys);
collectPlacementKeys(entry.getTopObjectOverrides(), objectKeys);
collectPlacementKeys(entry.getBottomObjectOverrides(), objectKeys);
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getFloatingObjects(), data, sizeCache, warnedLargeObjects));
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getExtraObjects(), data, sizeCache, warnedLargeObjects));
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getTopObjectOverrides(), data, sizeCache, warnedLargeObjects));
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getBottomObjectOverrides(), data, sizeCache, warnedLargeObjects));
try {
IrisBiome target = entry.getRealBiome(biome, data);
if (target != null) {
collectPlacementKeys(entry.resolveTopObjects(target), objectKeys);
collectPlacementKeys(entry.resolveBottomObjects(target), objectKeys);
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.resolveTopObjects(target), data, sizeCache, warnedLargeObjects));
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.resolveBottomObjects(target), data, sizeCache, warnedLargeObjects));
}
} catch (Throwable ignored) {
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
}
for (String key : objectKeys) {
try {
File f = data.getObjectLoader().findFile(key);
if (f == null) {
continue;
}
IrisBlockVector sz = IrisObject.sampleSize(f);
int extent = Math.max(sz.getBlockX(), sz.getBlockZ());
if (extent > maxObjectExtent) {
maxObjectExtent = extent;
}
} catch (Throwable ignored) {
}
}
} catch (Throwable ignored) {
} catch (Throwable e) {
IrisLogging.reportError(e);
}
return Math.max(16, maxObjectExtent);
return maxObjectExtent;
}
private static void collectPlacementKeys(KList<IrisObjectPlacement> placements, Set<String> out) {
private int computePlacementRadius(KList<IrisObjectPlacement> placements, IrisData data,
Map<String, IrisBlockVector> sizeCache,
Set<String> warnedLargeObjects) {
int radius = 0;
if (placements == null) {
return;
return radius;
}
for (IrisObjectPlacement p : placements) {
if (p == null || p.getPlace() == null) {
for (IrisObjectPlacement placement : placements) {
if (placement == null || placement.getPlace() == null) {
continue;
}
out.addAll(p.getPlace());
for (String objectKey : placement.getPlace()) {
try {
IrisBlockVector size = sizeCache.get(objectKey);
if (size == null) {
File file = data.getObjectLoader().findFile(objectKey);
if (file == null) {
continue;
}
size = IrisObject.sampleSize(file);
sizeCache.put(objectKey, size);
}
int reach = MantleObjectComponent.calculatePlacementReach(size, placement);
if (reach > 128 && warnedLargeObjects.add(objectKey)) {
IrisLogging.warn("Floating object " + objectKey + " has a large placement reach (" + reach + " blocks) and may increase memory usage!");
}
radius = Math.max(radius, reach);
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
}
return radius;
}
}
@@ -1,79 +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.mantle.components;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.data.cache.Cache;
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.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisFluidBodies;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import art.arcane.volmlib.util.math.RNG;
@ComponentFlag(ReservedFlag.FLUID_BODIES)
public class MantleFluidBodyComponent extends IrisMantleComponent {
public MantleFluidBodyComponent(EngineMantle engineMantle) {
super(engineMantle, ReservedFlag.FLUID_BODIES, 0);
}
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
IrisComplex complex = context.getComplex();
RNG rng = new RNG(Cache.key(x, z) + seed() + 405666);
int xxx = 8 + (x << 4);
int zzz = 8 + (z << 4);
IrisRegion region = complex.getRegionStream().get(xxx, zzz);
IrisBiome biome = complex.getTrueBiomeStream().get(xxx, zzz);
generate(writer, rng, x, z, region, biome);
}
@ChunkCoordinates
private void generate(MantleWriter writer, RNG rng, int cx, int cz, IrisRegion region, IrisBiome biome) {
generate(getDimension().getFluidBodies(), writer, new RNG((rng.nextLong() * cx) + 490495 + cz), cx, cz);
generate(biome.getFluidBodies(), writer, new RNG((rng.nextLong() * cx) + 490495 + cz), cx, cz);
generate(region.getFluidBodies(), writer, new RNG((rng.nextLong() * cx) + 490495 + cz), cx, cz);
}
@ChunkCoordinates
private void generate(IrisFluidBodies bodies, MantleWriter writer, RNG rng, int cx, int cz) {
bodies.generate(writer, rng, getEngineMantle().getEngine(), cx << 4, -1, cz << 4);
}
protected int computeRadius() {
int max = 0;
max = Math.max(max, getDimension().getFluidBodies().getMaxRange(getData()));
for (IrisRegion i : getDimension().getAllRegions(this::getData)) {
max = Math.max(max, i.getFluidBodies().getMaxRange(getData()));
}
for (IrisBiome i : getDimension().getReachableBiomes(this::getData)) {
max = Math.max(max, i.getFluidBodies().getMaxRange(getData()));
}
return max;
}
}
@@ -41,7 +41,7 @@ import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.engine.object.IrisObjectScale;
import art.arcane.iris.engine.object.IrisObjectTranslate;
import art.arcane.iris.engine.object.IrisObjectVacuum;
import art.arcane.iris.engine.object.IrisProceduralObjects;
import art.arcane.iris.engine.object.IrisProceduralPlacement;
@@ -55,7 +55,6 @@ import art.arcane.volmlib.util.collection.KSet;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.MatterStructurePOI;
@@ -70,7 +69,6 @@ import java.io.IOException;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
@@ -413,15 +411,26 @@ public class MantleObjectComponent extends IrisMantleComponent {
@ChunkCoordinates
private void placeProceduralObjects(MantleWriter writer, RNG rng, int x, int z, IrisBiome surfaceBiome, IrisBiome caveBiome, IrisRegion region) {
placeProceduralFrom(writer, rng, x, z, surfaceBiome.getProceduralObjects(), surfaceBiome.getName());
placeProceduralFrom(writer, rng, x, z, region.getProceduralObjects(), region.getName());
IrisCaveProfile surfaceCaveProfile = resolveCaveProfile(surfaceBiome.getCaveProfile(), region.getCaveProfile());
IrisCaveProfile regionCaveProfile = resolveCaveProfile(region.getCaveProfile(), caveBiome == null ? null : caveBiome.getCaveProfile());
placeProceduralFrom(writer, rng, x, z, surfaceBiome.getProceduralObjects(), surfaceBiome.getName(), surfaceCaveProfile);
placeProceduralFrom(writer, rng, x, z, region.getProceduralObjects(), region.getName(), regionCaveProfile);
if (caveBiome != null && caveBiome != surfaceBiome) {
placeProceduralFrom(writer, rng, x, z, caveBiome.getProceduralObjects(), caveBiome.getName());
IrisCaveProfile caveProfile = resolveCaveProfile(caveBiome.getCaveProfile(), region.getCaveProfile());
placeProceduralFrom(writer, rng, x, z, caveBiome.getProceduralObjects(), caveBiome.getName(), caveProfile);
}
}
@ChunkCoordinates
private void placeProceduralFrom(MantleWriter writer, RNG rng, int x, int z, IrisProceduralObjects proceduralObjects, String scope) {
private void placeProceduralFrom(
MantleWriter writer,
RNG rng,
int x,
int z,
IrisProceduralObjects proceduralObjects,
String scope,
IrisCaveProfile caveProfile
) {
if (proceduralObjects == null || proceduralObjects.isEmpty()) {
return;
}
@@ -444,13 +453,15 @@ public class MantleObjectComponent extends IrisMantleComponent {
IrisObjectPlacement placement = p.asPlacement();
boolean carving = placement.getCarvingSupport() == CarvingMode.CARVING_ONLY;
if (carving && placement.getMode() == ObjectPlaceMode.CENTER_HEIGHT) {
placement.setMode(ObjectPlaceMode.FAST_MIN_HEIGHT);
}
IObjectPlacer placer = p.isPlausible() ? new DecayControlPlacer(writer) : writer;
if (golden) {
placer = new GoldenDebugPlacer(placer, scope + "/" + p.getName());
IrisCaveAnchorMode anchorMode = resolveAnchorMode(placement, caveProfile);
if (placement.getMode() == ObjectPlaceMode.CEILING_HANG) {
anchorMode = IrisCaveAnchorMode.CEILING;
}
int anchorScanStep = resolveAnchorScanStep(caveProfile);
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;
int density = Math.max(1, p.getDensity());
for (int i = 0; i < density; i++) {
IrisObject variant = p.getVariantObject(getData(), rng);
@@ -463,8 +474,31 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
continue;
}
int xx = rng.i(blockX, blockX + 15);
int zz = rng.i(blockZ, blockZ + 15);
CavePlacementAnchor caveAnchor = null;
if (carving) {
caveAnchor = findCavePlacementAnchor(
writer,
rng,
blockX,
blockZ,
anchorMode,
anchorScanStep,
minDepthBelowSurface,
anchorSearchAttempts,
null,
caveAnchorCache
);
}
if (carving && caveAnchor == null) {
if (golden) {
IrisLogging.info("Goldendebug procedural cave anchor rejected: chunk=" + x + "," + z
+ " placement=" + p.getName()
+ " minDepthBelowSurface=" + minDepthBelowSurface);
}
continue;
}
int xx = caveAnchor == null ? rng.i(blockX, blockX + 16) : caveAnchor.x();
int zz = caveAnchor == null ? rng.i(blockZ, blockZ + 16) : caveAnchor.z();
int id = rng.i(0, Integer.MAX_VALUE);
if (golden) {
KList<IrisObject> pool = p.getVariantObjects(getData());
@@ -480,8 +514,9 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
try {
int placeResult = -1;
CaveObjectPlacementTransaction.CommitResult commitResult = CaveObjectPlacementTransaction.CommitResult.EMPTY;
if (carving) {
int caveFloorY = findNearestCaveFloor(writer, xx, zz, caveAnchorCache);
int caveFloorY = caveAnchor.y();
if (golden) {
IrisLogging.info("Goldendebug procedural caveFloor: chunk=" + x + "," + z
+ " placement=" + p.getName()
@@ -489,19 +524,26 @@ public class MantleObjectComponent extends IrisMantleComponent {
+ " zz=" + zz
+ " caveFloorY=" + caveFloorY);
}
if (caveFloorY > 0) {
placeResult = variant.place(xx, caveFloorY, zz, placer, placement, rng, (b, data) -> {
String marker = placementMarker(variant, id, "procedural");
if (marker != null) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
}
}, null, getData());
}
IrisObjectPlacement effectivePlacement = resolveCavePlacement(placement, variant, caveProfile);
ContainedPlacementResult contained = placeContainedCaveObject(
placer,
variant,
xx,
caveFloorY,
zz,
effectivePlacement,
minDepthBelowSurface,
id,
"procedural",
rng
);
placeResult = contained.resultY();
commitResult = contained.commitResult();
} else {
placeResult = variant.place(xx, -1, zz, placer, placement, rng, (b, data) -> {
String marker = placementMarker(variant, id, "procedural");
if (marker != null) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
placer.setData(b.getX(), b.getY(), b.getZ(), marker);
}
}, null, getData());
}
@@ -511,7 +553,8 @@ public class MantleObjectComponent extends IrisMantleComponent {
+ " variant=" + variant.getLoadKey()
+ " xx=" + xx
+ " zz=" + zz
+ " resultY=" + placeResult);
+ " resultY=" + placeResult
+ " commit=" + commitResult);
}
} catch (Throwable e) {
IrisLogging.reportError(e);
@@ -522,6 +565,72 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
}
private CavePlacementAnchor findCavePlacementAnchor(
MantleWriter writer,
RNG rng,
int minX,
int minZ,
IrisCaveAnchorMode anchorMode,
int anchorScanStep,
int minDepthBelowSurface,
int searchAttempts,
String expectedCaveBiomeKey,
CaveAnchorCache anchorCache
) {
for (int search = 0; search < searchAttempts; search++) {
int candidateX = rng.i(minX, minX + 16);
int candidateZ = rng.i(minZ, minZ + 16);
int candidateY = findCaveAnchorY(
writer,
rng,
candidateX,
candidateZ,
anchorMode,
anchorScanStep,
minDepthBelowSurface,
anchorCache
);
if (candidateY < 0 || caveAnchorBiomeConflicts(candidateX, candidateY, candidateZ, expectedCaveBiomeKey)) {
continue;
}
return new CavePlacementAnchor(candidateX, candidateY, candidateZ);
}
return null;
}
private ContainedPlacementResult placeContainedCaveObject(
IObjectPlacer placer,
IrisObject object,
int x,
int anchorY,
int z,
IrisObjectPlacement placement,
int minDepthBelowSurface,
int id,
String markerContext,
RNG rng
) {
CaveObjectPlacementTransaction transaction = new CaveObjectPlacementTransaction(placer, anchorY, minDepthBelowSurface);
int placeY = anchorY;
if (placement.getMode() == ObjectPlaceMode.CEILING_HANG) {
placeY = Math.max(1, transaction.getCaveCeiling(x, z) - 1 - Math.floorDiv(object.getH(), 2));
}
String marker = placementMarker(object, id, markerContext);
int result = object.place(x, placeY, z, transaction, placement, rng, (block, data) -> {
if (marker != null) {
transaction.setData(block.getX(), block.getY(), block.getZ(), marker);
}
if (placement.isDolphinTarget() && placement.isUnderwater() && B.isStorageChest(data)) {
transaction.setData(block.getX(), block.getY(), block.getZ(), MatterStructurePOI.BURIED_TREASURE);
}
}, null, getData());
if (result < 0) {
transaction.discard();
return new ContainedPlacementResult(result, CaveObjectPlacementTransaction.CommitResult.EMPTY);
}
return new ContainedPlacementResult(result, transaction.commit());
}
@BlockCoordinates
private ObjectPlacementResult placeObject(
MantleWriter writer,
@@ -557,8 +666,8 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
continue;
}
int xx = rng.i(x, x + 15);
int zz = rng.i(z, z + 15);
int xx = rng.i(x, x + 16);
int zz = rng.i(z, z + 16);
IrisObjectPlacement effectivePlacement = resolveEffectivePlacement(objectPlacement, v);
int id = rng.i(0, Integer.MAX_VALUE);
IObjectPlacer placePlacer = golden ? new GoldenDebugPlacer(writer, scope + "/" + v.getLoadKey()) : writer;
@@ -705,28 +814,20 @@ public class MantleObjectComponent extends IrisMantleComponent {
continue;
}
int x = 0;
int z = 0;
int y = -1;
for (int search = 0; search < anchorSearchAttempts; search++) {
int candidateX = rng.i(minX, minX + 15);
int candidateZ = rng.i(minZ, minZ + 15);
int candidateY = findCaveAnchorY(writer, rng, candidateX, candidateZ, anchorMode, anchorScanStep, objectMinDepthBelowSurface, anchorCache);
if (candidateY < 0) {
continue;
}
CavePlacementAnchor anchor = findCavePlacementAnchor(
writer,
rng,
minX,
minZ,
anchorMode,
anchorScanStep,
objectMinDepthBelowSurface,
anchorSearchAttempts,
expectedCaveBiomeKey,
anchorCache
);
if (caveAnchorBiomeConflicts(candidateX, candidateY, candidateZ, expectedCaveBiomeKey)) {
continue;
}
x = candidateX;
z = candidateZ;
y = candidateY;
break;
}
if (y < 0) {
if (anchor == null) {
rejected++;
logCaveReject(
scope,
@@ -747,44 +848,43 @@ public class MantleObjectComponent extends IrisMantleComponent {
continue;
}
int x = anchor.x();
int y = anchor.y();
int z = anchor.z();
int id = rng.i(0, Integer.MAX_VALUE);
IrisObjectPlacement resolvedPlacement = resolveEffectivePlacement(objectPlacement, object);
if (resolvedPlacement.getMode() == ObjectPlaceMode.CENTER_HEIGHT && caveProfile != null) {
ObjectPlaceMode profileMode = caveProfile.getDefaultObjectPlaceMode();
if (profileMode != null) {
resolvedPlacement = resolvedPlacement.toPlacement(object.getLoadKey());
resolvedPlacement.setMode(profileMode);
}
}
IrisObjectPlacement effectivePlacement = resolvedPlacement;
AtomicBoolean wrotePlacementData = new AtomicBoolean(false);
IrisObjectPlacement effectivePlacement = resolveCavePlacement(objectPlacement, object, caveProfile);
try {
int caveCeiling = findCaveCeiling(writer, x, y, z);
IObjectPlacer clampedPlacer = new CeilingClampedPlacer(writer, caveCeiling);
int placeY = y;
if (effectivePlacement.getMode() == ObjectPlaceMode.CEILING_HANG) {
placeY = Math.max(1, caveCeiling - 1 - Math.floorDiv(object.getH(), 2));
}
int result = object.place(x, placeY, z, clampedPlacer, effectivePlacement, rng, (b, data) -> {
wrotePlacementData.set(true);
String marker = placementMarker(object, id, "cave");
if (marker != null) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
}
if (effectivePlacement.isDolphinTarget() && effectivePlacement.isUnderwater() && B.isStorageChest(data)) {
writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE);
}
}, null, getData());
boolean wroteBlocks = wrotePlacementData.get();
ContainedPlacementResult contained = placeContainedCaveObject(
writer,
object,
x,
y,
z,
effectivePlacement,
objectMinDepthBelowSurface,
id,
"cave",
rng
);
int result = contained.resultY();
boolean wroteBlocks = contained.commitResult() == CaveObjectPlacementTransaction.CommitResult.COMMITTED;
if (wroteBlocks) {
placed++;
} else if (result < 0) {
} else {
rejected++;
String rejectReason;
if (result < 0) {
rejectReason = "PLACE_NEGATIVE";
} else if (contained.commitResult() == CaveObjectPlacementTransaction.CommitResult.REJECTED_BOUNDS) {
rejectReason = "CONTAINMENT";
} else {
rejectReason = "NO_WRITES";
}
logCaveReject(
scope,
"PLACE_NEGATIVE",
rejectReason,
metricChunkX,
metricChunkZ,
objectPlacement,
@@ -923,8 +1023,8 @@ public class MantleObjectComponent extends IrisMantleComponent {
continue;
}
int xx = rng.i(minX, minX + 15);
int zz = rng.i(minZ, minZ + 15);
int xx = rng.i(minX, minX + 16);
int zz = rng.i(minZ, minZ + 16);
int columnLowerSurfaceY = getEngineMantle().getEngine().getHeight(xx, zz, true);
int rawUpperSurface = upperCtx.getUpperSurfaceY(xx, zz);
int upperSurfaceY = Math.max(rawUpperSurface, columnLowerSurfaceY + upperGap);
@@ -1069,24 +1169,33 @@ public class MantleObjectComponent extends IrisMantleComponent {
return effectivePlacement;
}
private int findNearestCaveFloor(MantleWriter writer, int x, int z, CaveAnchorCache anchorCache) {
KList<Integer> anchors = anchorCache.get(writer, IrisCaveAnchorMode.FLOOR, 1, 0, x, z);
if (anchors.isEmpty()) {
return -1;
private IrisObjectPlacement resolveCavePlacement(IrisObjectPlacement objectPlacement, IrisObject object, IrisCaveProfile caveProfile) {
IrisObjectPlacement resolvedPlacement = resolveEffectivePlacement(objectPlacement, object);
if (resolvedPlacement.getMode() != ObjectPlaceMode.CENTER_HEIGHT || caveProfile == null) {
return resolvedPlacement;
}
return anchors.get(anchors.size() - 1);
ObjectPlaceMode profileMode = caveProfile.getDefaultObjectPlaceMode();
if (profileMode == null) {
return resolvedPlacement;
}
String loadKey = object.getLoadKey();
if (loadKey == null || loadKey.isBlank()) {
resolvedPlacement = resolvedPlacement.toPlacement();
} else {
resolvedPlacement = resolvedPlacement.toPlacement(loadKey);
}
resolvedPlacement.setMode(profileMode);
return resolvedPlacement;
}
private int findCaveCeiling(MantleWriter writer, int x, int anchorY, int z) {
Engine engine = getEngineMantle().getEngine();
int surfaceY = engine.getHeight(x, z);
int maxScan = Math.min(engine.getHeight() - 1, Math.max(0, surfaceY));
for (int sy = anchorY + 1; sy <= maxScan; sy++) {
if (!writer.isCarved(x, sy, z)) {
return sy;
}
static int caveAnchorScanUpperBound(int worldHeight, int surfaceY, int minDepthBelowSurface) {
int maxAnchorY = CaveObjectPlacementTransaction.maxBuriedY(worldHeight, surfaceY, minDepthBelowSurface);
if (maxAnchorY <= 1) {
return 0;
}
return maxScan;
return maxAnchorY + 1;
}
private static final class GoldenDebugPlacer implements IObjectPlacer {
@@ -1177,92 +1286,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
}
private static final class CeilingClampedPlacer implements IObjectPlacer {
private final IObjectPlacer delegate;
private final int maxY;
private CeilingClampedPlacer(IObjectPlacer delegate, int maxY) {
this.delegate = delegate;
this.maxY = maxY;
}
@Override
public int getHighest(int x, int z, IrisData data) {
return delegate.getHighest(x, z, data);
}
@Override
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
return delegate.getHighest(x, z, data, ignoreFluid);
}
@Override
public void set(int x, int y, int z, PlatformBlockState d) {
if (y >= maxY) {
return;
}
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) {
return delegate.isCarved(x, y, z);
}
@Override
public boolean isSolid(int x, int y, int z) {
return delegate.isSolid(x, y, z);
}
@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) {
if (yy >= maxY) {
return;
}
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()) {
@@ -1273,35 +1296,21 @@ public class MantleObjectComponent extends IrisMantleComponent {
return anchors.get(0);
}
return anchors.get(rng.i(0, anchors.size() - 1));
return anchors.get(rng.i(anchors.size()));
}
private KList<Integer> scanCaveAnchorColumn(MantleWriter writer, IrisCaveAnchorMode anchorMode, int anchorScanStep, int objectMinDepthBelowSurface, int x, int z, CaveAnchorCache anchorCache) {
int height = getEngineMantle().getEngine().getHeight();
int step = Math.max(1, anchorScanStep);
int surfaceY = anchorCache.getSurfaceHeight(x, z);
int baseMaxAnchorY = Math.min(height - 1, surfaceY - Math.max(0, objectMinDepthBelowSurface));
if (baseMaxAnchorY <= 1) {
int maxAnchorExclusive = caveAnchorScanUpperBound(height, surfaceY, objectMinDepthBelowSurface);
if (maxAnchorExclusive == 0) {
return new KList<>();
}
int widenedMaxAnchorY = Math.min(height - 1, surfaceY - 3);
widenedMaxAnchorY = Math.min(widenedMaxAnchorY, baseMaxAnchorY + Math.max(0, objectMinDepthBelowSurface) / 2);
int carvedHeight = Math.min(height, Math.max(baseMaxAnchorY, widenedMaxAnchorY) + 4);
int carvedHeight = Math.min(height, maxAnchorExclusive + 3);
byte[] carvedColumn = anchorCache.getCarvedColumn(writer, x, z, carvedHeight);
KList<Integer> anchors = scanCaveAnchorRange(anchorMode, step, carvedHeight, BEDROCK_CLEARANCE, baseMaxAnchorY, carvedColumn);
if (!anchors.isEmpty()) {
return anchors;
}
if (widenedMaxAnchorY > baseMaxAnchorY) {
anchors = scanCaveAnchorRange(anchorMode, step, carvedHeight, baseMaxAnchorY, widenedMaxAnchorY, carvedColumn);
if (!anchors.isEmpty()) {
return anchors;
}
}
return anchors;
return scanCaveAnchorRange(anchorMode, step, carvedHeight, BEDROCK_CLEARANCE, maxAnchorExclusive, carvedColumn);
}
private KList<Integer> scanCaveAnchorRange(IrisCaveAnchorMode anchorMode, int step, int height, int minAnchorY, int maxAnchorY, byte[] carvedColumn) {
@@ -1441,6 +1450,15 @@ public class MantleObjectComponent extends IrisMantleComponent {
private record ObjectPlacementResult(int attempts, int placed, int rejected, int nullObjects, int errors) {
}
private record CavePlacementAnchor(int x, int y, int z) {
}
private record ContainedPlacementResult(
int resultY,
CaveObjectPlacementTransaction.CommitResult commitResult
) {
}
private static final class CaveRejectLogState {
private final AtomicLong lastLogMs = new AtomicLong(0L);
private final AtomicInteger suppressed = new AtomicInteger(0);
@@ -1484,63 +1502,61 @@ public class MantleObjectComponent extends IrisMantleComponent {
protected int computeRadius() {
IrisDimension dimension = getDimension();
AtomicInteger xg = new AtomicInteger();
AtomicInteger zg = new AtomicInteger();
KSet<String> objects = new KSet<>();
KMap<IrisObjectScale, KList<String>> scalars = new KMap<>();
KList<IrisObjectPlacement> vacuumPlacements = new KList<>();
KMap<String, IrisBlockVector> sizeCache = new KMap<>();
KSet<String> warnedLargeObjects = new KSet<>();
int radius = 0;
for (IrisRegion region : dimension.getAllRegions(this::getData)) {
for (IrisObjectPlacement j : region.getObjects()) {
if (j.getScale().canScaleBeyond()) {
scalars.put(j.getScale(), j.getPlace());
} else {
objects.addAll(j.getPlace());
}
if (IrisObjectVacuum.isVacuumMode(j.getMode())) {
vacuumPlacements.add(j);
}
if (region == null) {
continue;
}
updateProceduralRadiusBounds(region.getProceduralObjects(), xg, zg);
radius = Math.max(radius, computePlacementRadius(region.getObjects(), sizeCache, warnedLargeObjects));
radius = Math.max(radius, computeProceduralRadius(region.getProceduralObjects()));
}
for (IrisBiome biome : dimension.getReachableBiomes(this::getData)) {
updateProceduralRadiusBounds(biome.getProceduralObjects(), xg, zg);
for (IrisObjectPlacement j : biome.getObjects()) {
if (j.getScale().canScaleBeyond()) {
scalars.put(j.getScale(), j.getPlace());
} else {
objects.addAll(j.getPlace());
}
if (IrisObjectVacuum.isVacuumMode(j.getMode())) {
vacuumPlacements.add(j);
}
if (biome == null) {
continue;
}
radius = Math.max(radius, computePlacementRadius(biome.getObjects(), sizeCache, warnedLargeObjects));
radius = Math.max(radius, computeProceduralRadius(biome.getProceduralObjects()));
}
KMap<String, IrisBlockVector> sizeCache = new KMap<>();
for (String i : objects) {
updateRadiusBounds(sizeCache, xg, zg, i, 1D);
}
for (Map.Entry<IrisObjectScale, KList<String>> entry : scalars.entrySet()) {
double ms = entry.getKey().getMaxScale();
for (String j : entry.getValue()) {
updateRadiusBounds(sizeCache, xg, zg, j, ms);
}
}
for (IrisObjectPlacement j : vacuumPlacements) {
updateVacuumRadiusBounds(sizeCache, xg, zg, j);
}
return Math.max(xg.get(), zg.get());
return radius;
}
private void updateProceduralRadiusBounds(IrisProceduralObjects procedural, AtomicInteger xg, AtomicInteger zg) {
if (procedural == null || procedural.isEmpty()) {
return;
private int computePlacementRadius(
KList<IrisObjectPlacement> placements,
KMap<String, IrisBlockVector> sizeCache,
KSet<String> warnedLargeObjects
) {
int radius = 0;
for (IrisObjectPlacement placement : placements) {
if (placement == null) {
continue;
}
for (String objectKey : placement.getPlace()) {
try {
IrisBlockVector size = loadObjectSize(sizeCache, objectKey);
if (size == null) {
continue;
}
int reach = calculatePlacementReach(size, placement);
if (reach > 128 && warnedLargeObjects.add(objectKey)) {
IrisLogging.warn("Object " + objectKey + " has a large placement reach (" + reach + " blocks) and may increase memory usage!");
}
radius = Math.max(radius, reach);
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
}
return radius;
}
private int computeProceduralRadius(IrisProceduralObjects procedural) {
if (procedural == null || procedural.isEmpty()) {
return 0;
}
int radius = 0;
for (IrisProceduralPlacement placement : procedural.getAllPlacements()) {
if (placement == null) {
continue;
@@ -1549,71 +1565,65 @@ public class MantleObjectComponent extends IrisMantleComponent {
if (variants == null) {
continue;
}
IrisObjectPlacement objectPlacement = placement.asPlacement();
for (IrisObject variant : variants) {
if (variant == null) {
continue;
}
xg.getAndSet(Math.max(variant.getW(), xg.get()));
zg.getAndSet(Math.max(variant.getD(), zg.get()));
IrisBlockVector size = new IrisBlockVector(variant.getW(), variant.getH(), variant.getD());
radius = Math.max(radius, calculatePlacementReach(size, objectPlacement));
}
}
return radius;
}
private void updateRadiusBounds(
KMap<String, IrisBlockVector> sizeCache,
AtomicInteger xg,
AtomicInteger zg,
String objectKey,
double scale
) {
try {
IrisBlockVector bv = loadObjectSize(sizeCache, objectKey);
if (bv == null) {
throw new RuntimeException();
}
if (Math.max(bv.getBlockX(), bv.getBlockZ()) > 128) {
if (scale > 1D) {
IrisLogging.warn("Object " + objectKey + " has a large size (" + bv + ") and may increase memory usage! (Object scaled up to " + Form.pc(scale, 2) + ")");
} else {
IrisLogging.warn("Object " + objectKey + " has a large size (" + bv + ") and may increase memory usage!");
}
}
xg.getAndSet(Math.max((int) Math.ceil(bv.getBlockX() * scale), xg.get()));
zg.getAndSet(Math.max((int) Math.ceil(bv.getBlockZ() * scale), zg.get()));
} catch (Throwable e) {
IrisLogging.reportError(e);
static int calculatePlacementReach(IrisBlockVector size, IrisObjectPlacement placement) {
if (size == null) {
return 0;
}
}
private void updateVacuumRadiusBounds(
KMap<String, IrisBlockVector> sizeCache,
AtomicInteger xg,
AtomicInteger zg,
IrisObjectPlacement placement
) {
int pad = 2 * IrisObjectVacuum.resolveRadius(placement.getMode(), placement.getVacuumSettings());
if (pad <= 0) {
return;
if (placement == null) {
return Math.max(Math.abs(size.getBlockX()), Math.abs(size.getBlockZ()));
}
double scale = placement.getScale() != null ? Math.max(1D, placement.getScale().getMaxScale()) : 1D;
for (String objectKey : placement.getPlace()) {
try {
IrisBlockVector bv = loadObjectSize(sizeCache, objectKey);
if (bv == null) {
continue;
}
int width = scaledDimension(size.getBlockX(), scale);
int height = scaledDimension(size.getBlockY(), scale);
int depth = scaledDimension(size.getBlockZ(), scale);
IrisObjectRotation rotation = placement.getRotation();
boolean rotateX = rotation != null && rotation.isEnabled() && rotation.getXAxis() != null && rotation.getXAxis().isEnabled();
boolean rotateZ = rotation != null && rotation.isEnabled() && rotation.getZAxis() != null && rotation.getZAxis().isEnabled();
int footprint = rotateX || rotateZ ? Math.max(width, Math.max(height, depth)) : Math.max(width, depth);
int translation = calculateTranslationReach(placement.getTranslate(), rotation, rotateX || rotateZ);
int warp = placement.getWarp() != null && !placement.getWarp().isFlat()
? (int) Math.ceil(Math.abs(placement.getWarp().getMultiplier()) / 2D)
: 0;
int vacuum = IrisObjectVacuum.isVacuumMode(placement.getMode())
? 2 * IrisObjectVacuum.resolveRadius(placement.getMode(), placement.getVacuumSettings())
: 0;
long reach = (long) footprint + translation + warp + vacuum;
return reach > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) reach;
}
int reachX = (int) Math.ceil(Math.abs(bv.getBlockX()) * scale) + pad;
int reachZ = (int) Math.ceil(Math.abs(bv.getBlockZ()) * scale) + pad;
xg.getAndSet(Math.max(reachX, xg.get()));
zg.getAndSet(Math.max(reachZ, zg.get()));
} catch (Throwable e) {
IrisLogging.reportError(e);
}
private static int scaledDimension(int dimension, double scale) {
int absoluteDimension = Math.abs(dimension);
if (scale <= 1D) {
return absoluteDimension;
}
return (int) Math.ceil((absoluteDimension * scale) + (scale * 2D));
}
private static int calculateTranslationReach(IrisObjectTranslate translate, IrisObjectRotation rotation, boolean rotateVertically) {
if (translate == null || !translate.canTranslate()) {
return 0;
}
double x = translate.getX();
double y = translate.getY();
double z = translate.getZ();
if (rotateVertically) {
return (int) Math.ceil(Math.sqrt((x * x) + (y * y) + (z * z)));
}
boolean rotateY = rotation != null && rotation.isEnabled() && rotation.getYAxis() != null && rotation.getYAxis().isEnabled();
return rotateY ? (int) Math.ceil(Math.hypot(x, z)) : (int) Math.max(Math.abs(x), Math.abs(z));
}
private IrisBlockVector loadObjectSize(KMap<String, IrisBlockVector> sizeCache, String objectKey) {
@@ -0,0 +1,73 @@
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.spi.PlatformBlockState;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import java.util.function.IntConsumer;
import java.util.function.IntPredicate;
final class StructureFoundationPlanner {
static final int NO_GROUND = Integer.MIN_VALUE;
private StructureFoundationPlanner() {
}
static void recordBaseCell(Long2IntOpenHashMap columns, int x, int y, int z,
PlatformBlockState state) {
if (columns == null || state == null || !state.isOccluding()) {
return;
}
long columnKey = pack(x, z);
if (!columns.containsKey(columnKey) || y < columns.get(columnKey)) {
columns.put(columnKey, y);
}
}
static int findGroundY(int foundationY, int maxDepth, int minimumY, IntPredicate solidAtY) {
if (foundationY <= minimumY) {
return NO_GROUND;
}
long requestedFloor = (long) foundationY - Math.max(1, maxDepth);
int scanFloor = (int) Math.max(minimumY, requestedFloor);
for (int y = foundationY - 1; y >= scanFloor; y--) {
if (solidAtY.test(y)) {
return y;
}
}
return NO_GROUND;
}
static boolean isGroundSolid(PlatformBlockState overlay, boolean carved, int mantleY, int terrainHeight) {
if (mantleY <= 0) {
return true;
}
if (overlay != null) {
return overlay.isSolid();
}
return !carved && mantleY <= terrainHeight;
}
static int fillSupportColumn(int foundationY, int groundY, IntConsumer supportAtY) {
if (groundY == NO_GROUND || groundY >= foundationY - 1) {
return 0;
}
int written = 0;
for (int y = foundationY - 1; y > groundY; y--) {
supportAtY.accept(y);
written++;
}
return written;
}
static long pack(int x, int z) {
return ((long) x << 32) | (z & 0xffffffffL);
}
static int unpackX(long packed) {
return (int) (packed >> 32);
}
static int unpackZ(long packed) {
return (int) packed;
}
}
@@ -122,8 +122,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
}
PlatformBlockState current = output.getRaw(rx, yy, rz);
boolean explicitCarveIntent = hasExplicitCarveIntent(c);
if (B.isFluid(current)) {
if (shouldPreserveExistingFluid(c, current)) {
return;
}
@@ -133,17 +134,14 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
scratch.customCaveBiomePresent = true;
}
if (current.isAir()) {
if (current.isAir() && !explicitCarveIntent) {
return;
}
if (c.isWater()) {
output.setRaw(rx, yy, rz, context.getFluid().get(rx, rz));
} else if (c.isLava()) {
output.setRaw(rx, yy, rz, LAVA);
} else if (c.getLiquid() == 3) {
output.setRaw(rx, yy, rz, AIR);
} else if (getEngine().getDimension().getCaveLavaHeight() > yy) {
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)) {
output.setRaw(rx, yy, rz, LAVA);
} else {
output.setRaw(rx, yy, rz, AIR);
@@ -200,6 +198,32 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
}
}
static boolean hasExplicitCarveIntent(MatterCavern cavern) {
return cavern != null && (cavern.isWater() || cavern.isLava() || cavern.getLiquid() == 3);
}
static boolean shouldPreserveExistingFluid(MatterCavern cavern, PlatformBlockState current) {
return B.isFluid(current) && !hasExplicitCarveIntent(cavern);
}
static boolean usesDefaultLava(int caveLavaHeight, int y) {
return y <= caveLavaHeight;
}
static PlatformBlockState resolveExplicitCarveState(MatterCavern cavern, PlatformBlockState fluid,
PlatformBlockState lava, PlatformBlockState air) {
if (cavern == null) {
return null;
}
if (cavern.isWater()) {
return fluid;
}
if (cavern.isLava()) {
return lava;
}
return cavern.getLiquid() == 3 ? air : null;
}
private void addInternalWallsFromMasks(PackedWallBuffer walls, ColumnMask[] columnMasks) {
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
ColumnMask columnMask = columnMasks[columnIndex];
@@ -595,12 +619,14 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
output.setRaw(rx, cy, rz, b);
}
for (IrisDecorator decorator : biome.getDecorators()) {
if (decorator.getPartOf().equals(IrisDecorationPart.NONE) && zone.getFloor() > 0 && B.isSolid(output.getRaw(rx, zone.getFloor() - 1, rz))) {
decorant.getSurfaceDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, biome, InferredType.CAVE, zone.getFloor() - 1, zone.airThickness());
} else if (decorator.getPartOf().equals(IrisDecorationPart.CEILING) && zone.getCeiling() + 1 < maxY && B.isSolid(output.getRaw(rx, zone.getCeiling() + 1, rz))) {
decorant.getCeilingDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, biome, InferredType.CAVE, zone.getCeiling(), zone.airThickness());
}
IrisDecorator[] surfaceDecorators = biome.getDecoratorBucket(IrisDecorationPart.NONE);
if (surfaceDecorators.length > 0 && zone.getFloor() > 0 && B.isSolid(output.getRaw(rx, zone.getFloor() - 1, rz))) {
decorant.getSurfaceDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, biome, InferredType.CAVE, zone.getFloor() - 1, zone.airThickness());
}
IrisDecorator[] ceilingDecorators = biome.getDecoratorBucket(IrisDecorationPart.CEILING);
if (ceilingDecorators.length > 0 && zone.getCeiling() + 1 < maxY && B.isSolid(output.getRaw(rx, zone.getCeiling() + 1, rz))) {
decorant.getCeilingDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, biome, InferredType.CAVE, zone.getCeiling(), zone.airThickness());
}
}
@@ -107,14 +107,13 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
int x = rng.i(min, max + 1);
int z = rng.i(min, max + 1);
int height = (he != null ? he.getHeight((cx << 4) + x, (cz << 4) + z) : context.getRoundedHeight(x, z)) - 7;
int height = getDepositSurfaceLimit(cx, cz, x, z, he, context);
if (height <= 0)
continue;
int minY = Math.max(0, k.getMinHeight());
// TODO: WARNING HEIGHT
int maxY = Math.min(height, Math.min(getEngine().getHeight(), k.getMaxHeight()));
int maxY = Math.min(height, Math.min(getEngine().getHeight() - 1, k.getMaxHeight()));
if (minY >= maxY)
continue;
@@ -131,10 +130,19 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
int ny = j.getBlockY() + y;
int nz = j.getBlockZ() + z;
if (ny > height || nx > 15 || nx < 0 || ny > getEngine().getHeight() || ny < 0 || nz < 0 || nz > 15) {
if (nx > 15 || nx < 0 || ny >= getEngine().getHeight() || ny < 0 || nz < 0 || nz > 15) {
continue;
}
if (!k.isReplaceBedrock() && IrisProceduralBlocks.materialKey(data.get(nx, ny, nz)).equals("minecraft:bedrock")) {
int columnSurfaceLimit = getDepositSurfaceLimit(cx, cz, nx, nz, he, context);
if (ny > columnSurfaceLimit) {
continue;
}
PlatformBlockState current = data.get(nx, ny, nz);
if (!canReplaceDepositTarget(current)) {
continue;
}
if (!k.isReplaceBedrock() && IrisProceduralBlocks.materialKey(current).equals("minecraft:bedrock")) {
continue;
}
@@ -143,20 +151,40 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
PlatformBlockState remapped = resolveDepositVariant(cx, cz, nx, ny, nz, ore, dimension, context);
PlatformBlockState finalBlock = remapped != null
? remapped
: B.toDeepSlateOre(data.get(nx, ny, nz), ore);
: B.toDeepSlateOre(current, ore);
data.set(nx, ny, nz, finalBlock);
}
}
}
}
private PlatformBlockState resolveDepositVariant(int cx, int cz, int nx, int ny, int nz, PlatformBlockState ore, IrisDimension dimension, ChunkContext context) {
private int getDepositSurfaceLimit(int cx, int cz, int localX, int localZ, HeightMap heightMap, ChunkContext context) {
int surfaceY = heightMap != null
? heightMap.getHeight((cx << 4) + localX, (cz << 4) + localZ)
: context.getRoundedHeight(localX, localZ);
return depositSurfaceLimit(surfaceY);
}
static int depositSurfaceLimit(int surfaceY) {
return surfaceY - 7;
}
static int absoluteWorldY(int minHeight, int localY) {
return minHeight + localY;
}
static boolean canReplaceDepositTarget(PlatformBlockState state) {
return state != null && !state.isAir() && !state.isFluid();
}
private PlatformBlockState resolveDepositVariant(int cx, int cz, int nx, int localY, int nz, PlatformBlockState ore, IrisDimension dimension, ChunkContext context) {
int worldX = (cx << 4) + nx;
int worldZ = (cz << 4) + nz;
int worldY = absoluteWorldY(getEngine().getMinHeight(), localY);
IrisBiome biome = getEngine().getBiome(worldX, ny, worldZ);
IrisBiome biome = getEngine().getBiome(worldX, localY, worldZ);
if (biome != null) {
PlatformBlockState match = matchDepositVariant(biome.getDepositVariants(), ore, ny);
PlatformBlockState match = matchDepositVariant(biome.getDepositVariants(), ore, worldY);
if (match != null) {
return match;
}
@@ -164,14 +192,14 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
IrisRegion region = context.getRegion().get(nx, nz);
if (region != null) {
PlatformBlockState match = matchDepositVariant(region.getDepositVariants(), ore, ny);
PlatformBlockState match = matchDepositVariant(region.getDepositVariants(), ore, worldY);
if (match != null) {
return match;
}
}
if (dimension != null) {
PlatformBlockState match = matchDepositVariant(dimension.getDepositVariants(), ore, ny);
PlatformBlockState match = matchDepositVariant(dimension.getDepositVariants(), ore, worldY);
if (match != null) {
return match;
}
@@ -27,6 +27,7 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineAssignedModifier;
import art.arcane.iris.engine.framework.EngineDecorator;
import art.arcane.iris.engine.object.FloatingBottomPaletteMode;
import art.arcane.iris.engine.object.FloatingIslandBoundarySampler;
import art.arcane.iris.engine.object.FloatingIslandSample;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomePaletteLayer;
@@ -212,16 +213,18 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
IrisDimension dimension = getDimension();
IrisComplex complex = getComplex();
long baseSeed = getEngine().getSeedManager().getTerrain() ^ FLOATING_BASE_SEED_SALT;
FloatingIslandBoundarySampler boundarySampler = context.getFloatingIslandBoundarySampler();
FloatingIslandSample.clearChunkMemo();
for (int xf = 0; xf < 16; xf++) {
for (int zf = 0; zf < 16; zf++) {
int wx = x + xf;
int wz = z + zf;
IrisBiome parent = complex.getTrueBiomeStream().get(wx, wz);
IrisBiome parent = boundarySampler.parent(wx, wz);
if (parent == null || parent.getFloatingChildBiomes() == null || parent.getFloatingChildBiomes().isEmpty()) {
continue;
}
FloatingIslandSample sample = FloatingIslandSample.sampleMemoized(parent, wx, wz, chunkHeight, baseSeed, data, getEngine());
FloatingIslandSample sample = FloatingIslandSample.sampleMemoized(parent, wx, wz, chunkHeight, baseSeed, data, getEngine(), boundarySampler);
if (sample == null) {
continue;
}
@@ -295,16 +298,17 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
IrisData data = getData();
IrisComplex complex = getComplex();
long baseSeed = getEngine().getSeedManager().getTerrain() ^ FLOATING_BASE_SEED_SALT;
FloatingIslandBoundarySampler boundarySampler = context.getFloatingIslandBoundarySampler();
for (int xf = 0; xf < 16; xf++) {
for (int zf = 0; zf < 16; zf++) {
int wx = x + xf;
int wz = z + zf;
IrisBiome parent = complex.getTrueBiomeStream().get(wx, wz);
IrisBiome parent = boundarySampler.parent(wx, wz);
if (parent == null || parent.getFloatingChildBiomes() == null || parent.getFloatingChildBiomes().isEmpty()) {
continue;
}
FloatingIslandSample sample = FloatingIslandSample.sampleMemoized(parent, wx, wz, chunkHeight, baseSeed, data, getEngine());
FloatingIslandSample sample = FloatingIslandSample.sampleMemoized(parent, wx, wz, chunkHeight, baseSeed, data, getEngine(), boundarySampler);
if (sample == null) {
continue;
}
@@ -5,11 +5,12 @@ import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
import org.bukkit.block.data.BlockData;
import java.util.Objects;
import java.util.function.Function;
public final class BlockDataMergeSupport {
private static final boolean BUKKIT_PRESENT = detectBukkit();
private static volatile StateMerger FALLBACK_MERGER = null;
private static volatile StateMerger PLATFORM_MERGER = null;
private BlockDataMergeSupport() {
}
@@ -18,8 +19,14 @@ public final class BlockDataMergeSupport {
PlatformBlockState merge(PlatformBlockState base, PlatformBlockState update);
}
public static void bindFallbackMerger(StateMerger merger) {
FALLBACK_MERGER = merger;
public static synchronized StateMerger bindPlatformMerger(StateMerger merger) {
StateMerger previous = PLATFORM_MERGER;
PLATFORM_MERGER = Objects.requireNonNull(merger, "merger");
return previous;
}
public static synchronized void restorePlatformMerger(StateMerger merger) {
PLATFORM_MERGER = merger;
}
private static boolean detectBukkit() {
@@ -33,29 +40,18 @@ public final class BlockDataMergeSupport {
static PlatformBlockState merge(PlatformBlockState base, PlatformBlockState update) {
if (!BUKKIT_PRESENT) {
StateMerger merger = FALLBACK_MERGER;
return merger == null ? mergeByKey(base, update) : merger.merge(base, update);
StateMerger merger = requirePlatformMerger(PLATFORM_MERGER);
return merger.merge(base, update);
}
BlockData merged = merge((BlockData) base.nativeHandle(), (BlockData) update.nativeHandle(), BukkitBlockResolution::get);
return merged == null ? null : BukkitBlockState.of(merged);
}
private static PlatformBlockState mergeByKey(PlatformBlockState base, PlatformBlockState update) {
String key = update.key();
int bracket = key.indexOf('[');
if (bracket < 0) {
return base;
static StateMerger requirePlatformMerger(StateMerger merger) {
if (merger == null) {
throw new IllegalStateException("No platform block-state merger is bound");
}
PlatformBlockState merged = base;
String body = key.substring(bracket + 1, key.lastIndexOf(']'));
for (String entry : body.split(",")) {
int equals = entry.indexOf('=');
if (equals < 0) {
continue;
}
merged = merged.withProperty(entry.substring(0, equals).trim(), entry.substring(equals + 1).trim());
}
return merged;
return merger;
}
static BlockData merge(BlockData base, BlockData update, Function<String, BlockData> resolver) {
@@ -0,0 +1,594 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.volmlib.util.collection.KList;
import org.jetbrains.annotations.Nullable;
import java.util.IdentityHashMap;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
public final class FloatingIslandBoundarySampler {
static final int EDGE_TAPER_WIDTH = FloatingIslandEdgeProfile.DEFAULT_WIDTH;
static final int EDGE_FADE_RADIUS = EDGE_TAPER_WIDTH + 1;
private static final int CHUNK_SIZE = 16;
private static final int MIN_CORE_DISTANCE = 3;
private final BiomeSampler source;
private final ConcurrentHashMap<ParentFieldKey, ParentBoundaryField> parentFields;
private final ConcurrentHashMap<FootprintFieldKey, FootprintField> footprintFields;
private final ConcurrentHashMap<OwnershipFieldKey, OwnershipField> ownershipFields;
public FloatingIslandBoundarySampler(BiomeSampler source) {
this.source = Objects.requireNonNull(source);
this.parentFields = new ConcurrentHashMap<>();
this.footprintFields = new ConcurrentHashMap<>();
this.ownershipFields = new ConcurrentHashMap<>();
}
public @Nullable IrisBiome parent(int x, int z) {
return parentField(x, z, EDGE_TAPER_WIDTH).parent(x, z);
}
public double edgeFade(IrisBiome parent, int x, int z) {
return edgeFade(parent, x, z, EDGE_TAPER_WIDTH);
}
public double edgeFade(IrisBiome parent, int x, int z, int taperWidth) {
int boundaryDistance = edgeDistance(parent, x, z, taperWidth);
return edgeFadeForDistance(boundaryDistance, taperWidth);
}
public int edgeDistance(IrisBiome parent, int x, int z, int taperWidth) {
if (parent == null) {
return 0;
}
return parentField(x, z, taperWidth).edgeDistance(parent, x, z);
}
public FootprintSample footprint(CNG footprint, int x, int z, double signedCut) {
return footprint(footprint, x, z, signedCut, EDGE_TAPER_WIDTH);
}
public FootprintSample footprint(CNG footprint, int x, int z, double signedCut, int taperWidth) {
int width = FloatingIslandEdgeProfile.clampWidth(taperWidth);
int chunkX = Math.floorDiv(x, CHUNK_SIZE);
int chunkZ = Math.floorDiv(z, CHUNK_SIZE);
FootprintFieldKey key = new FootprintFieldKey(
footprint, chunkX, chunkZ, Double.doubleToLongBits(signedCut), width);
FootprintField field = footprintFields.computeIfAbsent(key,
ignored -> new FootprintField(footprint, chunkX, chunkZ, signedCut, width));
return field.sample(x, z);
}
public OwnershipSample ownership(KList<IrisFloatingChildBiomes> entries, CNG picker, int x, int z) {
return ownership(entries, picker, x, z, EDGE_TAPER_WIDTH);
}
public OwnershipSample ownership(KList<IrisFloatingChildBiomes> entries, CNG picker, int x, int z,
int taperWidth) {
int width = FloatingIslandEdgeProfile.clampWidth(taperWidth);
int chunkX = Math.floorDiv(x, CHUNK_SIZE);
int chunkZ = Math.floorDiv(z, CHUNK_SIZE);
OwnershipFieldKey key = new OwnershipFieldKey(entries, picker, chunkX, chunkZ, width);
OwnershipField field = ownershipFields.computeIfAbsent(key,
ignored -> new OwnershipField(entries, picker, chunkX, chunkZ, width));
return field.sample(x, z);
}
static double edgeFadeForDistance(int boundaryDistance) {
return edgeFadeForDistance(boundaryDistance, EDGE_TAPER_WIDTH);
}
static double edgeFadeForDistance(int boundaryDistance, int taperWidth) {
int width = FloatingIslandEdgeProfile.clampWidth(taperWidth);
double normalized = Math.max(0.0D, Math.min(1.0D, (boundaryDistance - 1.0D) / width));
return normalized * normalized * (3.0D - (2.0D * normalized));
}
private ParentBoundaryField parentField(int x, int z, int taperWidth) {
int width = FloatingIslandEdgeProfile.clampWidth(taperWidth);
int chunkX = Math.floorDiv(x, CHUNK_SIZE);
int chunkZ = Math.floorDiv(z, CHUNK_SIZE);
ParentFieldKey key = new ParentFieldKey(Cache.key(chunkX, chunkZ), width);
return parentFields.computeIfAbsent(key,
ignored -> new ParentBoundaryField(source, chunkX, chunkZ, width));
}
private static boolean sameBiome(@Nullable IrisBiome expected, @Nullable IrisBiome actual) {
if (actual == expected) {
return true;
}
if (expected == null || actual == null || expected.getLoadKey() == null) {
return false;
}
return expected.getLoadKey().equals(actual.getLoadKey());
}
public record FootprintSample(double signed, int cardinalSupport, int diagonalSupport, boolean accepted,
int boundaryDistance, double edgeFade) {
}
public record OwnershipSample(IrisFloatingChildBiomes owner, int boundaryDistance, double edgeFade) {
}
private record ParentFieldKey(long chunkKey, int taperWidth) {
}
private static final class FootprintFieldKey {
private final CNG footprint;
private final int chunkX;
private final int chunkZ;
private final long signedCutBits;
private final int taperWidth;
private FootprintFieldKey(CNG footprint, int chunkX, int chunkZ, long signedCutBits, int taperWidth) {
this.footprint = footprint;
this.chunkX = chunkX;
this.chunkZ = chunkZ;
this.signedCutBits = signedCutBits;
this.taperWidth = taperWidth;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof FootprintFieldKey key)) {
return false;
}
return footprint == key.footprint && chunkX == key.chunkX && chunkZ == key.chunkZ
&& signedCutBits == key.signedCutBits && taperWidth == key.taperWidth;
}
@Override
public int hashCode() {
int result = System.identityHashCode(footprint);
result = (31 * result) + chunkX;
result = (31 * result) + chunkZ;
result = (31 * result) + Long.hashCode(signedCutBits);
return (31 * result) + taperWidth;
}
}
private static final class OwnershipFieldKey {
private final KList<IrisFloatingChildBiomes> entries;
private final CNG picker;
private final int chunkX;
private final int chunkZ;
private final int taperWidth;
private OwnershipFieldKey(KList<IrisFloatingChildBiomes> entries, CNG picker, int chunkX, int chunkZ,
int taperWidth) {
this.entries = entries;
this.picker = picker;
this.chunkX = chunkX;
this.chunkZ = chunkZ;
this.taperWidth = taperWidth;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof OwnershipFieldKey key)) {
return false;
}
return entries == key.entries && picker == key.picker && chunkX == key.chunkX && chunkZ == key.chunkZ
&& taperWidth == key.taperWidth;
}
@Override
public int hashCode() {
int result = System.identityHashCode(entries);
result = (31 * result) + System.identityHashCode(picker);
result = (31 * result) + chunkX;
result = (31 * result) + chunkZ;
return (31 * result) + taperWidth;
}
}
private static final class ParentBoundaryField {
private final FieldGeometry geometry;
private final int minX;
private final int minZ;
private final IrisBiome[] parents;
private final byte[] distance;
private final boolean[] viable;
private ParentBoundaryField(BiomeSampler source, int chunkX, int chunkZ, int taperWidth) {
this.geometry = FieldGeometry.forWidth(taperWidth);
this.minX = (chunkX * CHUNK_SIZE) - geometry.fadeRadius();
this.minZ = (chunkZ * CHUNK_SIZE) - geometry.fadeRadius();
this.parents = new IrisBiome[geometry.area()];
this.distance = new byte[geometry.area()];
this.viable = new boolean[geometry.area()];
build(source);
}
private @Nullable IrisBiome parent(int x, int z) {
return parents[geometry.index(x - minX, z - minZ)];
}
private int edgeDistance(IrisBiome expected, int x, int z) {
int index = geometry.index(x - minX, z - minZ);
if (!sameBiome(expected, parents[index]) || !viable[index]) {
return 0;
}
return Math.min(geometry.maxRenderableDistance(), Byte.toUnsignedInt(distance[index]));
}
private void build(BiomeSampler source) {
int rawSize = geometry.fieldSize() + 2;
IrisBiome[] raw = new IrisBiome[rawSize * rawSize];
int rawMinX = minX - 1;
int rawMinZ = minZ - 1;
for (int z = 0; z < rawSize; z++) {
for (int x = 0; x < rawSize; x++) {
raw[rawIndex(x, z, rawSize)] = source.sample(rawMinX + x, rawMinZ + z);
}
}
for (int z = 0; z < geometry.fieldSize(); z++) {
for (int x = 0; x < geometry.fieldSize(); x++) {
int index = geometry.index(x, z);
IrisBiome parent = raw[rawIndex(x + 1, z + 1, rawSize)];
parents[index] = parent;
distance[index] = (byte) (touchesDifferentBiome(raw, parent, x, z, rawSize)
? 1
: geometry.maxDistance());
}
}
propagateDistanceField(distance, geometry);
markLabelCoreSupported(parents, distance, viable, geometry);
}
private static boolean touchesDifferentBiome(IrisBiome[] raw, @Nullable IrisBiome parent, int x, int z,
int rawSize) {
for (int dz = -1; dz <= 1; dz++) {
for (int dx = -1; dx <= 1; dx++) {
if (dx == 0 && dz == 0) {
continue;
}
if (!sameBiome(parent, raw[rawIndex(x + dx + 1, z + dz + 1, rawSize)])) {
return true;
}
}
}
return false;
}
private static int rawIndex(int x, int z, int rawSize) {
return (z * rawSize) + x;
}
}
private static final class FootprintField {
private final FieldGeometry geometry;
private final int occupancyMinX;
private final int occupancyMinZ;
private final double[] signed;
private final byte[] cardinalSupport;
private final byte[] diagonalSupport;
private final boolean[] accepted;
private final byte[] distance;
private final boolean[] viable;
private FootprintField(CNG footprint, int chunkX, int chunkZ, double signedCut, int taperWidth) {
this.geometry = FieldGeometry.forWidth(taperWidth);
this.occupancyMinX = (chunkX * CHUNK_SIZE) - geometry.fadeRadius();
this.occupancyMinZ = (chunkZ * CHUNK_SIZE) - geometry.fadeRadius();
this.signed = new double[geometry.area()];
this.cardinalSupport = new byte[geometry.area()];
this.diagonalSupport = new byte[geometry.area()];
this.accepted = new boolean[geometry.area()];
this.distance = new byte[geometry.area()];
this.viable = new boolean[geometry.area()];
build(footprint, signedCut);
}
private FootprintSample sample(int x, int z) {
int index = geometry.index(x - occupancyMinX, z - occupancyMinZ);
int inwardDistance = Byte.toUnsignedInt(distance[index]);
boolean renderable = accepted[index] && viable[index] && inwardDistance > 1;
int boundaryDistance = renderable
? Math.min(geometry.maxRenderableDistance(), inwardDistance)
: 0;
double fade = edgeFadeForDistance(boundaryDistance, geometry.taperWidth());
return new FootprintSample(signed[index], Byte.toUnsignedInt(cardinalSupport[index]),
Byte.toUnsignedInt(diagonalSupport[index]), renderable, boundaryDistance, fade);
}
private void build(CNG footprint, double signedCut) {
double[] rawSigned = sampleRawFootprint(footprint);
buildOccupancy(rawSigned, signedCut);
buildDistanceField();
}
private double[] sampleRawFootprint(CNG footprint) {
int rawSize = geometry.fieldSize() + 2;
double[] rawSigned = new double[rawSize * rawSize];
int rawMinX = occupancyMinX - 1;
int rawMinZ = occupancyMinZ - 1;
for (int z = 0; z < rawSize; z++) {
for (int x = 0; x < rawSize; x++) {
rawSigned[rawIndex(x, z, rawSize)] = signedFromUnit(footprint.noise(rawMinX + x, rawMinZ + z));
}
}
return rawSigned;
}
private void buildOccupancy(double[] rawSigned, double signedCut) {
int rawSize = geometry.fieldSize() + 2;
for (int z = 0; z < geometry.fieldSize(); z++) {
for (int x = 0; x < geometry.fieldSize(); x++) {
int index = geometry.index(x, z);
double value = rawSigned[rawIndex(x + 1, z + 1, rawSize)];
int cardinal = 0;
int diagonal = 0;
for (int dx = -1; dx <= 1; dx++) {
for (int dz = -1; dz <= 1; dz++) {
if (dx == 0 && dz == 0) {
continue;
}
if (rawSigned[rawIndex(x + dx + 1, z + dz + 1, rawSize)] <= signedCut) {
continue;
}
if (Math.abs(dx) + Math.abs(dz) == 1) {
cardinal++;
} else {
diagonal++;
}
}
}
boolean solid = value > signedCut;
FloatingIslandSample.NeighborSupport support = new FloatingIslandSample.NeighborSupport(cardinal, diagonal);
boolean supported = support.hasSolidSupport();
boolean repairedPinhole = FloatingIslandSample.isFootprintPinholeRepairable(support);
signed[index] = value;
cardinalSupport[index] = (byte) cardinal;
diagonalSupport[index] = (byte) diagonal;
accepted[index] = (solid && supported) || (!solid && repairedPinhole);
}
}
}
private void buildDistanceField() {
fillDistanceField(accepted, distance, geometry);
markCoreSupported(accepted, distance, viable, geometry);
}
private static int rawIndex(int x, int z, int rawSize) {
return (z * rawSize) + x;
}
private static double signedFromUnit(double value) {
return (Math.max(0.0D, Math.min(1.0D, value)) * 2.0D) - 1.0D;
}
}
private static final class OwnershipField {
private final FieldGeometry geometry;
private final int minX;
private final int minZ;
private final IrisFloatingChildBiomes[] owners;
private final IdentityHashMap<IrisFloatingChildBiomes, ComponentDistanceField> distances;
private OwnershipField(KList<IrisFloatingChildBiomes> entries, CNG picker, int chunkX, int chunkZ,
int taperWidth) {
this.geometry = FieldGeometry.forWidth(taperWidth);
this.minX = (chunkX * CHUNK_SIZE) - geometry.fadeRadius();
this.minZ = (chunkZ * CHUNK_SIZE) - geometry.fadeRadius();
this.owners = new IrisFloatingChildBiomes[geometry.area()];
this.distances = new IdentityHashMap<>();
for (int z = 0; z < geometry.fieldSize(); z++) {
for (int x = 0; x < geometry.fieldSize(); x++) {
double value = Math.max(0.0D, Math.min(1.0D, picker.noise(minX + x, minZ + z)));
owners[geometry.index(x, z)] = IRare.pick(entries, value);
}
}
for (IrisFloatingChildBiomes entry : entries) {
if (entry != null && !distances.containsKey(entry)) {
distances.put(entry, buildDistanceField(entry));
}
}
}
private OwnershipSample sample(int x, int z) {
int index = geometry.index(x - minX, z - minZ);
IrisFloatingChildBiomes owner = owners[index];
if (owner == null) {
return new OwnershipSample(null, 0, 0.0D);
}
ComponentDistanceField field = distances.get(owner);
if (field == null) {
return new OwnershipSample(null, 0, 0.0D);
}
int inwardDistance = Byte.toUnsignedInt(field.distance()[index]);
int boundaryDistance = field.viable()[index]
? Math.min(geometry.maxRenderableDistance(), inwardDistance)
: 0;
double fade = edgeFadeForDistance(boundaryDistance, geometry.taperWidth());
return new OwnershipSample(owner, boundaryDistance, fade);
}
private ComponentDistanceField buildDistanceField(IrisFloatingChildBiomes owner) {
boolean[] owned = new boolean[owners.length];
byte[] distance = new byte[owners.length];
boolean[] viable = new boolean[owners.length];
for (int i = 0; i < owners.length; i++) {
owned[i] = owners[i] == owner;
}
fillDistanceField(owned, distance, geometry);
markCoreSupported(owned, distance, viable, geometry);
return new ComponentDistanceField(distance, viable);
}
}
private record ComponentDistanceField(byte[] distance, boolean[] viable) {
}
private record FieldGeometry(int taperWidth, int fadeRadius, int fieldSize, int maxDistance) {
private static FieldGeometry forWidth(int taperWidth) {
int width = FloatingIslandEdgeProfile.clampWidth(taperWidth);
int radius = width + 1;
return new FieldGeometry(width, radius, CHUNK_SIZE + (radius * 2), radius + 1);
}
private int area() {
return fieldSize * fieldSize;
}
private int maxRenderableDistance() {
return fadeRadius;
}
private int index(int x, int z) {
return (z * fieldSize) + x;
}
}
private static void fillDistanceField(boolean[] inside, byte[] distance, FieldGeometry geometry) {
for (int i = 0; i < inside.length; i++) {
distance[i] = (byte) (inside[i] ? geometry.maxDistance() : 0);
}
for (int z = 0; z < geometry.fieldSize(); z++) {
for (int x = 0; x < geometry.fieldSize(); x++) {
if (!inside[geometry.index(x, z)]) {
continue;
}
setMinimumDistance(distance, x, z, x - 1, z, geometry);
setMinimumDistance(distance, x, z, x, z - 1, geometry);
setMinimumDistance(distance, x, z, x - 1, z - 1, geometry);
setMinimumDistance(distance, x, z, x + 1, z - 1, geometry);
}
}
for (int z = geometry.fieldSize() - 1; z >= 0; z--) {
for (int x = geometry.fieldSize() - 1; x >= 0; x--) {
if (!inside[geometry.index(x, z)]) {
continue;
}
setMinimumDistance(distance, x, z, x + 1, z, geometry);
setMinimumDistance(distance, x, z, x, z + 1, geometry);
setMinimumDistance(distance, x, z, x + 1, z + 1, geometry);
setMinimumDistance(distance, x, z, x - 1, z + 1, geometry);
}
}
}
private static void propagateDistanceField(byte[] distance, FieldGeometry geometry) {
for (int z = 0; z < geometry.fieldSize(); z++) {
for (int x = 0; x < geometry.fieldSize(); x++) {
setMinimumDistance(distance, x, z, x - 1, z, geometry);
setMinimumDistance(distance, x, z, x, z - 1, geometry);
setMinimumDistance(distance, x, z, x - 1, z - 1, geometry);
setMinimumDistance(distance, x, z, x + 1, z - 1, geometry);
}
}
for (int z = geometry.fieldSize() - 1; z >= 0; z--) {
for (int x = geometry.fieldSize() - 1; x >= 0; x--) {
setMinimumDistance(distance, x, z, x + 1, z, geometry);
setMinimumDistance(distance, x, z, x, z + 1, geometry);
setMinimumDistance(distance, x, z, x + 1, z + 1, geometry);
setMinimumDistance(distance, x, z, x - 1, z + 1, geometry);
}
}
}
private static void setMinimumDistance(byte[] distance, int x, int z, int neighborX, int neighborZ,
FieldGeometry geometry) {
if (neighborX < 0 || neighborX >= geometry.fieldSize()
|| neighborZ < 0 || neighborZ >= geometry.fieldSize()) {
return;
}
int index = geometry.index(x, z);
int neighborDistance = Byte.toUnsignedInt(distance[geometry.index(neighborX, neighborZ)]);
int currentDistance = Byte.toUnsignedInt(distance[index]);
distance[index] = (byte) Math.min(currentDistance, neighborDistance + 1);
}
private static void markCoreSupported(boolean[] inside, byte[] distance, boolean[] viable,
FieldGeometry geometry) {
for (int z = 0; z < geometry.fieldSize(); z++) {
for (int x = 0; x < geometry.fieldSize(); x++) {
int index = geometry.index(x, z);
if (!inside[index]) {
continue;
}
int inwardDistance = Byte.toUnsignedInt(distance[index]);
viable[index] = inwardDistance >= MIN_CORE_DISTANCE
|| inwardDistance == MIN_CORE_DISTANCE - 1
&& hasCoreNeighbor(inside, distance, null, x, z, geometry);
}
}
}
private static void markLabelCoreSupported(IrisBiome[] labels, byte[] distance, boolean[] viable,
FieldGeometry geometry) {
for (int z = 0; z < geometry.fieldSize(); z++) {
for (int x = 0; x < geometry.fieldSize(); x++) {
int index = geometry.index(x, z);
int inwardDistance = Byte.toUnsignedInt(distance[index]);
viable[index] = inwardDistance >= MIN_CORE_DISTANCE
|| inwardDistance == MIN_CORE_DISTANCE - 1
&& hasCoreNeighbor(null, distance, labels, x, z, geometry);
}
}
}
private static boolean hasCoreNeighbor(@Nullable boolean[] inside, byte[] distance,
@Nullable IrisBiome[] labels, int x, int z, FieldGeometry geometry) {
int index = geometry.index(x, z);
for (int dz = -1; dz <= 1; dz++) {
for (int dx = -1; dx <= 1; dx++) {
if (dx == 0 && dz == 0) {
continue;
}
int neighborX = x + dx;
int neighborZ = z + dz;
if (neighborX < 0 || neighborX >= geometry.fieldSize()
|| neighborZ < 0 || neighborZ >= geometry.fieldSize()) {
continue;
}
int neighbor = geometry.index(neighborX, neighborZ);
if (Byte.toUnsignedInt(distance[neighbor]) < MIN_CORE_DISTANCE) {
continue;
}
if (inside != null && !inside[neighbor]) {
continue;
}
if (labels != null && !sameBiome(labels[index], labels[neighbor])) {
continue;
}
return true;
}
}
return false;
}
@FunctionalInterface
public interface BiomeSampler {
@Nullable IrisBiome sample(int x, int z);
}
}
@@ -0,0 +1,91 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.object;
import art.arcane.iris.util.project.noise.CNG;
public final class FloatingIslandEdgeProfile {
public static final int DEFAULT_WIDTH = 10;
public static final int MIN_WIDTH = 2;
public static final int MAX_WIDTH = 32;
public static final double DEFAULT_EXPONENT = 1.0D;
public static final double MIN_EXPONENT = 0.25D;
public static final double MAX_EXPONENT = 4.0D;
public static final double MAX_VARIATION_AMPLITUDE = 8.0D;
public static final FloatingIslandEdgeProfile DEFAULT = new FloatingIslandEdgeProfile(
DEFAULT_WIDTH, DEFAULT_EXPONENT, 0.0D, null);
private final int width;
private final double exponent;
private final double variationAmplitude;
private final CNG variation;
public FloatingIslandEdgeProfile(int width, double exponent, double variationAmplitude, CNG variation) {
this.width = clampWidth(width);
this.exponent = clampExponent(exponent);
this.variationAmplitude = clampVariationAmplitude(variationAmplitude, this.width);
this.variation = variation;
}
public int width() {
return width;
}
public double exponent() {
return exponent;
}
public double variationAmplitude() {
return variationAmplitude;
}
public int fieldWidth() {
return Math.min(MAX_WIDTH, (int) Math.ceil(width + variationAmplitude));
}
public double fade(int boundaryDistance, int x, int z) {
if (boundaryDistance <= 1) {
return 0.0D;
}
double localWidth = width;
if (variation != null && variationAmplitude > 0.0D) {
double noise = Math.max(0.0D, Math.min(1.0D, variation.noise(x, z)));
double signedNoise = (noise * 2.0D) - 1.0D;
localWidth += signedNoise * variationAmplitude;
}
double position = Math.max(0.0D, Math.min(1.0D, (boundaryDistance - 1.0D) / localWidth));
double smooth = position * position * (3.0D - (2.0D * position));
return exponent == DEFAULT_EXPONENT ? smooth : Math.pow(smooth, exponent);
}
public static int clampWidth(int width) {
return Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, width));
}
public static double clampExponent(double exponent) {
return Math.max(MIN_EXPONENT, Math.min(MAX_EXPONENT, exponent));
}
public static double clampVariationAmplitude(double amplitude, int width) {
int clampedWidth = clampWidth(width);
double widthLimit = Math.min(clampedWidth - MIN_WIDTH, MAX_WIDTH - clampedWidth);
return Math.max(0.0D, Math.min(Math.min(MAX_VARIATION_AMPLITUDE, widthLimit), amplitude));
}
}
@@ -41,9 +41,7 @@ public final class FloatingIslandSample {
public static final int REJECT_COUNT = 7;
public static final int REJECT_CLUSTER = REJECT_NO_SEED;
private static final double EDGE_ROUNDING_BAND = 0.28;
private static final double FOOTPRINT_PINHOLE_REPAIR_MARGIN = 0.16;
private static final int PINHOLE_CARDINAL_FILL = 4;
private static final int PINHOLE_CARDINAL_FILL = 3;
private static final int PINHOLE_TOTAL_FILL = 7;
private static final int CARVE_CARDINAL_SUPPORT = 2;
private static final int CARVE_TOTAL_SUPPORT = 4;
@@ -75,13 +73,13 @@ public final class FloatingIslandSample {
FOOTPRINT_MEMO.get().clear();
}
public static FloatingIslandSample sampleMemoized(IrisBiome parent, int wx, int wz, int chunkHeight, long baseSeed, IrisData data, Engine engine) {
public static FloatingIslandSample sampleMemoized(IrisBiome parent, int wx, int wz, int chunkHeight, long baseSeed, IrisData data, Engine engine, FloatingIslandBoundarySampler boundarySampler) {
long key = (((long) wx) << 32) ^ (wz & 0xFFFFFFFFL);
HashMap<Long, FloatingIslandSample> memo = CHUNK_MEMO.get();
if (memo.containsKey(key)) {
return memo.get(key);
}
FloatingIslandSample result = sample(parent, wx, wz, chunkHeight, baseSeed, data, engine);
FloatingIslandSample result = sample(parent, wx, wz, chunkHeight, baseSeed, data, engine, boundarySampler);
memo.put(key, result);
return result;
}
@@ -172,17 +170,42 @@ public final class FloatingIslandSample {
return baseSeed ^ ((long) wx * 341873128712L) ^ ((long) wz * 132897987541L);
}
public static FloatingIslandSample sample(IrisBiome parent, int wx, int wz, int chunkHeight, long baseSeed, IrisData data, Engine engine) {
private static int maximumEdgeFieldWidth(KList<IrisFloatingChildBiomes> entries, long baseSeed,
IrisData data, IrisBiome parent) {
int maximumWidth = FloatingIslandEdgeProfile.MIN_WIDTH;
for (IrisFloatingChildBiomes entry : entries) {
FloatingIslandEdgeProfile profile = resolveEdgeProfile(entry, baseSeed, data, parent);
maximumWidth = Math.max(maximumWidth, profile.fieldWidth());
}
return maximumWidth;
}
private static FloatingIslandEdgeProfile resolveEdgeProfile(IrisFloatingChildBiomes entry, long baseSeed,
IrisData data, IrisBiome parent) {
FloatingIslandEdgeProfile profile = entry.getEdgeTaperProfile(baseSeed, data);
if (profile != null) {
return profile;
}
warnNullCng("edgeTaperVariationStyle", parent);
return FloatingIslandEdgeProfile.DEFAULT;
}
public static FloatingIslandSample sample(IrisBiome parent, int wx, int wz, int chunkHeight, long baseSeed, IrisData data, Engine engine, FloatingIslandBoundarySampler boundarySampler) {
KList<IrisFloatingChildBiomes> entries = parent.getFloatingChildBiomes();
if (entries == null || entries.isEmpty()) {
return reject(REJECT_NO_ENTRIES);
}
int boundaryFieldWidth = maximumEdgeFieldWidth(entries, baseSeed, data, parent);
int parentBoundaryDistance = boundarySampler.edgeDistance(parent, wx, wz, boundaryFieldWidth);
if (parent.isMergeFloatingChildBiomes()) {
return sampleMerged(parent, entries, wx, wz, chunkHeight, baseSeed, data, engine);
return sampleMerged(parent, entries, wx, wz, chunkHeight, baseSeed, data, engine, boundarySampler,
parentBoundaryDistance, boundaryFieldWidth);
}
IrisFloatingChildBiomes entry;
int ownershipBoundaryDistance = boundaryFieldWidth + 1;
if (entries.size() == 1) {
entry = entries.getFirst();
} else {
@@ -192,24 +215,30 @@ public final class FloatingIslandSample {
warnNullCng("pickerStyle", parent);
return reject(REJECT_NO_PICK);
}
double pickerValue = picker.noise(wx, wz);
double clamped = Math.max(0, Math.min(1, pickerValue));
entry = IRare.pick(entries, clamped);
FloatingIslandBoundarySampler.OwnershipSample ownership = boundarySampler.ownership(
entries, picker, wx, wz, boundaryFieldWidth);
entry = ownership.owner();
if (entry == null) {
return reject(REJECT_NO_PICK);
}
ownershipBoundaryDistance = ownership.boundaryDistance();
}
return sampleEntry(parent, entry, wx, wz, chunkHeight, baseSeed, data, engine);
return sampleEntry(parent, entry, wx, wz, chunkHeight, baseSeed, data, engine, boundarySampler,
parentBoundaryDistance, ownershipBoundaryDistance);
}
private static FloatingIslandSample sampleMerged(IrisBiome parent, KList<IrisFloatingChildBiomes> entries, int wx, int wz, int chunkHeight, long baseSeed, IrisData data, Engine engine) {
private static FloatingIslandSample sampleMerged(IrisBiome parent, KList<IrisFloatingChildBiomes> entries,
int wx, int wz, int chunkHeight, long baseSeed, IrisData data,
Engine engine, FloatingIslandBoundarySampler boundarySampler,
int parentBoundaryDistance, int boundaryFieldWidth) {
KList<FloatingIslandSample> samples = new KList<>();
int minY = Integer.MAX_VALUE;
int maxY = Integer.MIN_VALUE;
for (IrisFloatingChildBiomes entry : entries) {
FloatingIslandSample sample = sampleEntry(parent, entry, wx, wz, chunkHeight, baseSeed, data, engine);
FloatingIslandSample sample = sampleEntry(parent, entry, wx, wz, chunkHeight, baseSeed, data, engine,
boundarySampler, parentBoundaryDistance, boundaryFieldWidth + 1);
if (sample == null) {
continue;
}
@@ -270,30 +299,36 @@ public final class FloatingIslandSample {
return new FloatingIslandSample(topEntry, minY, thickness, topIdx, solidCount, solidMask, entryMask);
}
private static FloatingIslandSample sampleEntry(IrisBiome parent, IrisFloatingChildBiomes entry, int wx, int wz, int chunkHeight, long baseSeed, IrisData data, Engine engine) {
private static FloatingIslandSample sampleEntry(IrisBiome parent, IrisFloatingChildBiomes entry, int wx, int wz,
int chunkHeight, long baseSeed, IrisData data, Engine engine,
FloatingIslandBoundarySampler boundarySampler,
int parentBoundaryDistance, int ownershipBoundaryDistance) {
FloatingIslandEdgeProfile edgeProfile = resolveEdgeProfile(entry, baseSeed, data, parent);
CNG footprintCng = entry.getFootprintCng(baseSeed, data);
if (footprintCng == null) {
warnNullCng("footprintStyle", parent);
return reject(REJECT_NO_SEED);
}
double signed = footprintSigned(footprintCng, wx, wz);
double threshold = Math.max(0, Math.min(1, entry.getFootprintThreshold()));
double signedCut = (threshold * 2.0) - 1.0;
FloatingIslandBoundarySampler.FootprintSample footprint = boundarySampler.footprint(
footprintCng, wx, wz, signedCut, edgeProfile.fieldWidth());
double signed = footprint.signed();
double[] diag = LAST_DENSITY.get();
diag[0] = signed;
diag[1] = signedCut;
NeighborSupport footprintSupport = footprintNeighborSupport(footprintCng, wx, wz, signedCut);
boolean footprintSolid = signed > signedCut;
boolean repairedFootprint = !footprintSolid && isFootprintPinholeRepairable(signed, signedCut, footprintSupport);
if (!footprintSolid && !repairedFootprint) {
NeighborSupport footprintSupport = new NeighborSupport(footprint.cardinalSupport(), footprint.diagonalSupport());
if (!footprint.accepted()) {
return reject(REJECT_NO_SEED);
}
if (footprintSolid && !footprintSupport.hasSolidSupport()) {
return reject(REJECT_NO_SEED);
int boundaryDistance = Math.min(footprint.boundaryDistance(),
Math.min(parentBoundaryDistance, ownershipBoundaryDistance));
double edgeFade = edgeProfile.fade(boundaryDistance, wx, wz);
if (edgeFade <= 0.0D) {
return reject(REJECT_NO_THICKNESS);
}
double shapeSigned = repairedFootprint ? signedCut + FOOTPRINT_PINHOLE_REPAIR_MARGIN : signed;
CNG altitudeCng = entry.getAltitudeCng(baseSeed, data);
if (altitudeCng == null) {
@@ -307,10 +342,9 @@ public final class FloatingIslandSample {
int maxAlt = Math.max(minAlt, entry.getMaxHeightAboveSurface() - worldMin);
int baseY = minAlt + (int) Math.round(altClamped * (maxAlt - minAlt));
double edgeFade = edgeFade(shapeSigned, signedCut);
IrisBiome target = entry.getRealBiome(parent, data);
int topH = roundedEdgeHeight(computeTopHeight(entry, target, engine, baseSeed, wx, wz, data), edgeFade);
int topY = baseY + topH;
int fullTopHeight = computeTopHeight(entry, target, engine, baseSeed, wx, wz, data);
int topH = roundedEdgeHeight(fullTopHeight, edgeFade);
CNG bottomCng = entry.getBottomCng(baseSeed, data);
if (bottomCng == null) {
@@ -323,6 +357,17 @@ public final class FloatingIslandSample {
int minDepth = Math.max(0, entry.getBottomDepthMin());
int maxDepth = Math.max(minDepth, entry.getBottomDepthMax());
int depth = roundedEdgeDepth(minDepth, maxDepth, bottomShaped, edgeFade);
if (topH == 0 && depth == 0) {
double fullDepth = minDepth + (bottomShaped * (maxDepth - minDepth));
if (fullDepth > 0.0D) {
depth = 1;
} else if (fullTopHeight > 0) {
topH = 1;
} else {
return reject(REJECT_NO_THICKNESS);
}
}
int topY = baseY + topH;
int botY = baseY - depth;
Integer minAbsoluteY = entry.getMinAbsoluteY();
@@ -360,18 +405,21 @@ public final class FloatingIslandSample {
boolean[] solidMask = new boolean[thickness];
CNG wallWarp = entry.getWallWarpCng(baseSeed, data);
double warpAmp = Math.max(0, entry.getWallWarpAmplitude());
double effectiveWarpAmp = effectiveWallWarpAmplitude(warpAmp, edgeFade);
IrisCaveProfileSampler carvingProfileSampler = entry.getCarvingProfileSampler(engine, data);
CNG carve = carvingProfileSampler == null && !entry.hasCarvingReference() ? entry.getCarveCng(baseSeed, data) : null;
double carveThreshold = entry.getCarveThreshold();
boolean useWarp = wallWarp != null && warpAmp > 0;
boolean useWarp = wallWarp != null && effectiveWarpAmp > 0;
boolean useProfileCarve = carvingProfileSampler != null;
boolean useCarve = directCarveEnabled(entry, carvingProfileSampler, carve, carveThreshold);
boolean[] lateralCarveMask = useProfileCarve || useCarve ? new boolean[thickness] : null;
boolean baseCarveInterior = lateralCarveMask != null && canCarveLaterally(edgeFade, footprintSupport);
if (useWarp) {
for (int k = 0; k < thickness; k++) {
int wy = botY + k;
boolean layerSolid = layerFootprintSolid(footprintCng, wallWarp, true, warpAmp, wx, wy, wz, signedCut);
NeighborSupport layerSupport = layerNeighborSupport(footprintCng, wallWarp, true, warpAmp, wx, wy, wz, signedCut);
boolean layerSolid = layerFootprintSolid(footprintCng, wallWarp, true, effectiveWarpAmp, wx, wy, wz, signedCut);
NeighborSupport layerSupport = layerNeighborSupport(footprintCng, wallWarp, true, effectiveWarpAmp, wx, wy, wz, signedCut);
if (layerSolid && !layerSupport.hasSolidSupport()) {
continue;
}
@@ -379,16 +427,22 @@ public final class FloatingIslandSample {
continue;
}
solidMask[k] = true;
if (lateralCarveMask != null) {
lateralCarveMask[k] = baseCarveInterior && layerSolid && layerSupport.isFullySurrounded();
}
}
} else {
Arrays.fill(solidMask, true);
if (baseCarveInterior) {
Arrays.fill(lateralCarveMask, true);
}
}
int solidCount = solidifyUncarvedInterior(solidMask);
if (useProfileCarve) {
solidCount = carveSolidInterior(solidMask, botY, wx, wz, carvingProfileSampler, carveThreshold);
solidCount = carveSolidInterior(solidMask, lateralCarveMask, botY, wx, wz, carvingProfileSampler, carveThreshold);
} else if (useCarve) {
solidCount = carveSolidInterior(solidMask, botY, wx, wz, carve, carveThreshold);
solidCount = carveSolidInterior(solidMask, lateralCarveMask, botY, wx, wz, carve, carveThreshold);
}
int highestSolidIdx = highestSolidIndex(solidMask);
@@ -402,12 +456,6 @@ public final class FloatingIslandSample {
return new FloatingIslandSample(entry, botY, thickness, topIdx, solidCount, solidMask);
}
static double edgeFade(double signed, double signedCut) {
double edge = (signed - signedCut) / EDGE_ROUNDING_BAND;
double edgeClamped = Math.max(0, Math.min(1, edge));
return edgeClamped * edgeClamped * (3.0 - 2.0 * edgeClamped);
}
static int roundedEdgeHeight(int topHeight, double edgeFade) {
return Math.max(0, (int) Math.round(Math.max(0, topHeight) * Math.max(0, Math.min(1, edgeFade))));
}
@@ -421,23 +469,33 @@ public final class FloatingIslandSample {
return (int) Math.round(fullDepth * fade);
}
static double effectiveWallWarpAmplitude(double wallWarpAmplitude, double edgeFade) {
double amplitude = Math.max(0, wallWarpAmplitude);
double fade = Math.max(0, Math.min(1, edgeFade));
return amplitude * fade;
}
static boolean directCarveEnabled(IrisFloatingChildBiomes entry, IrisCaveProfileSampler carvingProfileSampler, CNG carve, double carveThreshold) {
return carvingProfileSampler == null && (entry == null || !entry.hasCarvingReference()) && carve != null && carveThreshold < 1.0;
}
static int carveSolidInterior(boolean[] solidMask, int botY, int wx, int wz, CNG carve, double carveThreshold) {
return carveSolidInterior(solidMask, botY, wx, wz, (x, y, z) -> {
static boolean canCarveLaterally(double edgeFade, NeighborSupport support) {
return edgeFade >= 1.0 && support.isFullySurrounded();
}
static int carveSolidInterior(boolean[] solidMask, boolean[] lateralCarveMask, int botY, int wx, int wz, CNG carve, double carveThreshold) {
return carveSolidInterior(solidMask, lateralCarveMask, botY, wx, wz, (x, y, z) -> {
double carveNoise = carve.noise(x, y, z);
double carveClamped = Math.max(0, Math.min(1, carveNoise));
return carveClamped > carveThreshold;
});
}
static int carveSolidInterior(boolean[] solidMask, int botY, int wx, int wz, IrisCaveProfileSampler carve, double carveThreshold) {
return carveSolidInterior(solidMask, botY, wx, wz, (x, y, z) -> carve.shouldCarve(x, y, z, carveThreshold));
static int carveSolidInterior(boolean[] solidMask, boolean[] lateralCarveMask, int botY, int wx, int wz, IrisCaveProfileSampler carve, double carveThreshold) {
return carveSolidInterior(solidMask, lateralCarveMask, botY, wx, wz, (x, y, z) -> carve.shouldCarve(x, y, z, carveThreshold));
}
private static int carveSolidInterior(boolean[] solidMask, int botY, int wx, int wz, CarveSampler carve) {
private static int carveSolidInterior(boolean[] solidMask, boolean[] lateralCarveMask, int botY, int wx, int wz, CarveSampler carve) {
int firstSolid = -1;
int lastSolid = -1;
for (int i = 0; i < solidMask.length; i++) {
@@ -459,7 +517,7 @@ public final class FloatingIslandSample {
boolean[] carveMask = new boolean[solidMask.length];
if (carveStart <= carveEnd) {
for (int i = carveStart; i <= carveEnd; i++) {
if (!solidMask[i]) {
if (!solidMask[i] || !lateralCarveMask[i]) {
continue;
}
@@ -549,8 +607,8 @@ public final class FloatingIslandSample {
return footprintNeighborSupport(footprintCng, wx, wz, signedCut).hasSolidSupport();
}
static boolean isFootprintPinholeRepairable(double signed, double signedCut, NeighborSupport support) {
return signed >= signedCut - FOOTPRINT_PINHOLE_REPAIR_MARGIN && support.canFillPinhole();
static boolean isFootprintPinholeRepairable(NeighborSupport support) {
return support.canFillPinhole();
}
static NeighborSupport footprintNeighborSupport(CNG footprintCng, int wx, int wz, double signedCut) {
@@ -678,7 +736,7 @@ public final class FloatingIslandSample {
private final int cardinal;
private final int diagonal;
private NeighborSupport(int cardinal, int diagonal) {
NeighborSupport(int cardinal, int diagonal) {
this.cardinal = cardinal;
this.diagonal = diagonal;
}
@@ -702,6 +760,10 @@ public final class FloatingIslandSample {
boolean canFillPinhole() {
return cardinal >= PINHOLE_CARDINAL_FILL && total() >= PINHOLE_TOTAL_FILL;
}
boolean isFullySurrounded() {
return cardinal == 4 && diagonal == 4;
}
}
private static int computeTopHeight(IrisFloatingChildBiomes entry, IrisBiome target, Engine engine, long baseSeed, int wx, int wz, IrisData data) {
@@ -134,8 +134,6 @@ public class IrisBiome extends IrisRegistrant implements IRare {
private int lockLayersMax = 7;
@Desc("Profile-driven 3D cave configuration")
private IrisCaveProfile caveProfile = new IrisCaveProfile();
@Desc("Configuration of fluid bodies such as rivers & lakes")
private IrisFluidBodies fluidBodies = new IrisFluidBodies();
@MinNumber(1)
@MaxNumber(512)
@Desc("The rarity of this biome (integer)")
@@ -20,6 +20,7 @@ package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.framework.LootResolver;
import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.Required;
@@ -82,7 +83,7 @@ public class IrisBlockDrops {
public void fillDrops(boolean debug, KList<ItemStack> d) {
for (IrisLoot i : getDrops()) {
if (RNG.r.i(1, i.getRarity()) == i.getRarity()) {
if (LootResolver.oneIn(RNG.r, i.getRarity())) {
d.add(i.get(debug, RNG.r));
}
}

Some files were not shown because too many files have changed in this diff Show More