mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 12:41:43 +00:00
Vwoop
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
+35
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+107
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-6
@@ -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
|
||||
}
|
||||
+15
-7
@@ -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
|
||||
}
|
||||
+148
@@ -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
|
||||
}
|
||||
}
|
||||
+122
@@ -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";
|
||||
}
|
||||
}
|
||||
+56
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+234
@@ -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
|
||||
}
|
||||
}
|
||||
+139
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1005
File diff suppressed because it is too large
Load Diff
+4
-10
@@ -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
|
||||
}
|
||||
+39
@@ -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);
|
||||
}
|
||||
}
|
||||
+119
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+315
@@ -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);
|
||||
}
|
||||
}
|
||||
+401
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+74
@@ -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;
|
||||
}
|
||||
}
|
||||
+140
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+66
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
+113
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+52
@@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
+233
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
@@ -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;
|
||||
}
|
||||
}
|
||||
+43
@@ -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);
|
||||
}
|
||||
}
|
||||
+42
@@ -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);
|
||||
}
|
||||
}
|
||||
+62
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+101
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+386
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -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();
|
||||
}
|
||||
}
|
||||
+24
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+102
@@ -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;
|
||||
}
|
||||
}
|
||||
+183
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+871
@@ -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) {
|
||||
}
|
||||
}
|
||||
+52
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+74
@@ -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();
|
||||
}
|
||||
}
|
||||
+191
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+281
@@ -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) {
|
||||
}
|
||||
}
|
||||
+74
@@ -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);
|
||||
}
|
||||
}
|
||||
+206
@@ -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) {
|
||||
}
|
||||
}
|
||||
+236
-18
@@ -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];
|
||||
|
||||
+109
-12
@@ -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);
|
||||
}
|
||||
|
||||
+100
-144
@@ -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
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
+12
-6
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+130
-119
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
-79
@@ -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;
|
||||
}
|
||||
}
|
||||
+308
-298
@@ -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) {
|
||||
|
||||
+73
@@ -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;
|
||||
}
|
||||
|
||||
+8
-4
@@ -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
Reference in New Issue
Block a user