mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 21:40:05 +00:00
Vwoop
This commit is contained in:
1 parent
55b7460b9a
commit
699247b1de
327 files changed
+30138
-3934
No files matched your search
+85
-69
@@ -75,7 +75,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
if (registry == null) {
|
||||
throw new IllegalStateException("Iris cannot create structure state without the biome registry");
|
||||
}
|
||||
Set<String> generatedBiomeKeys = exactStructureBiomeKeys();
|
||||
Set<String> generatedBiomeKeys = requireConfiguredStructureBiomeKeys(exactStructureBiomeKeys());
|
||||
Set<String> missingBiomeKeys = new LinkedHashSet<>(generatedBiomeKeys);
|
||||
missingBiomeKeys.removeAll(registeredBiomeKeys(registry));
|
||||
if (!missingBiomeKeys.isEmpty()) {
|
||||
@@ -101,21 +101,32 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
|
||||
@Override
|
||||
protected Stream<Holder<Biome>> collectPossibleBiomes() {
|
||||
Set<String> generatedBiomeKeys = requireConfiguredStructureBiomeKeys(exactStructureBiomeKeys());
|
||||
Registry<Biome> registry = biomeRegistry();
|
||||
if (registry == null) {
|
||||
return serializedSource.possibleBiomes().stream();
|
||||
}
|
||||
Set<String> generatedBiomeKeys = possibleStructureBiomeKeys();
|
||||
LinkedHashSet<Holder<Biome>> possible = new LinkedHashSet<>();
|
||||
Holder<Biome> fallback = fallbackHolder(registry);
|
||||
registry.listElements().forEach((Holder.Reference<Biome> reference) -> {
|
||||
String key = holderKey(reference);
|
||||
if (isGeneratedBiomeKey(key, generatedBiomeKeys)) {
|
||||
possible.add(reference);
|
||||
if (registry == null) {
|
||||
for (Holder<Biome> biome : serializedSource.possibleBiomes()) {
|
||||
if (isGeneratedBiomeKey(holderKey(biome), generatedBiomeKeys)) {
|
||||
possible.add(biome);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (possible.isEmpty() && fallback != null) {
|
||||
possible.add(fallback);
|
||||
} else {
|
||||
Set<String> missingBiomeKeys = new LinkedHashSet<>(generatedBiomeKeys);
|
||||
missingBiomeKeys.removeAll(registeredBiomeKeys(registry));
|
||||
if (!missingBiomeKeys.isEmpty()) {
|
||||
throw new IllegalStateException("Iris structure biomes are not registered: "
|
||||
+ missingBiomeKeys);
|
||||
}
|
||||
registry.listElements().forEach((Holder.Reference<Biome> reference) -> {
|
||||
if (isGeneratedBiomeKey(holderKey(reference), generatedBiomeKeys)) {
|
||||
possible.add(reference);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (possible.isEmpty()) {
|
||||
String phase = registry == null ? "serialized biome bootstrap" : "biome registry";
|
||||
throw new IllegalStateException("Iris configured structure biomes are absent from the "
|
||||
+ phase + ": " + generatedBiomeKeys);
|
||||
}
|
||||
return possible.stream();
|
||||
}
|
||||
@@ -220,31 +231,44 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
private Holder<Biome> resolveSurfaceStructureBiome(Engine engine, int quartX, int quartZ,
|
||||
Climate.Sampler sampler) {
|
||||
Registry<Biome> registry = biomeRegistry();
|
||||
if (!isReady(engine) || registry == null) {
|
||||
return serializedSource.getNoiseBiome(quartX, 0, quartZ, sampler);
|
||||
if (!isReady(engine)) {
|
||||
throw new IllegalStateException("Iris structure biome lookup ran before engine binding");
|
||||
}
|
||||
if (registry == null) {
|
||||
throw new IllegalStateException("Iris structure biome lookup has no biome registry");
|
||||
}
|
||||
IrisBiome irisBiome = engine.getComplex().getTrueBiomeStream().get(quartX << 2, quartZ << 2);
|
||||
Holder<Biome> resolved = irisBiome == null ? null : resolveHolder(registry, irisBiome.getVanillaDerivativeKey());
|
||||
return resolved == null ? serializedSource.getNoiseBiome(quartX, 0, quartZ, sampler) : resolved;
|
||||
if (irisBiome == null) {
|
||||
throw new IllegalStateException("Iris returned no surface structure biome at quart "
|
||||
+ quartX + "," + quartZ);
|
||||
}
|
||||
Holder<Biome> resolved = resolveHolder(registry, irisBiome.getVanillaDerivativeKey());
|
||||
if (resolved == null) {
|
||||
throw new IllegalStateException("Iris structure biome derivative '"
|
||||
+ irisBiome.getVanillaDerivativeKey() + "' is not registered at quart "
|
||||
+ quartX + "," + quartZ);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveStructureBiome(Engine engine, int quartX, int quartY, int quartZ,
|
||||
Climate.Sampler sampler) {
|
||||
Registry<Biome> registry = biomeRegistry();
|
||||
BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ);
|
||||
if (resolution == null || registry == null) {
|
||||
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
|
||||
if (resolution == null) {
|
||||
throw new IllegalStateException("Iris returned no structure biome at quart "
|
||||
+ quartX + "," + quartY + "," + quartZ);
|
||||
}
|
||||
if (registry == null) {
|
||||
throw new IllegalStateException("Iris structure biome lookup has no biome registry");
|
||||
}
|
||||
Holder<Biome> resolved = resolveHolder(registry, resolution.irisBiome().getVanillaDerivativeKey());
|
||||
if (resolved == null && resolution.irisBiome().isCustom()) {
|
||||
IrisBiomeCustom customBiome = resolution.irisBiome().getCustomBiome(
|
||||
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
|
||||
if (customBiome != null) {
|
||||
resolved = resolveHolder(registry, engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT)
|
||||
+ ":" + customBiome.getId().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
if (resolved == null) {
|
||||
throw new IllegalStateException("Iris structure biome derivative '"
|
||||
+ resolution.irisBiome().getVanillaDerivativeKey() + "' is not registered at block "
|
||||
+ resolution.blockX() + "," + resolution.blockY() + "," + resolution.blockZ());
|
||||
}
|
||||
return resolved == null ? fallbackBiome(registry, quartX, quartY, quartZ, sampler) : resolved;
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveVisibleBiome(Engine engine, int quartX, int quartY, int quartZ,
|
||||
@@ -349,6 +373,13 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
return key != null && generatedBiomeKeys.contains(key.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
static Set<String> requireConfiguredStructureBiomeKeys(Set<String> configuredBiomeKeys) {
|
||||
if (configuredBiomeKeys == null || configuredBiomeKeys.isEmpty()) {
|
||||
throw new IllegalStateException("Iris has no configured structure biomes");
|
||||
}
|
||||
return configuredBiomeKeys;
|
||||
}
|
||||
|
||||
static boolean isGuaranteedSurfaceBiome(int quartY, int minHeight) {
|
||||
int internalY = (quartY << 2) - minHeight;
|
||||
int caveSwitchY = Math.max(-8 - minHeight, 40);
|
||||
@@ -377,15 +408,15 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
LinkedHashSet<String> possible = new LinkedHashSet<>(exactStructureBiomeKeys());
|
||||
Set<String> possible = requireConfiguredStructureBiomeKeys(exactStructureBiomeKeys());
|
||||
Registry<Biome> registry = biomeRegistry();
|
||||
if (registry != null) {
|
||||
Set<String> registered = registeredBiomeKeys(registry);
|
||||
Holder<Biome> fallback = fallbackHolder(registry);
|
||||
String fallbackKey = fallback == null ? null : holderKey(fallback);
|
||||
if (fallbackKey != null && requiresPossibleBiomeFallback(possible, registered)) {
|
||||
possible.add(fallbackKey);
|
||||
}
|
||||
if (registry == null) {
|
||||
throw new IllegalStateException("Iris cannot resolve structure biomes without the biome registry");
|
||||
}
|
||||
Set<String> missing = new LinkedHashSet<>(possible);
|
||||
missing.removeAll(registeredBiomeKeys(registry));
|
||||
if (!missing.isEmpty()) {
|
||||
throw new IllegalStateException("Iris structure biomes are not registered: " + missing);
|
||||
}
|
||||
Set<String> resolved = Set.copyOf(possible);
|
||||
possibleStructureBiomeKeys = resolved;
|
||||
@@ -393,34 +424,31 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
}
|
||||
|
||||
private Set<String> exactStructureBiomeKeys() {
|
||||
IrisModdedChunkGenerator current = generator;
|
||||
if (current == null) {
|
||||
return Set.of();
|
||||
}
|
||||
Engine engine = current.structureEngineOrNull();
|
||||
if (!isReady(engine)) {
|
||||
return current.configuredStructureBiomeKeys();
|
||||
}
|
||||
LinkedHashSet<String> possible = new LinkedHashSet<>();
|
||||
Engine engine = engineOrNull();
|
||||
if (isReady(engine)) {
|
||||
String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
|
||||
for (IrisBiome irisBiome : engine.getAllBiomes()) {
|
||||
String derivative = normalizeKey(irisBiome.getVanillaDerivativeKey());
|
||||
if (derivative != null) {
|
||||
possible.add(derivative);
|
||||
}
|
||||
if (!irisBiome.isCustom()) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
possible.add(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
|
||||
for (IrisBiome irisBiome : engine.getAllBiomes()) {
|
||||
String derivative = normalizeKey(irisBiome.getVanillaDerivativeKey());
|
||||
if (derivative != null) {
|
||||
possible.add(derivative);
|
||||
}
|
||||
if (!irisBiome.isCustom()) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
possible.add(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
IrisModdedChunkGenerator current = generator;
|
||||
if (current != null) {
|
||||
possible.addAll(current.configuredStructureBiomeKeys());
|
||||
}
|
||||
return Set.copyOf(possible);
|
||||
}
|
||||
|
||||
static boolean requiresPossibleBiomeFallback(Set<String> configured, Set<String> registered) {
|
||||
return configured.isEmpty() || !registered.containsAll(configured);
|
||||
}
|
||||
|
||||
private static Set<String> registeredBiomeKeys(Registry<Biome> registry) {
|
||||
Set<String> registered = new HashSet<>();
|
||||
registry.listElements().forEach((Holder.Reference<Biome> reference) -> {
|
||||
@@ -459,18 +487,6 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
return identifier == null ? null : identifier.toString().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private static Holder<Biome> fallbackHolder(Registry<Biome> registry) {
|
||||
if (registry == null) {
|
||||
return null;
|
||||
}
|
||||
Holder<Biome> plains = resolveHolder(registry, "minecraft:plains");
|
||||
if (plains != null) {
|
||||
return plains;
|
||||
}
|
||||
return registry.listElements().findFirst().<Holder<Biome>>map(
|
||||
(Holder.Reference<Biome> reference) -> reference).orElse(null);
|
||||
}
|
||||
|
||||
private Holder<Biome> fallbackBiome(Registry<Biome> registry, int quartX, int quartY, int quartZ,
|
||||
Climate.Sampler sampler) {
|
||||
Holder<Biome> plains = resolveHolder(registry, "minecraft:plains");
|
||||
|
||||
+220
-100
@@ -19,15 +19,19 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
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.IrisVanillaStructureStiltSettings;
|
||||
import art.arcane.iris.engine.object.IrisStructureStiltSettings;
|
||||
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
||||
import art.arcane.iris.nativegen.NativeStructureGenerationException;
|
||||
import art.arcane.iris.nativegen.NativeStructureLocateResults;
|
||||
import art.arcane.iris.nativegen.NativeStructurePostProcessor;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
@@ -99,6 +103,7 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -208,11 +213,62 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
public synchronized void repoint(String pack, String packDimensionKey, long seed) {
|
||||
ServerLevel level = boundLevel();
|
||||
if (level != null) {
|
||||
ModdedWorldEngines.evict(level);
|
||||
repointAndBind(level, pack, packDimensionKey, seed);
|
||||
return;
|
||||
}
|
||||
applyUnboundConfiguration(pack, packDimensionKey, seed);
|
||||
}
|
||||
|
||||
synchronized void repointAndBind(ServerLevel level, String pack, String packDimensionKey, long seed) {
|
||||
if (level.getChunkSource().getGenerator() != this) {
|
||||
throw new IllegalArgumentException("ServerLevel does not use Iris generator '" + dimensionKey + "'");
|
||||
}
|
||||
requireGlobalStructureGeneration(
|
||||
level.getServer().getWorldGenSettings().options().generateStructures(), dimensionKey);
|
||||
Engine replacement = ModdedWorldEngines.prepareReplacement(level, pack, packDimensionKey, seed);
|
||||
try {
|
||||
ModdedWorldEngines.installReplacement(level, replacement);
|
||||
} catch (Throwable error) {
|
||||
try {
|
||||
ModdedWorldEngines.closeUnregistered(replacement);
|
||||
} catch (Throwable cleanupError) {
|
||||
error.addSuppressed(cleanupError);
|
||||
}
|
||||
if (error instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
if (error instanceof Error fatalError) {
|
||||
throw fatalError;
|
||||
}
|
||||
throw new IllegalStateException("Iris generator '" + dimensionKey + "' failed to replace its engine", error);
|
||||
}
|
||||
this.activePack = pack;
|
||||
this.activeDimensionKey = packDimensionKey;
|
||||
this.seedOverride = seed;
|
||||
this.engine = replacement;
|
||||
this.configuredStructureBiomeKeys = null;
|
||||
this.configuredPack = null;
|
||||
this.engineBinding.reset();
|
||||
this.engineBinding.complete(replacement);
|
||||
this.announced.set(false);
|
||||
this.structureBiomeSource.clearCaches();
|
||||
this.worldCheckStructureShifts.clear();
|
||||
resetVanillaSpawnBiomes();
|
||||
}
|
||||
|
||||
public synchronized void unbindEngine() {
|
||||
ServerLevel level = boundLevel();
|
||||
unbindEngine(level);
|
||||
}
|
||||
|
||||
synchronized void unbindEngine(ServerLevel level) {
|
||||
if (level != null) {
|
||||
ModdedWorldEngines.evictOrThrow(level);
|
||||
}
|
||||
clearEngineBinding();
|
||||
}
|
||||
|
||||
private void clearEngineBinding() {
|
||||
this.engine = null;
|
||||
this.configuredStructureBiomeKeys = null;
|
||||
this.configuredPack = null;
|
||||
@@ -223,11 +279,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
resetVanillaSpawnBiomes();
|
||||
}
|
||||
|
||||
public synchronized void unbindEngine() {
|
||||
ServerLevel level = boundLevel();
|
||||
if (level != null) {
|
||||
ModdedWorldEngines.evict(level);
|
||||
}
|
||||
private void applyUnboundConfiguration(String pack, String packDimensionKey, long seed) {
|
||||
this.activePack = pack;
|
||||
this.activeDimensionKey = packDimensionKey;
|
||||
this.seedOverride = seed;
|
||||
this.engine = null;
|
||||
this.configuredStructureBiomeKeys = null;
|
||||
this.configuredPack = null;
|
||||
@@ -265,33 +320,14 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
public Pair<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel level, HolderSet<Structure> holders,
|
||||
BlockPos pos, int radius,
|
||||
boolean findUnexplored) {
|
||||
Engine current = engineOrNull();
|
||||
if (current == null) {
|
||||
return super.findNearestMapStructure(level, holders, pos, radius, findUnexplored);
|
||||
}
|
||||
try {
|
||||
Pair<BlockPos, Holder<Structure>> irisPlaced = findNearestIrisStructure(
|
||||
level, holders, pos, Math.max(1, radius), current);
|
||||
if (irisPlaced != null) {
|
||||
return irisPlaced;
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris-placed structure locate failed near {},{}", pos.getX(), pos.getZ(), e);
|
||||
}
|
||||
IrisImportedStructureControl control = current.getDimension().getImportedStructures();
|
||||
if (control == null || !control.active()) {
|
||||
return null;
|
||||
}
|
||||
HolderSet<Structure> reachable = filterReachableNativeStructures(level, holders, current, control);
|
||||
if (reachable.size() == 0) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return super.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Native structure locate failed near {},{}", pos.getX(), pos.getZ(), e);
|
||||
return null;
|
||||
}
|
||||
Engine current = engine();
|
||||
Pair<BlockPos, Holder<Structure>> irisPlaced = findNearestIrisStructure(
|
||||
level, holders, pos, Math.max(1, radius), findUnexplored, current);
|
||||
HolderSet<Structure> reachable = filterReachableNativeStructures(level, holders, current);
|
||||
Pair<BlockPos, Holder<Structure>> nativeLocated = reachable.size() == 0
|
||||
? null
|
||||
: super.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
|
||||
return NativeStructureLocateResults.nearest(pos, irisPlaced, nativeLocated);
|
||||
}
|
||||
|
||||
public boolean isNativeStructureReachable(Holder<Structure> structure) {
|
||||
@@ -300,8 +336,11 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
|
||||
private Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(ServerLevel level,
|
||||
HolderSet<Structure> holders,
|
||||
BlockPos pos, int radius,
|
||||
BlockPos pos, int radius, boolean findUnexplored,
|
||||
Engine current) {
|
||||
if (findUnexplored) {
|
||||
return null;
|
||||
}
|
||||
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
BlockPos best = null;
|
||||
Holder<Structure> bestHolder = null;
|
||||
@@ -309,12 +348,19 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
for (Holder<Structure> holder : holders) {
|
||||
Identifier id = registry.getKey(holder.value());
|
||||
if (id == null) {
|
||||
throw new IllegalStateException("Native structure locate received an unregistered structure holder");
|
||||
}
|
||||
String structureId = id.toString();
|
||||
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(current,
|
||||
structureId, NativeStructurePostProcessor.isUndergroundStep(holder.value().step()));
|
||||
if (decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
|
||||
continue;
|
||||
}
|
||||
IrisStructureLocator.LocateResult result = IrisStructureLocator.locate(
|
||||
current, id.toString(), pos.getX(), pos.getZ(), radius);
|
||||
current, structureId, pos.getX(), pos.getZ(), radius);
|
||||
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
|
||||
continue;
|
||||
throw new IllegalStateException("Iris structure locate reached its safety limit for "
|
||||
+ structureId + " within " + radius + " chunks");
|
||||
}
|
||||
if (!result.found()) {
|
||||
continue;
|
||||
@@ -332,28 +378,23 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
|
||||
private HolderSet<Structure> filterReachableNativeStructures(ServerLevel level, HolderSet<Structure> holders,
|
||||
Engine current,
|
||||
IrisImportedStructureControl control) {
|
||||
try {
|
||||
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
List<Holder<Structure>> kept = new ArrayList<>(holders.size());
|
||||
for (Holder<Structure> holder : holders) {
|
||||
Identifier id = registry.getKey(holder.value());
|
||||
String key = id == null ? null : id.toString();
|
||||
IrisNativeStructureDecision decision = control.resolve(
|
||||
key, NativeStructurePostProcessor.isUndergroundStep(holder.value().step()));
|
||||
if (key == null || !decision.generate()
|
||||
|| IrisStructureLocator.suppressesVanilla(current, key)
|
||||
|| !structureBiomeSource.isStructureReachable(holder)) {
|
||||
continue;
|
||||
}
|
||||
kept.add(holder);
|
||||
Engine current) {
|
||||
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
List<Holder<Structure>> kept = new ArrayList<>(holders.size());
|
||||
for (Holder<Structure> holder : holders) {
|
||||
Identifier id = registry.getKey(holder.value());
|
||||
if (id == null) {
|
||||
throw new IllegalStateException("Native structure filtering received an unregistered structure holder");
|
||||
}
|
||||
return kept.size() == holders.size() ? holders : HolderSet.direct(kept);
|
||||
} catch (Throwable error) {
|
||||
LOGGER.error("Iris could not resolve native structure biome reachability; native locate failed closed", error);
|
||||
return HolderSet.direct(List.of());
|
||||
String key = id.toString();
|
||||
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(current,
|
||||
key, NativeStructurePostProcessor.isUndergroundStep(holder.value().step()));
|
||||
if (!decision.generate() || !structureBiomeSource.isStructureReachable(holder)) {
|
||||
continue;
|
||||
}
|
||||
kept.add(holder);
|
||||
}
|
||||
return kept.size() == holders.size() ? holders : HolderSet.direct(kept);
|
||||
}
|
||||
|
||||
private ServerLevel boundLevel() {
|
||||
@@ -389,6 +430,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
|
||||
private Engine bindEngine(ServerLevel level) {
|
||||
try {
|
||||
requireGlobalStructureGeneration(
|
||||
level.getServer().getWorldGenSettings().options().generateStructures(), dimensionKey);
|
||||
} catch (RuntimeException error) {
|
||||
engineBinding.fail(error);
|
||||
throw error;
|
||||
}
|
||||
Engine cached = engine;
|
||||
if (cached != null && !cached.isClosed() && cached.getComplex() != null) {
|
||||
engineBinding.complete(cached);
|
||||
@@ -424,6 +472,15 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
static void requireGlobalStructureGeneration(boolean enabled, String dimensionKey) {
|
||||
if (enabled) {
|
||||
return;
|
||||
}
|
||||
throw new IllegalStateException("Iris generator '" + dimensionKey
|
||||
+ "' cannot bind while generate-structures=false; set generate-structures=true, restart the server, "
|
||||
+ "and deny individual structures through importedStructures.disabled");
|
||||
}
|
||||
|
||||
private Engine engineOrNull() {
|
||||
Engine cached = engine;
|
||||
if (cached != null) {
|
||||
@@ -437,7 +494,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
|
||||
Engine structureEngineOrNull() {
|
||||
return engineOrNull();
|
||||
engineBinding.throwIfFailed(dimensionKey);
|
||||
Engine cached = engine;
|
||||
if (cached != null && !cached.isClosed() && cached.getComplex() != null) {
|
||||
return cached;
|
||||
}
|
||||
ServerLevel level = boundLevel();
|
||||
return level == null ? null : bindEngine(level);
|
||||
}
|
||||
|
||||
Engine awaitStructureEngine() {
|
||||
@@ -523,6 +586,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
if (configuredPack != null) {
|
||||
return configuredPack;
|
||||
}
|
||||
PackValidationRegistry.requireLoadable(activePack);
|
||||
File packDirectory = ModdedWorldEngines.resolvePack(activePack, activeDimensionKey);
|
||||
IrisData data = IrisData.get(packDirectory);
|
||||
IrisDimension dimension = data.getDimensionLoader().load(activeDimensionKey);
|
||||
@@ -807,19 +871,15 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
|
||||
@Override
|
||||
public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk, StructureManager structureManager) {
|
||||
if (importedControl().active()) {
|
||||
placeVanillaStructures(level, chunk, structureManager);
|
||||
}
|
||||
placeVanillaStructures(level, chunk, structureManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess chunk, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
|
||||
if (!importedControl().active()) {
|
||||
return;
|
||||
}
|
||||
Engine current = engine();
|
||||
Map<Structure, StructureStart> previousStarts = new HashMap<>(chunk.getAllStarts());
|
||||
super.createStructures(registryAccess, structureState, structureManager, chunk, templateManager, levelKey);
|
||||
adjustGeneratedStructures(registryAccess, chunk, previousStarts);
|
||||
adjustGeneratedStructures(registryAccess, chunk, previousStarts, current);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -828,9 +888,9 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
|
||||
private void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess chunk,
|
||||
Map<Structure, StructureStart> previousStarts) {
|
||||
Map<Structure, StructureStart> previousStarts, Engine current) {
|
||||
Registry<Structure> registry = registryAccess.lookupOrThrow(Registries.STRUCTURE);
|
||||
IrisImportedStructureControl control = importedControl();
|
||||
ChunkPos chunkPos = chunk.getPos();
|
||||
for (Map.Entry<Structure, StructureStart> entry : chunk.getAllStarts().entrySet()) {
|
||||
Structure structure = entry.getKey();
|
||||
StructureStart start = entry.getValue();
|
||||
@@ -839,24 +899,37 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
Identifier id = registry.getKey(structure);
|
||||
String structureId = id == null ? null : id.toString();
|
||||
IrisNativeStructureDecision decision = control.resolve(
|
||||
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
|
||||
if (!decision.generate() || IrisStructureLocator.suppressesVanilla(engine(), structureId)) {
|
||||
if (structureId == null) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"resolution", null, chunkPos.x(), chunkPos.z());
|
||||
}
|
||||
boolean undergroundStep = NativeStructurePostProcessor.isUndergroundStep(structure.step());
|
||||
IrisNativeStructureDecision decision;
|
||||
try {
|
||||
decision = NativeStructureGenerationPolicy.resolve(current,
|
||||
structureId, undergroundStep);
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"policy resolution", structureId, chunkPos.x(), chunkPos.z(), error);
|
||||
}
|
||||
if (!decision.generate()) {
|
||||
chunk.setStartForStructure(structure, StructureStart.INVALID_START);
|
||||
continue;
|
||||
}
|
||||
int offsetY;
|
||||
try {
|
||||
offsetY = NativeStructurePostProcessor.applyVerticalShift(
|
||||
offsetY = NativeStructurePostProcessor.applyVerticalPlacement(
|
||||
start,
|
||||
structureId,
|
||||
decision.yShift(),
|
||||
getSeaLevel(),
|
||||
chunk.getMinY(),
|
||||
chunk.getMinY() + chunk.getHeight());
|
||||
} catch (RuntimeException error) {
|
||||
chunk.setStartForStructure(structure, StructureStart.INVALID_START);
|
||||
LOGGER.error("Iris rejected native structure {} in chunk {},{} because its vertical bounds are invalid",
|
||||
structureId, chunk.getPos().x(), chunk.getPos().z(), error);
|
||||
continue;
|
||||
chunk.getMinY() + chunk.getHeight(),
|
||||
undergroundStep,
|
||||
(x, z) -> current.getHeight(x, z, true) + current.getMinHeight());
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"vertical adjustment", structureId, chunkPos.x(), chunkPos.z(), error);
|
||||
}
|
||||
recordWorldCheckStructureShift(structureId, start.getChunkPos(), offsetY);
|
||||
}
|
||||
@@ -864,7 +937,11 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
|
||||
private void placeVanillaStructures(WorldGenLevel world, ChunkAccess chunk, StructureManager structureManager) {
|
||||
if (!structureManager.shouldGenerateStructures()) {
|
||||
return;
|
||||
ChunkPos disabledChunk = chunk.getPos();
|
||||
throw new IllegalStateException("Iris cannot generate native structures in chunk "
|
||||
+ disabledChunk.x() + "," + disabledChunk.z()
|
||||
+ " because generate-structures=false disables them outside the pack; set generate-structures=true, "
|
||||
+ "restart the server, and deny individual structures through importedStructures.disabled");
|
||||
}
|
||||
ChunkPos chunkPos = chunk.getPos();
|
||||
SectionPos sectionPos = SectionPos.of(chunkPos, world.getMinSectionY());
|
||||
@@ -875,42 +952,61 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
long decorationSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ());
|
||||
BoundingBox area = writableArea(chunk);
|
||||
int steps = GenerationStep.Decoration.values().length;
|
||||
IrisImportedStructureControl control = importedControl();
|
||||
Engine current = engine();
|
||||
List<NativePlacementGroup> placementGroups = new ArrayList<>();
|
||||
List<StructureStart> nativeStarts = new ArrayList<>();
|
||||
List<NativeStructurePostProcessor.VegetationTarget> vegetationTargets = new ArrayList<>();
|
||||
for (int step = 0; step < steps; step++) {
|
||||
int index = 0;
|
||||
for (Structure structure : byStep.get(step)) {
|
||||
Identifier id = registry.getKey(structure);
|
||||
String structureId = id == null ? null : id.toString();
|
||||
IrisNativeStructureDecision decision = control.resolve(
|
||||
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
|
||||
if (decision.generate() && !IrisStructureLocator.suppressesVanilla(engine(), structureId)) {
|
||||
try {
|
||||
if (structureId == null) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"resolution", null, chunkPos.x(), chunkPos.z());
|
||||
}
|
||||
try {
|
||||
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(current,
|
||||
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
|
||||
if (decision.generate()) {
|
||||
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
|
||||
if (!starts.isEmpty()) {
|
||||
List<StructureStart> resolvedStarts = List.copyOf(starts);
|
||||
placementGroups.add(new NativePlacementGroup(
|
||||
structureId, decision, index, step, resolvedStarts));
|
||||
nativeStarts.addAll(resolvedStarts);
|
||||
boolean clearEntireFootprint = NativeStructurePostProcessor
|
||||
.shouldClearEntireVegetationFootprint(
|
||||
structure.step(), decision.clearVegetation());
|
||||
for (StructureStart start : resolvedStarts) {
|
||||
vegetationTargets.add(new NativeStructurePostProcessor.VegetationTarget(
|
||||
start, decision.clearVegetation()));
|
||||
start, clearEntireFootprint));
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris failed to resolve native structure {} in chunk {},{}",
|
||||
structureId, chunkPos.x(), chunkPos.z(), e);
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"resolution", structureId, chunkPos.x(), chunkPos.z(), error);
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
try {
|
||||
NativeStructurePostProcessor.prepareSurfaceStructures(
|
||||
world, area, nativeStarts,
|
||||
(x, z) -> current.getHeight(x, z, true) + current.getMinHeight());
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"terrain integration", nativeStructureBatchContext(placementGroups),
|
||||
chunkPos.x(), chunkPos.z(), error);
|
||||
}
|
||||
try {
|
||||
NativeStructurePostProcessor.clearIntersectingVegetation(
|
||||
world, chunk, area, vegetationTargets);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris failed to clear vegetation from native structures in chunk {},{}",
|
||||
chunkPos.x(), chunkPos.z(), e);
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"vegetation cleanup", nativeStructureBatchContext(placementGroups),
|
||||
chunkPos.x(), chunkPos.z(), error);
|
||||
}
|
||||
for (NativePlacementGroup group : placementGroups) {
|
||||
random.setFeatureSeed(decorationSeed, group.featureIndex(), group.step());
|
||||
@@ -919,13 +1015,27 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
placeVanillaStructure(world, structureManager, random, area, chunkPos,
|
||||
group.structureId(), start, group.decision());
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris failed to place native structure {} in chunk {},{}",
|
||||
group.structureId(), chunkPos.x(), chunkPos.z(), e);
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"placement", group.structureId(), chunkPos.x(), chunkPos.z(), error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String nativeStructureBatchContext(List<NativePlacementGroup> placementGroups) {
|
||||
if (placementGroups.isEmpty()) {
|
||||
return "<no resolved native structures>";
|
||||
}
|
||||
StringBuilder context = new StringBuilder("[");
|
||||
for (int i = 0; i < placementGroups.size(); i++) {
|
||||
if (i > 0) {
|
||||
context.append(", ");
|
||||
}
|
||||
context.append(placementGroups.get(i).structureId());
|
||||
}
|
||||
return context.append(']').toString();
|
||||
}
|
||||
|
||||
private void placeVanillaStructure(WorldGenLevel world, StructureManager structureManager,
|
||||
WorldgenRandom random, BoundingBox area, ChunkPos chunkPos,
|
||||
String structureId, StructureStart start,
|
||||
@@ -978,22 +1088,19 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
return worldCheckStructureShifts.get(new NativeStructureStartKey(structureId, startChunk.pack()));
|
||||
}
|
||||
|
||||
private BlockState resolveStiltBlock(IrisVanillaStructureStiltSettings settings, RNG rng,
|
||||
private BlockState resolveStiltBlock(IrisStructureStiltSettings settings, RNG rng,
|
||||
int x, int y, int z) {
|
||||
if (settings.getPalette() == null) {
|
||||
return Blocks.COBBLESTONE.defaultBlockState();
|
||||
}
|
||||
PlatformBlockState platformState = settings.getPalette().get(rng, x, y, z, engine().getData());
|
||||
if (platformState == null || !(platformState.nativeHandle() instanceof BlockState blockState)) {
|
||||
return Blocks.COBBLESTONE.defaultBlockState();
|
||||
throw new IllegalStateException("Configured native structure stilt palette did not resolve a Minecraft block at "
|
||||
+ x + "," + y + "," + z);
|
||||
}
|
||||
return blockState;
|
||||
}
|
||||
|
||||
private IrisImportedStructureControl importedControl() {
|
||||
return engine().getDimension().getImportedStructures();
|
||||
}
|
||||
|
||||
private BoundingBox writableArea(ChunkAccess chunk) {
|
||||
ChunkPos chunkPos = chunk.getPos();
|
||||
int minX = chunkPos.getMinBlockX();
|
||||
@@ -1128,6 +1235,19 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
future.completeExceptionally(error);
|
||||
}
|
||||
|
||||
void throwIfFailed(String dimensionKey) {
|
||||
CompletableFuture<T> current = future;
|
||||
if (!current.isCompletedExceptionally()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
current.join();
|
||||
} catch (CompletionException error) {
|
||||
throw new IllegalStateException("Iris generator '" + dimensionKey + "' failed to bind",
|
||||
error.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
future = new CompletableFuture<>();
|
||||
}
|
||||
|
||||
+2
-1
@@ -20,6 +20,7 @@ package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.LootResolver;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBlockData;
|
||||
import art.arcane.iris.engine.object.IrisBlockDrops;
|
||||
@@ -145,7 +146,7 @@ public final class ModdedBlockBreakHandler {
|
||||
for (IrisBlockDrops provider : providers) {
|
||||
replaceVanillaDrops |= provider.isReplaceVanillaDrops();
|
||||
for (IrisLoot loot : provider.getDrops()) {
|
||||
if (RNG.r.i(1, loot.getRarity()) != loot.getRarity()) {
|
||||
if (!LootResolver.oneIn(RNG.r, loot.getRarity())) {
|
||||
continue;
|
||||
}
|
||||
ItemStack stack = ModdedItemTranslator.stack(loot, RNG.r, level);
|
||||
|
||||
@@ -38,7 +38,6 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class ModdedDeathLoot {
|
||||
private static final String TAG_PREFIX = "iris_loot|";
|
||||
private static final int LOOT_RNG_SALT = 345911;
|
||||
private static final Set<String> WARNED_TABLES = ConcurrentHashMap.newKeySet();
|
||||
private static final Set<String> WARNED_ENTITY_TYPES = ConcurrentHashMap.newKeySet();
|
||||
|
||||
@@ -112,8 +111,7 @@ public final class ModdedDeathLoot {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
RNG lootRng = new RNG(binding.seed()).nextParallelRNG(LOOT_RNG_SALT);
|
||||
drops.addAll(ModdedItemTranslator.loot(table, lootRng, InventorySlotType.STORAGE, level,
|
||||
drops.addAll(ModdedItemTranslator.loot(table, engine.getSeedManager().getLoot(), InventorySlotType.STORAGE, level,
|
||||
binding.spawnX(), binding.spawnY(), binding.spawnZ()));
|
||||
}
|
||||
return drops;
|
||||
|
||||
+74
-15
@@ -68,7 +68,13 @@ public final class ModdedDimensionManager {
|
||||
private ModdedDimensionManager() {
|
||||
}
|
||||
|
||||
public static void bindAccess(ModdedServerAccess serverAccess) {
|
||||
public static synchronized ModdedServerAccess bindAccess(ModdedServerAccess serverAccess) {
|
||||
ModdedServerAccess previous = access;
|
||||
access = serverAccess;
|
||||
return previous;
|
||||
}
|
||||
|
||||
public static synchronized void restoreAccess(ModdedServerAccess serverAccess) {
|
||||
access = serverAccess;
|
||||
}
|
||||
|
||||
@@ -110,12 +116,13 @@ public final class ModdedDimensionManager {
|
||||
}
|
||||
|
||||
public static Handle create(MinecraftServer server, String dimensionId, String pack, String packDimensionKey, long seed) {
|
||||
ModdedStartup.requirePackForWorldCreation(pack);
|
||||
ModdedServerAccess serverAccess = requireAccess();
|
||||
synchronized (LOCK) {
|
||||
ResourceKey<Level> key = levelKey(dimensionId);
|
||||
Handle existing = HANDLES.get(dimensionId);
|
||||
if (existing != null && serverAccess.hasLevel(server, key)) {
|
||||
existing.generator().repoint(pack, packDimensionKey, seed);
|
||||
existing.generator().repointAndBind(existing.level(), pack, packDimensionKey, seed);
|
||||
Handle refreshed = new Handle(dimensionId, pack, packDimensionKey, seed, existing.level(), existing.generator());
|
||||
HANDLES.put(dimensionId, refreshed);
|
||||
return refreshed;
|
||||
@@ -126,7 +133,7 @@ public final class ModdedDimensionManager {
|
||||
throw new IllegalStateException("Iris cannot inject dimension '" + dimensionId + "': a non-Iris level with that id is already loaded");
|
||||
}
|
||||
LOGGER.warn("Iris dimension '{}' is already present in the running server; reusing it", dimensionId);
|
||||
generator.repoint(pack, packDimensionKey, seed);
|
||||
generator.repointAndBind(present, pack, packDimensionKey, seed);
|
||||
Handle handle = new Handle(dimensionId, pack, packDimensionKey, seed, present, generator);
|
||||
HANDLES.put(dimensionId, handle);
|
||||
return handle;
|
||||
@@ -231,10 +238,6 @@ public final class ModdedDimensionManager {
|
||||
private static Holder<DimensionType> resolveDimensionType(RegistryAccess registryAccess, String pack, String packDimensionKey) {
|
||||
Registry<DimensionType> registry = registryAccess.lookupOrThrow(Registries.DIMENSION_TYPE);
|
||||
IrisDimension dimension = loadPackDimension(pack, packDimensionKey);
|
||||
if (dimension == null) {
|
||||
throw new IllegalStateException("Iris cannot resolve dimension type for missing pack dimension '"
|
||||
+ packDimensionKey + "' in pack '" + pack + "'");
|
||||
}
|
||||
String typeRef = ModdedForcedDatapack.dimensionTypeRef(dimension);
|
||||
ResourceKey<DimensionType> typeKey = ResourceKey.create(Registries.DIMENSION_TYPE, Identifier.parse(typeRef));
|
||||
Holder.Reference<DimensionType> packType = ModdedForcedDatapack.requireRegisteredDimensionType(
|
||||
@@ -246,12 +249,22 @@ public final class ModdedDimensionManager {
|
||||
try {
|
||||
File packFolder = ModdedWorldEngines.packFolder(pack);
|
||||
if (!packFolder.isDirectory()) {
|
||||
return null;
|
||||
throw new IllegalStateException("Iris pack directory is missing: " + packFolder.getAbsolutePath());
|
||||
}
|
||||
return IrisData.get(packFolder).getDimensionLoader().load(packDimensionKey);
|
||||
IrisDimension dimension = IrisData.get(packFolder).getDimensionLoader().load(packDimensionKey);
|
||||
if (dimension == null) {
|
||||
throw new IllegalStateException("Iris pack '" + pack
|
||||
+ "' does not contain dimension '" + packDimensionKey + "'");
|
||||
}
|
||||
return dimension;
|
||||
} catch (Throwable e) {
|
||||
LOGGER.warn("Iris could not load pack '{}' dimension '{}' for dimension type resolution: {}", pack, packDimensionKey, e.toString());
|
||||
return null;
|
||||
LOGGER.error("Iris could not load pack '{}' dimension '{}' for dimension type resolution",
|
||||
pack, packDimensionKey, e);
|
||||
if (e instanceof Error fatalError) {
|
||||
throw fatalError;
|
||||
}
|
||||
throw new IllegalStateException("Iris could not load pack '" + pack + "' dimension '"
|
||||
+ packDimensionKey + "' for dimension type resolution", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,10 +298,56 @@ public final class ModdedDimensionManager {
|
||||
List.of(),
|
||||
false);
|
||||
|
||||
serverAccess.putLevel(server, key, level);
|
||||
generator.bindLevel(level);
|
||||
server.getPlayerList().addWorldborderListener(level);
|
||||
return new Handle(dimensionId, pack, packDimensionKey, seed, level, generator);
|
||||
try {
|
||||
generator.bindLevel(level);
|
||||
Handle handle = new Handle(dimensionId, pack, packDimensionKey, seed, level, generator);
|
||||
ServerLevel previous = serverAccess.putLevelIfAbsent(server, key, level);
|
||||
if (previous != null) {
|
||||
throw new IllegalStateException("Iris cannot inject dimension '" + dimensionId
|
||||
+ "': the level was registered concurrently");
|
||||
}
|
||||
server.getPlayerList().addWorldborderListener(level);
|
||||
return handle;
|
||||
} catch (Throwable error) {
|
||||
rollbackInjection(server, serverAccess, key, level, generator, error);
|
||||
if (error instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
if (error instanceof Error fatalError) {
|
||||
throw fatalError;
|
||||
}
|
||||
throw new IllegalStateException("Iris runtime dimension injection failed for " + dimensionId, error);
|
||||
}
|
||||
}
|
||||
|
||||
private static void rollbackInjection(MinecraftServer server, ModdedServerAccess serverAccess,
|
||||
ResourceKey<Level> key, ServerLevel level,
|
||||
IrisModdedChunkGenerator generator, Throwable failure) {
|
||||
try {
|
||||
if (serverAccess.hasLevel(server, key)) {
|
||||
ServerLevel removed = serverAccess.removeLevel(server, key);
|
||||
if (removed != null && removed != level) {
|
||||
serverAccess.putLevel(server, key, removed);
|
||||
throw new IllegalStateException("Iris injection rollback encountered another registered level for "
|
||||
+ key.identifier());
|
||||
}
|
||||
}
|
||||
} catch (Throwable cleanupError) {
|
||||
failure.addSuppressed(cleanupError);
|
||||
LOGGER.error("Iris failed to remove a partially injected level for {}", key.identifier(), cleanupError);
|
||||
}
|
||||
try {
|
||||
generator.unbindEngine(level);
|
||||
} catch (Throwable cleanupError) {
|
||||
failure.addSuppressed(cleanupError);
|
||||
LOGGER.error("Iris failed to close a partially bound engine for {}", key.identifier(), cleanupError);
|
||||
}
|
||||
try {
|
||||
level.close();
|
||||
} catch (Throwable cleanupError) {
|
||||
failure.addSuppressed(cleanupError);
|
||||
LOGGER.error("Iris failed to close a partially injected level for {}", key.identifier(), cleanupError);
|
||||
}
|
||||
}
|
||||
|
||||
public static int evacuate(MinecraftServer server, ServerLevel from) {
|
||||
|
||||
+144
-42
@@ -42,6 +42,7 @@ import art.arcane.iris.modded.service.ModdedLogFilterService;
|
||||
import art.arcane.iris.modded.service.ModdedPreservationService;
|
||||
import art.arcane.iris.modded.service.ModdedSettingsHotloadService;
|
||||
import art.arcane.iris.modded.service.ModdedStudioHotloadService;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import net.minecraft.core.BlockPos;
|
||||
@@ -53,6 +54,8 @@ import net.minecraft.world.level.storage.LevelData;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
|
||||
public final class ModdedEngineBootstrap {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final String[] CORE_SELF_TEST_CLASSES = {
|
||||
@@ -61,9 +64,9 @@ public final class ModdedEngineBootstrap {
|
||||
"art.arcane.iris.core.loader.IrisData"
|
||||
};
|
||||
private static final Object LOCK = new Object();
|
||||
private static final ModdedServiceManager SERVICE_MANAGER = new ModdedServiceManager();
|
||||
private static final ModdedServiceManager UNBOUND_SERVICE_MANAGER = new ModdedServiceManager();
|
||||
private static volatile ModdedLoader loader;
|
||||
private static volatile ModdedPlatform platform;
|
||||
private static volatile BoundRuntime runtime;
|
||||
private static volatile MinecraftServer currentServer;
|
||||
private static volatile MinecraftServer spawnCaptureServer;
|
||||
private static volatile boolean initialSpawnWasDefault;
|
||||
@@ -72,19 +75,20 @@ public final class ModdedEngineBootstrap {
|
||||
}
|
||||
|
||||
public static ModdedServiceManager services() {
|
||||
return SERVICE_MANAGER;
|
||||
BoundRuntime bound = runtime;
|
||||
return bound == null ? UNBOUND_SERVICE_MANAGER : bound.services();
|
||||
}
|
||||
|
||||
public static ModdedScheduler schedulerOrNull() {
|
||||
ModdedPlatform bound = platform;
|
||||
return bound == null ? null : bound.moddedScheduler();
|
||||
BoundRuntime bound = runtime;
|
||||
return bound == null ? null : bound.platform().moddedScheduler();
|
||||
}
|
||||
|
||||
public static void tick(MinecraftServer server) {
|
||||
ModdedScheduler.tick(server);
|
||||
ModdedStartup.runOnce(server);
|
||||
ModdedPrimaryWorldRouter.tick(server);
|
||||
SERVICE_MANAGER.tick(server);
|
||||
services().tick(server);
|
||||
ModdedPregenBossBar.tick(server);
|
||||
ModdedProtocolHandler.tickDimensionSync(server);
|
||||
}
|
||||
@@ -93,14 +97,15 @@ public final class ModdedEngineBootstrap {
|
||||
captureInitialSpawn(server);
|
||||
currentServer = server;
|
||||
bind();
|
||||
bindWorldGenerators(server);
|
||||
ModdedStartup.reset();
|
||||
ModdedScheduler scheduler = schedulerOrNull();
|
||||
if (scheduler != null) {
|
||||
scheduler.reset();
|
||||
}
|
||||
ModdedStartup.prepareForStartup();
|
||||
IrisModdedChunkGenerator.startGenPool();
|
||||
SERVICE_MANAGER.enableAll();
|
||||
bindWorldGenerators(server);
|
||||
ModdedStartup.runOnce(server);
|
||||
services().enableAll();
|
||||
ModdedProtocolHandler.start(server);
|
||||
ModdedSentry.start(loader());
|
||||
}
|
||||
@@ -115,6 +120,7 @@ public final class ModdedEngineBootstrap {
|
||||
|
||||
public static void serverAboutToStart(MinecraftServer server) {
|
||||
captureInitialSpawn(server);
|
||||
ModdedStartup.prepareForStartup();
|
||||
}
|
||||
|
||||
public static void serverStarted(MinecraftServer server) {
|
||||
@@ -140,7 +146,7 @@ public final class ModdedEngineBootstrap {
|
||||
ModdedStudioCommands.clear();
|
||||
ModdedWorldEngines.shutdown();
|
||||
ModdedPrimaryWorldRouter.clear();
|
||||
SERVICE_MANAGER.disableAll();
|
||||
services().disableAll();
|
||||
ModdedDimensionManager.clear();
|
||||
ModdedScheduler scheduler = schedulerOrNull();
|
||||
if (scheduler != null) {
|
||||
@@ -256,45 +262,141 @@ public final class ModdedEngineBootstrap {
|
||||
}
|
||||
|
||||
public static ModdedPlatform bind() {
|
||||
ModdedPlatform bound = platform;
|
||||
BoundRuntime bound = runtime;
|
||||
if (bound != null) {
|
||||
return bound;
|
||||
return bound.platform();
|
||||
}
|
||||
synchronized (LOCK) {
|
||||
if (platform != null) {
|
||||
return platform;
|
||||
BoundRuntime synchronizedBound = runtime;
|
||||
if (synchronizedBound != null) {
|
||||
return synchronizedBound.platform();
|
||||
}
|
||||
ModdedLoader boundLoader = loader();
|
||||
GuiHost.suppressDesktop(boundLoader.clientEnvironment());
|
||||
ModdedPlatform created = new ModdedPlatform(boundLoader);
|
||||
IrisPlatforms.bind(created);
|
||||
ModdedDimensionManager.bindAccess(new ModdedServerLevels());
|
||||
IrisObjectRotation.bindFallbackRotator(new ModdedStateRotator());
|
||||
BlockDataMergeSupport.bindFallbackMerger(new ModdedStateMerger());
|
||||
TileData.bindFallbackReader(new ModdedTileReader(boundLoader::currentServer));
|
||||
TileData.bindFallbackFactory(ModdedTileData::fromProperties);
|
||||
ModdedGuiHost.install();
|
||||
ModdedDecoratorHooks decoratorHooks = new ModdedDecoratorHooks();
|
||||
DecoratorPlatformHooks.bind(decoratorHooks, decoratorHooks);
|
||||
ModdedPreservationService preservation = SERVICE_MANAGER.register(ModdedPreservationService.class, new ModdedPreservationService());
|
||||
SERVICE_MANAGER.register(ModdedLogFilterService.class, new ModdedLogFilterService());
|
||||
SERVICE_MANAGER.register(ModdedEngineMaintenanceService.class, new ModdedEngineMaintenanceService());
|
||||
SERVICE_MANAGER.register(ModdedSettingsHotloadService.class, new ModdedSettingsHotloadService());
|
||||
ModdedStudioHotloadService studioHotloadService = SERVICE_MANAGER.register(ModdedStudioHotloadService.class, new ModdedStudioHotloadService());
|
||||
SERVICE_MANAGER.register(ModdedChunkUpdateService.class, new ModdedChunkUpdateService());
|
||||
SERVICE_MANAGER.register(ModdedEntitySpawnService.class, new ModdedEntitySpawnService());
|
||||
IrisServices.register(PreservationRegistry.class, preservation);
|
||||
IrisServices.register(EngineEffectsProvider.class, (EngineEffectsProvider) ModdedEngineEffects::new);
|
||||
IrisServices.register(EnginePlatformHooks.class, studioHotloadService);
|
||||
IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> new ModdedWorldManager(engine));
|
||||
ModdedCustomContentRegistry.discover();
|
||||
platform = created;
|
||||
SERVICE_MANAGER.enableAll();
|
||||
if (boundLoader.clientEnvironment()) {
|
||||
ModdedStartup.prefetchDefaultPack();
|
||||
ModdedServiceManager createdServices = new ModdedServiceManager();
|
||||
BindRollback rollback = new BindRollback();
|
||||
try {
|
||||
boolean previousDesktopSuppression = GuiHost.isDesktopSuppressed();
|
||||
GuiHost.suppressDesktop(boundLoader.clientEnvironment());
|
||||
rollback.add(() -> GuiHost.suppressDesktop(previousDesktopSuppression));
|
||||
|
||||
IrisPlatform previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
|
||||
IrisPlatforms.bind(created);
|
||||
rollback.add(() -> restorePlatform(previousPlatform));
|
||||
|
||||
ModdedServerAccess previousServerAccess = ModdedDimensionManager.bindAccess(new ModdedServerLevels());
|
||||
rollback.add(() -> ModdedDimensionManager.restoreAccess(previousServerAccess));
|
||||
|
||||
IrisObjectRotation.StateRotator previousRotator = IrisObjectRotation.bindPlatformRotator(new ModdedStateRotator());
|
||||
rollback.add(() -> IrisObjectRotation.restorePlatformRotator(previousRotator));
|
||||
|
||||
BlockDataMergeSupport.StateMerger previousMerger = BlockDataMergeSupport.bindPlatformMerger(new ModdedStateMerger());
|
||||
rollback.add(() -> BlockDataMergeSupport.restorePlatformMerger(previousMerger));
|
||||
|
||||
TileData.TileReader previousTileReader = TileData.bindPlatformReader(new ModdedTileReader(boundLoader::currentServer));
|
||||
rollback.add(() -> TileData.restorePlatformReader(previousTileReader));
|
||||
|
||||
TileData.TileFactory previousTileFactory = TileData.bindPlatformFactory(ModdedTileData::fromProperties);
|
||||
rollback.add(() -> TileData.restorePlatformFactory(previousTileFactory));
|
||||
|
||||
GuiHost.Provider previousGuiHost = GuiHost.get();
|
||||
ModdedGuiHost.install();
|
||||
rollback.add(() -> GuiHost.set(previousGuiHost));
|
||||
|
||||
ModdedDecoratorHooks decoratorHooks = new ModdedDecoratorHooks();
|
||||
DecoratorPlatformHooks.Bindings previousDecoratorBindings = DecoratorPlatformHooks.bind(decoratorHooks, decoratorHooks);
|
||||
rollback.add(() -> DecoratorPlatformHooks.restore(previousDecoratorBindings));
|
||||
|
||||
ModdedPreservationService preservation = createdServices.register(
|
||||
ModdedPreservationService.class, new ModdedPreservationService());
|
||||
createdServices.register(ModdedLogFilterService.class, new ModdedLogFilterService());
|
||||
createdServices.register(ModdedEngineMaintenanceService.class, new ModdedEngineMaintenanceService());
|
||||
createdServices.register(ModdedSettingsHotloadService.class, new ModdedSettingsHotloadService());
|
||||
ModdedStudioHotloadService studioHotloadService = createdServices.register(
|
||||
ModdedStudioHotloadService.class, new ModdedStudioHotloadService());
|
||||
createdServices.register(ModdedChunkUpdateService.class, new ModdedChunkUpdateService());
|
||||
createdServices.register(ModdedEntitySpawnService.class, new ModdedEntitySpawnService());
|
||||
|
||||
bindService(PreservationRegistry.class, preservation, rollback);
|
||||
bindService(EngineEffectsProvider.class,
|
||||
(EngineEffectsProvider) ModdedEngineEffects::new, rollback);
|
||||
bindService(EnginePlatformHooks.class, studioHotloadService, rollback);
|
||||
bindService(EngineWorldManagerProvider.class,
|
||||
(EngineWorldManagerProvider) (Engine engine) -> new ModdedWorldManager(engine), rollback);
|
||||
|
||||
ModdedCustomContentRegistry.Discovery customContentDiscovery =
|
||||
ModdedCustomContentRegistry.discover();
|
||||
rollback.add(customContentDiscovery::rollback);
|
||||
ModdedIrisSplash.print(boundLoader);
|
||||
createdServices.enableAll();
|
||||
runtime = new BoundRuntime(created, createdServices);
|
||||
rollback.clear();
|
||||
return created;
|
||||
} catch (Throwable failure) {
|
||||
createdServices.rollback(failure);
|
||||
rollback.restore(failure);
|
||||
LOGGER.error("Iris modded platform binding failed", failure);
|
||||
if (failure instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
if (failure instanceof Error fatalError) {
|
||||
throw fatalError;
|
||||
}
|
||||
throw new IllegalStateException("Iris modded platform binding failed", failure);
|
||||
}
|
||||
ModdedIrisSplash.print(boundLoader);
|
||||
return created;
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> void bindService(Class<T> type, T service, BindRollback rollback) {
|
||||
T previous = IrisServices.getOrNull(type);
|
||||
IrisServices.register(type, service);
|
||||
rollback.add(() -> restoreService(type, previous));
|
||||
}
|
||||
|
||||
private static <T> void restoreService(Class<T> type, T service) {
|
||||
if (service == null) {
|
||||
IrisServices.remove(type);
|
||||
return;
|
||||
}
|
||||
IrisServices.register(type, service);
|
||||
}
|
||||
|
||||
private static void restorePlatform(IrisPlatform platform) {
|
||||
IrisPlatforms.unbind();
|
||||
if (platform != null) {
|
||||
IrisPlatforms.bind(platform);
|
||||
}
|
||||
}
|
||||
|
||||
private record BoundRuntime(ModdedPlatform platform, ModdedServiceManager services) {
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface RollbackAction {
|
||||
void restore() throws Throwable;
|
||||
}
|
||||
|
||||
private static final class BindRollback {
|
||||
private final ArrayDeque<RollbackAction> actions = new ArrayDeque<>();
|
||||
|
||||
void add(RollbackAction action) {
|
||||
actions.push(action);
|
||||
}
|
||||
|
||||
void restore(Throwable failure) {
|
||||
while (!actions.isEmpty()) {
|
||||
try {
|
||||
actions.pop().restore();
|
||||
} catch (Throwable rollbackFailure) {
|
||||
if (rollbackFailure != failure) {
|
||||
failure.addSuppressed(rollbackFailure);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void clear() {
|
||||
actions.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -20,6 +20,7 @@ package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.LootResolver;
|
||||
import art.arcane.iris.engine.object.IrisAttributeModifier;
|
||||
import art.arcane.iris.engine.object.IrisEntity;
|
||||
import art.arcane.iris.engine.object.IrisEffect;
|
||||
@@ -241,7 +242,7 @@ public final class ModdedEntitySpawner {
|
||||
}
|
||||
|
||||
private static void setSlot(LivingEntity living, EquipmentSlot slot, IrisLoot loot, ServerLevel level, RNG rng) {
|
||||
if (loot == null || rng.i(1, loot.getRarity()) != 1) {
|
||||
if (loot == null || !LootResolver.oneIn(rng, loot.getRarity())) {
|
||||
return;
|
||||
}
|
||||
ItemStack stack = ModdedItemTranslator.stack(loot, rng, level);
|
||||
|
||||
+101
-41
@@ -41,12 +41,15 @@ import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -62,9 +65,7 @@ public final class ModdedForcedDatapack {
|
||||
public static RepositorySource repositorySource() {
|
||||
return (Consumer<Pack> consumer) -> {
|
||||
Pack pack = buildPack();
|
||||
if (pack != null) {
|
||||
consumer.accept(pack);
|
||||
}
|
||||
consumer.accept(pack);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -78,9 +79,10 @@ public final class ModdedForcedDatapack {
|
||||
|
||||
private static Pack buildPack() {
|
||||
Path directory = regenerate();
|
||||
if (directory == null) {
|
||||
return null;
|
||||
}
|
||||
return requireReadablePack(directory);
|
||||
}
|
||||
|
||||
private static Pack requireReadablePack(Path directory) {
|
||||
PackLocationInfo location = new PackLocationInfo(
|
||||
PACK_ID,
|
||||
Component.literal("Iris World Generation"),
|
||||
@@ -90,7 +92,8 @@ public final class ModdedForcedDatapack {
|
||||
PathPackResources.PathResourcesSupplier supplier = new PathPackResources.PathResourcesSupplier(directory);
|
||||
Pack pack = Pack.readMetaAndCreate(location, supplier, PackType.SERVER_DATA, selection);
|
||||
if (pack == null) {
|
||||
LOGGER.error("Iris forced datapack at {} produced no readable pack metadata", directory);
|
||||
throw new IllegalStateException("Iris forced datapack at " + directory
|
||||
+ " produced no readable pack metadata");
|
||||
}
|
||||
return pack;
|
||||
}
|
||||
@@ -99,24 +102,41 @@ public final class ModdedForcedDatapack {
|
||||
synchronized (LOCK) {
|
||||
try {
|
||||
return write();
|
||||
} catch (IOException e) {
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris failed to generate the forced startup datapack", e);
|
||||
return null;
|
||||
if (e instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
if (e instanceof Error fatalError) {
|
||||
throw fatalError;
|
||||
}
|
||||
throw new IllegalStateException("Iris failed to generate the forced startup datapack", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Path write() throws IOException {
|
||||
ModdedStartup.ensureDefaultPack();
|
||||
Path datapackRoot = datapackRoot();
|
||||
Files.createDirectories(datapackRoot);
|
||||
Path stagingDirectory = Files.createTempDirectory(datapackRoot, PACK_FOLDER + ".staging-");
|
||||
try {
|
||||
ModdedStartup.ensureDefaultPack();
|
||||
} catch (Throwable e) {
|
||||
LOGGER.warn("Iris could not ensure the default pack before building the forced datapack", e);
|
||||
writeStagedPack(stagingDirectory);
|
||||
requireReadablePack(stagingDirectory);
|
||||
publishDirectory(stagingDirectory, packDirectory());
|
||||
return packDirectory();
|
||||
} catch (IOException | RuntimeException | Error failure) {
|
||||
try {
|
||||
clean(stagingDirectory);
|
||||
} catch (Throwable cleanupError) {
|
||||
failure.addSuppressed(cleanupError);
|
||||
}
|
||||
throw failure;
|
||||
}
|
||||
Path packDirectory = packDirectory();
|
||||
clean(packDirectory);
|
||||
Files.createDirectories(packDirectory);
|
||||
}
|
||||
|
||||
File packFolder = packDirectory.toFile();
|
||||
private static void writeStagedPack(Path stagingDirectory) throws IOException {
|
||||
File packFolder = stagingDirectory.toFile();
|
||||
KList<File> folders = new KList<>();
|
||||
folders.add(packFolder);
|
||||
Map<String, KSet<String>> seenBiomes = new LinkedHashMap<>();
|
||||
@@ -124,8 +144,13 @@ public final class ModdedForcedDatapack {
|
||||
|
||||
int packCount = 0;
|
||||
KList<String> presetIds = new KList<>();
|
||||
File[] packs = packsRoot().toFile().listFiles(File::isDirectory);
|
||||
File root = packsRoot().toFile();
|
||||
File[] packs = root.listFiles(File::isDirectory);
|
||||
if (packs == null && root.exists()) {
|
||||
throw new IOException("Iris could not read installed pack directory " + root.getAbsolutePath());
|
||||
}
|
||||
if (packs != null) {
|
||||
Arrays.sort(packs, Comparator.comparing(File::getName));
|
||||
for (File pack : packs) {
|
||||
if (installPack(pack, fixer, folders, seenBiomes, presetIds)) {
|
||||
packCount++;
|
||||
@@ -133,46 +158,52 @@ public final class ModdedForcedDatapack {
|
||||
}
|
||||
}
|
||||
|
||||
writePackMeta(packDirectory);
|
||||
writePackMeta(stagingDirectory);
|
||||
if ("forge".equalsIgnoreCase(ModdedEngineBootstrap.loader().platformName())) {
|
||||
writeForgeBlockLootModifier(packDirectory);
|
||||
writeForgeBlockLootModifier(stagingDirectory);
|
||||
}
|
||||
if (!presetIds.isEmpty()) {
|
||||
writeWorldPresetTag(packDirectory, presetIds);
|
||||
writeWorldPresetTag(stagingDirectory, presetIds);
|
||||
}
|
||||
LOGGER.info("Iris forced startup datapack regenerated: {} pack(s), {} world preset(s), {} custom biome(s) at {}", packCount, presetIds.size(), countBiomes(seenBiomes), packDirectory);
|
||||
LOGGER.info("Iris forced startup datapack staged: {} pack(s), {} world preset(s), {} custom biome(s) at {}", packCount, presetIds.size(), countBiomes(seenBiomes), stagingDirectory);
|
||||
if (packCount == 0) {
|
||||
LOGGER.warn("Iris installed NO worldgen packs into the forced datapack - custom biomes and their colors will NOT generate. Install a pack (e.g. /iris download overworld) and restart the server before creating an Iris world.");
|
||||
}
|
||||
return packDirectory;
|
||||
}
|
||||
|
||||
private static boolean installPack(File packFolder, IDataFixer fixer, KList<File> folders, Map<String, KSet<String>> seenBiomes, KList<String> presetIds) {
|
||||
private static boolean installPack(File packFolder, IDataFixer fixer, KList<File> folders,
|
||||
Map<String, KSet<String>> seenBiomes,
|
||||
KList<String> presetIds) throws IOException {
|
||||
String packName = packFolder.getName();
|
||||
File[] dimensionFiles = new File(packFolder, "dimensions").listFiles((File file) -> file.isFile() && file.getName().endsWith(".json"));
|
||||
if (dimensionFiles == null || dimensionFiles.length == 0) {
|
||||
File dimensionsDirectory = new File(packFolder, "dimensions");
|
||||
if (!dimensionsDirectory.isDirectory()) {
|
||||
return false;
|
||||
}
|
||||
boolean installed = false;
|
||||
File[] dimensionFiles = dimensionsDirectory.listFiles(
|
||||
(File file) -> file.isFile() && file.getName().endsWith(".json"));
|
||||
if (dimensionFiles == null) {
|
||||
throw new IOException("Iris could not read dimensions for pack '" + packName + "'");
|
||||
}
|
||||
if (dimensionFiles.length == 0) {
|
||||
return false;
|
||||
}
|
||||
Arrays.sort(dimensionFiles, Comparator.comparing(File::getName));
|
||||
IrisData data = IrisData.get(packFolder);
|
||||
for (File dimensionFile : dimensionFiles) {
|
||||
String dimensionKey = dimensionFile.getName().substring(0, dimensionFile.getName().length() - ".json".length());
|
||||
try {
|
||||
IrisData data = IrisData.get(packFolder);
|
||||
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
|
||||
if (dimension == null) {
|
||||
continue;
|
||||
}
|
||||
dimension.installBiomes(fixer, () -> data, folders, biomesForNamespace(seenBiomes, dimension.getLoadKey()));
|
||||
writeDimensionType(folders, fixer, dimension);
|
||||
String presetKey = dimensionKey.equals(packName) ? packName : packName + "_" + dimensionKey;
|
||||
writeWorldPreset(folders, dimension, packName, dimensionKey, presetKey);
|
||||
presetIds.add("irisworldgen:" + presetKey);
|
||||
installed = true;
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris failed to install forced datapack content for pack '{}' dimension '{}'", packName, dimensionKey, e);
|
||||
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
|
||||
if (dimension == null) {
|
||||
throw new IllegalStateException("Iris pack '" + packName + "' dimension '"
|
||||
+ dimensionKey + "' did not load while building the forced datapack");
|
||||
}
|
||||
dimension.installBiomes(fixer, () -> data, folders,
|
||||
biomesForNamespace(seenBiomes, dimension.getLoadKey()));
|
||||
writeDimensionType(folders, fixer, dimension);
|
||||
String presetKey = dimensionKey.equals(packName) ? packName : packName + "_" + dimensionKey;
|
||||
writeWorldPreset(folders, dimension, packName, dimensionKey, presetKey);
|
||||
presetIds.add("irisworldgen:" + presetKey);
|
||||
}
|
||||
return installed;
|
||||
return true;
|
||||
}
|
||||
|
||||
static KSet<String> biomesForNamespace(Map<String, KSet<String>> biomes, String namespace) {
|
||||
@@ -319,6 +350,35 @@ public final class ModdedForcedDatapack {
|
||||
}
|
||||
}
|
||||
|
||||
static void publishDirectory(Path stagingDirectory, Path publishedDirectory) throws IOException {
|
||||
Path backupDirectory = publishedDirectory.resolveSibling(
|
||||
publishedDirectory.getFileName() + ".backup-" + UUID.randomUUID());
|
||||
boolean hadPublishedDirectory = Files.exists(publishedDirectory);
|
||||
if (hadPublishedDirectory) {
|
||||
Files.move(publishedDirectory, backupDirectory, StandardCopyOption.ATOMIC_MOVE);
|
||||
}
|
||||
try {
|
||||
Files.move(stagingDirectory, publishedDirectory, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (IOException | RuntimeException | Error failure) {
|
||||
if (hadPublishedDirectory) {
|
||||
try {
|
||||
Files.move(backupDirectory, publishedDirectory, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (Throwable rollbackError) {
|
||||
failure.addSuppressed(rollbackError);
|
||||
}
|
||||
}
|
||||
throw failure;
|
||||
}
|
||||
if (hadPublishedDirectory) {
|
||||
try {
|
||||
clean(backupDirectory);
|
||||
} catch (Throwable cleanupError) {
|
||||
LOGGER.warn("Iris published the forced datapack but could not remove backup {}",
|
||||
backupDirectory, cleanupError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Path packsRoot() {
|
||||
return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("packs");
|
||||
}
|
||||
|
||||
+13
-10
@@ -18,12 +18,12 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.engine.framework.LootResolver;
|
||||
import art.arcane.iris.engine.object.InventorySlotType;
|
||||
import art.arcane.iris.engine.object.IrisAttributeModifier;
|
||||
import art.arcane.iris.engine.object.IrisEnchantment;
|
||||
import art.arcane.iris.engine.object.IrisLoot;
|
||||
import art.arcane.iris.engine.object.IrisLootTable;
|
||||
import art.arcane.iris.engine.object.NoiseStyle;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
@@ -76,23 +76,25 @@ public final class ModdedItemTranslator {
|
||||
private ModdedItemTranslator() {
|
||||
}
|
||||
|
||||
public static KList<ItemStack> loot(IrisLootTable table, RNG rng, InventorySlotType slot, ServerLevel level, int x, int y, int z) {
|
||||
public static KList<ItemStack> loot(IrisLootTable table, long lootSeed, InventorySlotType slot, ServerLevel level, int x, int y, int z) {
|
||||
KList<ItemStack> out = new KList<>();
|
||||
KList<IrisLoot> entries = table.getLoot();
|
||||
if (entries.isEmpty()) {
|
||||
if (entries.isEmpty() || table.getMaxTries() <= 0) {
|
||||
return out;
|
||||
}
|
||||
|
||||
RNG rng = LootResolver.tableRng(lootSeed, table, x, y, z);
|
||||
int m = 0;
|
||||
int c = 0;
|
||||
int mx = rng.i(table.getMinPicked(), table.getMaxPicked());
|
||||
int mx = Math.max(0, LootResolver.inclusive(rng, table.getMinPicked(), table.getMaxPicked()));
|
||||
|
||||
while (m < mx && c++ < table.getMaxTries()) {
|
||||
IrisLoot entry = entries.get(rng.i(entries.size()));
|
||||
if (entry.getSlotTypes() != slot) {
|
||||
int entryIndex = rng.nextInt(entries.size());
|
||||
IrisLoot entry = entries.get(entryIndex);
|
||||
if (entry == null || entry.getSlotTypes() != slot) {
|
||||
continue;
|
||||
}
|
||||
ItemStack item = item(entry, table, rng, level, x, y, z);
|
||||
ItemStack item = item(entry, table, rng, lootSeed, entryIndex, level, x, y, z);
|
||||
if (item != null && !item.isEmpty()) {
|
||||
out.add(item);
|
||||
m++;
|
||||
@@ -117,8 +119,9 @@ public final class ModdedItemTranslator {
|
||||
}
|
||||
}
|
||||
|
||||
public static ItemStack item(IrisLoot loot, IrisLootTable table, RNG rng, ServerLevel level, int x, int y, int z) {
|
||||
if (loot.getChance().aquire(() -> NoiseStyle.STATIC.create(rng)).fit(1, loot.getRarity() * table.getRarity(), x, y, z) != 1) {
|
||||
public static ItemStack item(IrisLoot loot, IrisLootTable table, RNG rng, long lootSeed, int entryIndex, ServerLevel level, int x, int y, int z) {
|
||||
long combinedRarity = LootResolver.combinedRarity(loot.getRarity(), table.getRarity());
|
||||
if (!LootResolver.spatialOneIn(lootSeed, table, entryIndex, x, y, z, combinedRarity)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -150,7 +153,7 @@ public final class ModdedItemTranslator {
|
||||
}
|
||||
|
||||
Item item = BuiltInRegistries.ITEM.getValue(id);
|
||||
return new ItemStack(item, Math.max(1, rng.i(loot.getMinAmount(), loot.getMaxAmount())));
|
||||
return new ItemStack(item, Math.max(1, LootResolver.inclusive(rng, loot.getMinAmount(), loot.getMaxAmount())));
|
||||
}
|
||||
|
||||
private static void applyComponents(IrisLoot loot, ItemStack stack, RNG rng, ServerLevel level) {
|
||||
|
||||
+62
-78
@@ -19,17 +19,14 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.LootResolver;
|
||||
import art.arcane.iris.engine.framework.PlacedObject;
|
||||
import art.arcane.iris.engine.object.IObjectLoot;
|
||||
import art.arcane.iris.engine.object.InventorySlotType;
|
||||
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.IrisObjectLoot;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisObjectVanillaLoot;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
@@ -42,10 +39,12 @@ import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.RandomizableContainer;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.ChestBlock;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.ChestType;
|
||||
import net.minecraft.world.level.storage.loot.LootParams;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets;
|
||||
@@ -60,8 +59,13 @@ public final class ModdedLootApplier {
|
||||
private ModdedLootApplier() {
|
||||
}
|
||||
|
||||
public static void apply(Engine engine, ServerLevel level, BlockPos pos, BlockState state, MantleChunk<Matter> mc, RNG rng, int localX, int localZ) {
|
||||
List<LootSource> sources = resolveSources(engine, level, rng, pos, state, mc);
|
||||
public static void apply(Engine engine, ServerLevel level, BlockPos pos, BlockState state, MantleChunk<Matter> mc) {
|
||||
if (!isCanonicalContainer(pos, state)) {
|
||||
return;
|
||||
}
|
||||
RNG rng = LootResolver.containerRng(engine.getSeedManager().getLoot(), pos.getX(), pos.getY(), pos.getZ());
|
||||
boolean nativeLootDefined = hasNativeLootTable(level, pos, state);
|
||||
List<LootSource> sources = resolveSources(engine, level, rng, pos, state, mc, nativeLootDefined);
|
||||
if (sources.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@@ -80,7 +84,7 @@ public final class ModdedLootApplier {
|
||||
if (table == null) {
|
||||
continue;
|
||||
}
|
||||
items.addAll(ModdedItemTranslator.loot(table, rng, InventorySlotType.STORAGE, level, localX, pos.getY(), localZ));
|
||||
items.addAll(ModdedItemTranslator.loot(table, engine.getSeedManager().getLoot(), InventorySlotType.STORAGE, level, pos.getX(), pos.getY(), pos.getZ()));
|
||||
continue;
|
||||
}
|
||||
if (source instanceof NativeLootSource nativeSource) {
|
||||
@@ -96,15 +100,18 @@ public final class ModdedLootApplier {
|
||||
fillContainer(container, items, rng);
|
||||
}
|
||||
|
||||
private static List<LootSource> resolveSources(Engine engine, ServerLevel level, RNG rng, BlockPos pos, BlockState state, MantleChunk<Matter> mc) {
|
||||
private static List<LootSource> resolveSources(Engine engine, ServerLevel level, RNG rng, BlockPos pos, BlockState state, MantleChunk<Matter> mc, boolean nativeLootDefined) {
|
||||
int rx = pos.getX();
|
||||
int rz = pos.getZ();
|
||||
int ry = pos.getY() - engine.getWorld().minHeight();
|
||||
double he = engine.getComplex().getHeightStream().get(rx, rz);
|
||||
List<LootSource> sources = new ArrayList<>();
|
||||
boolean objectLootDefined = nativeLootDefined;
|
||||
|
||||
PlacedObject po = engine.getObjectPlacement(rx, ry, rz, mc);
|
||||
if (po != null && po.getPlacement() != null && ModdedBlockResolution.isStorageChest(state)) {
|
||||
objectLootDefined = objectLootDefined
|
||||
|| po.getPlacement().getLoot().isNotEmpty()
|
||||
|| po.getPlacement().getVanillaLoot().isNotEmpty();
|
||||
Candidate picked = pickPlacementTable(engine, level, po.getPlacement(), state, rng);
|
||||
if (picked != null) {
|
||||
sources.add(picked.source());
|
||||
@@ -114,17 +121,16 @@ public final class ModdedLootApplier {
|
||||
}
|
||||
}
|
||||
|
||||
IrisRegion region = engine.getComplex().getRegionStream().get(rx, rz);
|
||||
IrisBiome biomeSurface = engine.getComplex().getTrueBiomeStream().get(rx, rz);
|
||||
IrisBiome biomeUnder = ry < he ? engine.getCaveBiome(rx, ry, rz) : biomeSurface;
|
||||
|
||||
double multiplier = 1D * engine.getDimension().getLoot().getMultiplier() * region.getLoot().getMultiplier() * biomeSurface.getLoot().getMultiplier() * biomeUnder.getLoot().getMultiplier();
|
||||
boolean fallback = sources.isEmpty();
|
||||
injectReferenceSources(sources, engine.getDimension().getLoot(), engine, fallback);
|
||||
injectReferenceSources(sources, region.getLoot(), engine, fallback);
|
||||
injectReferenceSources(sources, biomeSurface.getLoot(), engine, fallback);
|
||||
injectReferenceSources(sources, biomeUnder.getLoot(), engine, fallback);
|
||||
scaleSources(sources, multiplier, rng);
|
||||
LootResolver.resolveEnvironmentSources(
|
||||
sources,
|
||||
engine,
|
||||
rng,
|
||||
rx,
|
||||
ry,
|
||||
rz,
|
||||
objectLootDefined,
|
||||
IrisLootSource::new
|
||||
);
|
||||
return sources;
|
||||
}
|
||||
|
||||
@@ -134,7 +140,7 @@ public final class ModdedLootApplier {
|
||||
List<Candidate> global = new ArrayList<>();
|
||||
|
||||
for (IrisObjectLoot loot : placement.getLoot()) {
|
||||
if (loot == null) {
|
||||
if (loot == null || loot.getWeight() <= 0) {
|
||||
continue;
|
||||
}
|
||||
IrisLootTable table = engine.getData().getLootLoader().load(loot.getName());
|
||||
@@ -146,7 +152,7 @@ public final class ModdedLootApplier {
|
||||
}
|
||||
|
||||
for (IrisObjectVanillaLoot loot : placement.getVanillaLoot()) {
|
||||
if (loot == null) {
|
||||
if (loot == null || loot.getWeight() <= 0) {
|
||||
continue;
|
||||
}
|
||||
ResourceKey<LootTable> key = resolveNativeKey(loot.getName(), candidate -> hasNativeTable(level, candidate));
|
||||
@@ -162,30 +168,7 @@ public final class ModdedLootApplier {
|
||||
}
|
||||
|
||||
List<Candidate> pool = !exact.isEmpty() ? exact : !basic.isEmpty() ? basic : global;
|
||||
return pickWeighted(pool, rng);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
static <T> void scaleSources(List<T> sources, double multiplier, RNG rng) {
|
||||
if (sources.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
int target = (int) Math.round(sources.size() * multiplier);
|
||||
while (sources.size() < target && !sources.isEmpty()) {
|
||||
sources.add(sources.get(rng.i(sources.size() - 1)));
|
||||
}
|
||||
while (sources.size() > target && !sources.isEmpty()) {
|
||||
sources.remove(rng.i(sources.size() - 1));
|
||||
}
|
||||
return LootResolver.pickWeighted(pool, Candidate::weight, rng);
|
||||
}
|
||||
|
||||
static ResourceKey<LootTable> resolveNativeKey(String name, Predicate<ResourceKey<LootTable>> registryContains) {
|
||||
@@ -205,23 +188,6 @@ public final class ModdedLootApplier {
|
||||
container.setChanged();
|
||||
}
|
||||
|
||||
private static void injectReferenceSources(List<LootSource> sources, IrisLootReference reference, Engine engine, boolean fallback) {
|
||||
IrisLootMode mode = reference.getMode();
|
||||
if (mode == IrisLootMode.FALLBACK && !fallback) {
|
||||
return;
|
||||
}
|
||||
injectSources(sources, irisSources(reference, engine), mode, fallback);
|
||||
}
|
||||
|
||||
private static List<LootSource> irisSources(IrisLootReference reference, Engine engine) {
|
||||
KList<IrisLootTable> tables = reference.getLootTables(engine.getComplex());
|
||||
List<LootSource> sources = new ArrayList<>(tables.size());
|
||||
for (IrisLootTable table : tables) {
|
||||
sources.add(new IrisLootSource(table));
|
||||
}
|
||||
return sources;
|
||||
}
|
||||
|
||||
private static boolean hasNativeTable(ServerLevel level, ResourceKey<LootTable> key) {
|
||||
return level.getServer().reloadableRegistries().lookup()
|
||||
.lookup(Registries.LOOT_TABLE)
|
||||
@@ -244,25 +210,43 @@ public final class ModdedLootApplier {
|
||||
}
|
||||
}
|
||||
|
||||
private static Candidate pickWeighted(List<Candidate> pool, RNG rng) {
|
||||
if (pool.isEmpty()) {
|
||||
return null;
|
||||
static boolean isCanonicalContainer(BlockPos pos, BlockState state) {
|
||||
if (!(state.getBlock() instanceof ChestBlock) || state.getValue(ChestBlock.TYPE) == ChestType.SINGLE) {
|
||||
return true;
|
||||
}
|
||||
int total = 0;
|
||||
for (Candidate candidate : pool) {
|
||||
total += Math.max(0, candidate.weight());
|
||||
BlockPos connected = ChestBlock.getConnectedBlockPos(pos, state);
|
||||
if (connected.equals(pos)) {
|
||||
return true;
|
||||
}
|
||||
if (total <= 0) {
|
||||
return pool.get(rng.nextInt(pool.size()));
|
||||
return isCanonicalPair(pos, connected);
|
||||
}
|
||||
|
||||
static boolean isCanonicalPair(BlockPos pos, BlockPos connected) {
|
||||
return pos.getX() < connected.getX()
|
||||
|| pos.getX() == connected.getX() && pos.getZ() <= connected.getZ();
|
||||
}
|
||||
|
||||
static boolean hasNativeLootTable(ServerLevel level, BlockPos pos, BlockState state) {
|
||||
if (hasNativeLootTableAt(level, pos)) {
|
||||
return true;
|
||||
}
|
||||
int pull = rng.nextInt(total);
|
||||
for (Candidate candidate : pool) {
|
||||
pull -= Math.max(0, candidate.weight());
|
||||
if (pull < 0) {
|
||||
return candidate;
|
||||
}
|
||||
if (!(state.getBlock() instanceof ChestBlock) || state.getValue(ChestBlock.TYPE) == ChestType.SINGLE) {
|
||||
return false;
|
||||
}
|
||||
return pool.get(pool.size() - 1);
|
||||
BlockPos connected = ChestBlock.getConnectedBlockPos(pos, state);
|
||||
return level.hasChunkAt(connected) && hasNativeLootTableAt(level, connected);
|
||||
}
|
||||
|
||||
private static boolean hasNativeLootTableAt(ServerLevel level, BlockPos pos) {
|
||||
return hasNativeLootTable(level.getBlockEntity(pos));
|
||||
}
|
||||
|
||||
static boolean hasNativeLootTable(BlockEntity blockEntity) {
|
||||
return blockEntity instanceof RandomizableContainer container && hasNativeLootTable(container);
|
||||
}
|
||||
|
||||
static boolean hasNativeLootTable(RandomizableContainer container) {
|
||||
return container != null && container.getLootTable() != null;
|
||||
}
|
||||
|
||||
private static Container resolveContainer(ServerLevel level, BlockPos pos, BlockState state) {
|
||||
|
||||
@@ -33,6 +33,8 @@ public interface ModdedServerAccess {
|
||||
|
||||
ServerLevel putLevel(MinecraftServer server, ResourceKey<Level> key, ServerLevel level);
|
||||
|
||||
ServerLevel putLevelIfAbsent(MinecraftServer server, ResourceKey<Level> key, ServerLevel level);
|
||||
|
||||
ServerLevel removeLevel(MinecraftServer server, ResourceKey<Level> key);
|
||||
|
||||
boolean hasLevel(MinecraftServer server, ResourceKey<Level> key);
|
||||
|
||||
@@ -42,6 +42,11 @@ public final class ModdedServerLevels implements ModdedServerAccess {
|
||||
return server.levels.put(key, level);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerLevel putLevelIfAbsent(MinecraftServer server, ResourceKey<Level> key, ServerLevel level) {
|
||||
return server.levels.putIfAbsent(key, level);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerLevel removeLevel(MinecraftServer server, ResourceKey<Level> key) {
|
||||
return server.levels.remove(key);
|
||||
|
||||
+49
-8
@@ -40,7 +40,13 @@ public final class ModdedServiceManager {
|
||||
}
|
||||
services.put(type, service);
|
||||
if (enabled) {
|
||||
enableService(service);
|
||||
try {
|
||||
service.onEnable();
|
||||
} catch (Throwable failure) {
|
||||
services.remove(type);
|
||||
disableAfterFailure(service, failure);
|
||||
throw serviceFailure(service, failure);
|
||||
}
|
||||
}
|
||||
return service;
|
||||
}
|
||||
@@ -55,8 +61,18 @@ public final class ModdedServiceManager {
|
||||
return;
|
||||
}
|
||||
enabled = true;
|
||||
for (ModdedService service : services.values()) {
|
||||
enableService(service);
|
||||
ModdedService[] ordered = services.values().toArray(new ModdedService[0]);
|
||||
for (int i = 0; i < ordered.length; i++) {
|
||||
ModdedService service = ordered[i];
|
||||
try {
|
||||
service.onEnable();
|
||||
} catch (Throwable failure) {
|
||||
enabled = false;
|
||||
for (int rollbackIndex = i; rollbackIndex >= 0; rollbackIndex--) {
|
||||
disableAfterFailure(ordered[rollbackIndex], failure);
|
||||
}
|
||||
throw serviceFailure(service, failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,12 +95,15 @@ public final class ModdedServiceManager {
|
||||
forEachReversed(this::disableService);
|
||||
}
|
||||
|
||||
private void enableService(ModdedService service) {
|
||||
try {
|
||||
service.onEnable();
|
||||
} catch (Throwable error) {
|
||||
LOGGER.error("Iris service onEnable failed for {}", service.getClass().getName(), error);
|
||||
synchronized void rollback(Throwable failure) {
|
||||
if (enabled) {
|
||||
enabled = false;
|
||||
ModdedService[] ordered = services.values().toArray(new ModdedService[0]);
|
||||
for (int i = ordered.length - 1; i >= 0; i--) {
|
||||
disableAfterFailure(ordered[i], failure);
|
||||
}
|
||||
}
|
||||
services.clear();
|
||||
}
|
||||
|
||||
private void disableService(ModdedService service) {
|
||||
@@ -103,6 +122,28 @@ public final class ModdedServiceManager {
|
||||
}
|
||||
}
|
||||
|
||||
private void disableAfterFailure(ModdedService service, Throwable failure) {
|
||||
try {
|
||||
service.onDisable();
|
||||
} catch (Throwable cleanupError) {
|
||||
if (cleanupError != failure) {
|
||||
failure.addSuppressed(cleanupError);
|
||||
}
|
||||
LOGGER.error("Iris service rollback failed for {}", service.getClass().getName(), cleanupError);
|
||||
}
|
||||
}
|
||||
|
||||
private RuntimeException serviceFailure(ModdedService service, Throwable failure) {
|
||||
LOGGER.error("Iris service onEnable failed for {}", service.getClass().getName(), failure);
|
||||
if (failure instanceof RuntimeException runtimeException) {
|
||||
return runtimeException;
|
||||
}
|
||||
if (failure instanceof Error fatalError) {
|
||||
throw fatalError;
|
||||
}
|
||||
return new IllegalStateException("Iris service failed to enable: " + service.getClass().getName(), failure);
|
||||
}
|
||||
|
||||
private void forEachReversed(Consumer<ModdedService> action) {
|
||||
ModdedService[] ordered = services.values().toArray(new ModdedService[0]);
|
||||
for (int i = ordered.length - 1; i >= 0; i--) {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.core.pack.BrokenPackException;
|
||||
import art.arcane.iris.core.pack.PackDownloader;
|
||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.core.pack.PackValidationResult;
|
||||
@@ -34,6 +35,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public final class ModdedStartup {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final AtomicBoolean PREPARED = new AtomicBoolean(false);
|
||||
private static final AtomicBoolean STARTED = new AtomicBoolean(false);
|
||||
private static final Object PACK_LOCK = new Object();
|
||||
|
||||
@@ -41,9 +43,17 @@ public final class ModdedStartup {
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
PREPARED.set(false);
|
||||
STARTED.set(false);
|
||||
}
|
||||
|
||||
public static void prepareForStartup() {
|
||||
if (!PREPARED.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
validateAllPacks();
|
||||
}
|
||||
|
||||
public static void prefetchDefaultPack() {
|
||||
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
|
||||
if (scheduler != null) {
|
||||
@@ -56,30 +66,30 @@ public final class ModdedStartup {
|
||||
}
|
||||
|
||||
public static void runOnce(MinecraftServer server) {
|
||||
if (server == null || !STARTED.compareAndSet(false, true)) {
|
||||
if (server == null) {
|
||||
return;
|
||||
}
|
||||
prepareForStartup();
|
||||
if (!STARTED.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
reinjectPersistentDimensions(server);
|
||||
|
||||
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
|
||||
if (scheduler == null) {
|
||||
validateAllPacks();
|
||||
ensureDefaultPack();
|
||||
return;
|
||||
}
|
||||
scheduler.async(() -> {
|
||||
validateAllPacks();
|
||||
ensureDefaultPack();
|
||||
});
|
||||
scheduler.async(ModdedStartup::ensureDefaultPack);
|
||||
}
|
||||
|
||||
public static void validateAllPacks() {
|
||||
File packsRoot = ModdedPackCommands.packsRoot();
|
||||
File[] packDirs = packsRoot.listFiles(File::isDirectory);
|
||||
PackValidationRegistry.clear();
|
||||
if (packDirs == null || packDirs.length == 0) {
|
||||
return;
|
||||
}
|
||||
PackValidationRegistry.clear();
|
||||
for (File packDir : packDirs) {
|
||||
try {
|
||||
PackValidationResult result = PackValidator.validate(packDir);
|
||||
@@ -99,10 +109,50 @@ public final class ModdedStartup {
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris pack validation failed for '{}'", packDir.getName(), e);
|
||||
String detail = e.getMessage();
|
||||
if (detail == null || detail.isBlank()) {
|
||||
detail = e.getClass().getSimpleName();
|
||||
}
|
||||
PackValidationRegistry.publish(new PackValidationResult(
|
||||
packDir.getName(),
|
||||
List.of("Pack validation failed with " + e.getClass().getSimpleName() + ": " + detail),
|
||||
List.of(),
|
||||
System.currentTimeMillis()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static PackValidationResult requirePackForWorldCreation(String pack) {
|
||||
if (pack == null || pack.isBlank()) {
|
||||
throw new IllegalArgumentException("Pack name is required for world creation");
|
||||
}
|
||||
File packDir = new File(ModdedPackCommands.packsRoot(), pack);
|
||||
if (!packDir.isDirectory()) {
|
||||
throw new BrokenPackException(pack, List.of(
|
||||
"Pack folder does not exist under " + ModdedPackCommands.packsRoot().getAbsolutePath() + "."));
|
||||
}
|
||||
try {
|
||||
PackValidationResult result = PackValidator.validate(packDir);
|
||||
PackValidationRegistry.publish(result);
|
||||
return PackValidationRegistry.requireLoadable(pack);
|
||||
} catch (BrokenPackException e) {
|
||||
throw e;
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris required world-creation validation failed for '{}'", pack, e);
|
||||
String detail = e.getMessage();
|
||||
if (detail == null || detail.isBlank()) {
|
||||
detail = e.getClass().getSimpleName();
|
||||
}
|
||||
PackValidationResult failure = new PackValidationResult(
|
||||
pack,
|
||||
List.of("Pack validation failed with " + e.getClass().getSimpleName() + ": " + detail),
|
||||
List.of(),
|
||||
System.currentTimeMillis());
|
||||
PackValidationRegistry.publish(failure);
|
||||
throw new BrokenPackException(pack, failure.getBlockingErrors());
|
||||
}
|
||||
}
|
||||
|
||||
private static void reinjectPersistentDimensions(MinecraftServer server) {
|
||||
List<ModdedDimensionRegistryStore.PersistentDimension> dimensions = ModdedDimensionRegistryStore.load(server);
|
||||
if (dimensions.isEmpty()) {
|
||||
@@ -115,6 +165,11 @@ public final class ModdedStartup {
|
||||
injected++;
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris failed to re-inject persistent dimension '{}' (pack={} dim={} seed={})", dimension.id(), dimension.pack(), dimension.dimension(), dimension.seed(), e);
|
||||
if (e instanceof Error fatalError) {
|
||||
throw fatalError;
|
||||
}
|
||||
throw new IllegalStateException("Iris failed to re-inject persistent dimension '"
|
||||
+ dimension.id() + "' from pack '" + dimension.pack() + "'", e);
|
||||
}
|
||||
}
|
||||
LOGGER.info("Iris re-injected {} persistent dimension(s) at startup", injected);
|
||||
|
||||
+55
-35
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.nativegen.NativeStructureGenerationException;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||
import art.arcane.iris.spi.PlatformWorld;
|
||||
@@ -51,6 +52,7 @@ import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
@@ -59,7 +61,7 @@ public final class ModdedStructureHooks implements PlatformStructureHooks {
|
||||
private final Supplier<MinecraftServer> server;
|
||||
|
||||
public ModdedStructureHooks(Supplier<MinecraftServer> server) {
|
||||
this.server = server;
|
||||
this.server = Objects.requireNonNull(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -75,19 +77,16 @@ public final class ModdedStructureHooks implements PlatformStructureHooks {
|
||||
@Override
|
||||
public List<String> structureBiomeKeys(String structureKey) {
|
||||
List<String> keys = new ArrayList<>();
|
||||
MinecraftServer instance = requireServer("resolve biome keys for registered structure '" + structureKey + "'");
|
||||
try {
|
||||
MinecraftServer instance = server.get();
|
||||
if (instance == null) {
|
||||
return keys;
|
||||
}
|
||||
Identifier identifier = Identifier.tryParse(structureKey);
|
||||
if (identifier == null) {
|
||||
return keys;
|
||||
throw new IllegalArgumentException("Registered structure key is invalid: " + structureKey);
|
||||
}
|
||||
Registry<Structure> registry = instance.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
Structure structure = registry.getValue(identifier);
|
||||
if (structure == null) {
|
||||
return keys;
|
||||
throw new IllegalArgumentException("Registered structure does not exist: " + structureKey);
|
||||
}
|
||||
for (Holder<Biome> holder : structure.biomes()) {
|
||||
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
|
||||
@@ -95,8 +94,13 @@ public final class ModdedStructureHooks implements PlatformStructureHooks {
|
||||
keys.add(key.get().identifier().toString());
|
||||
}
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError(error);
|
||||
if (keys.isEmpty()) {
|
||||
throw new IllegalStateException("Registered structure '" + structureKey
|
||||
+ "' exposes no registered biome keys");
|
||||
}
|
||||
} catch (RuntimeException error) {
|
||||
throw new IllegalStateException("Iris failed to resolve biome keys for registered structure '"
|
||||
+ structureKey + "' from the modded structure registry", error);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
@@ -129,24 +133,19 @@ public final class ModdedStructureHooks implements PlatformStructureHooks {
|
||||
@Override
|
||||
public List<String> reachableStructureKeys(PlatformWorld world) {
|
||||
List<String> keys = new ArrayList<>();
|
||||
ServerLevel level = requireLevel(world, "resolve reachable structures");
|
||||
try {
|
||||
ServerLevel level = level(world);
|
||||
if (level == null) {
|
||||
return keys;
|
||||
}
|
||||
BiomeSource source = level.getChunkSource().getGenerator().getBiomeSource();
|
||||
Set<String> possibleBiomes = possibleBiomeKeys(source);
|
||||
if (possibleBiomes.isEmpty()) {
|
||||
return keys;
|
||||
}
|
||||
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
for (Map.Entry<ResourceKey<Structure>, Structure> entry : registry.entrySet()) {
|
||||
if (hasPossibleBiome(entry.getValue(), possibleBiomes)) {
|
||||
keys.add(entry.getKey().identifier().toString());
|
||||
}
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError(error);
|
||||
} catch (RuntimeException error) {
|
||||
throw new IllegalStateException("Iris failed to resolve reachable structures for modded level '"
|
||||
+ level.dimension().identifier() + "'", error);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
@@ -154,14 +153,12 @@ public final class ModdedStructureHooks implements PlatformStructureHooks {
|
||||
@Override
|
||||
public List<String> possibleBiomeKeys(PlatformWorld world) {
|
||||
List<String> keys = new ArrayList<>();
|
||||
ServerLevel level = requireLevel(world, "resolve possible structure biome keys");
|
||||
try {
|
||||
ServerLevel level = level(world);
|
||||
if (level == null) {
|
||||
return keys;
|
||||
}
|
||||
keys.addAll(possibleBiomeKeys(level.getChunkSource().getGenerator().getBiomeSource()));
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError(error);
|
||||
} catch (RuntimeException error) {
|
||||
throw new IllegalStateException("Iris failed to resolve possible structure biome keys for modded level '"
|
||||
+ level.dimension().identifier() + "'", error);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
@@ -230,9 +227,9 @@ public final class ModdedStructureHooks implements PlatformStructureHooks {
|
||||
}
|
||||
placeChunks(level, structureManager, generator, start, box, seed);
|
||||
return bounds(box);
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError(error);
|
||||
return null;
|
||||
} catch (RuntimeException error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"capture placement", structureKey, chunkX, chunkZ, error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,16 +261,19 @@ public final class ModdedStructureHooks implements PlatformStructureHooks {
|
||||
}
|
||||
|
||||
private static Set<String> possibleBiomeKeys(BiomeSource source) {
|
||||
Set<String> keys = new LinkedHashSet<>();
|
||||
if (source == null) {
|
||||
return keys;
|
||||
throw new IllegalStateException("Minecraft chunk generator has no biome source");
|
||||
}
|
||||
Set<String> keys = new LinkedHashSet<>();
|
||||
for (Holder<Biome> holder : source.possibleBiomes()) {
|
||||
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
|
||||
if (key.isPresent()) {
|
||||
keys.add(key.get().identifier().toString());
|
||||
}
|
||||
}
|
||||
if (keys.isEmpty()) {
|
||||
throw new IllegalStateException("Minecraft biome source exposes no registered possible biomes");
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
@@ -312,18 +312,38 @@ public final class ModdedStructureHooks implements PlatformStructureHooks {
|
||||
|
||||
private <T> List<String> registryKeys(ResourceKey<Registry<T>> registryKey) {
|
||||
List<String> keys = new ArrayList<>();
|
||||
MinecraftServer instance = requireServer("read registry '" + registryKey.identifier() + "'");
|
||||
try {
|
||||
MinecraftServer instance = server.get();
|
||||
if (instance == null) {
|
||||
return keys;
|
||||
}
|
||||
Registry<T> registry = instance.registryAccess().lookupOrThrow(registryKey);
|
||||
for (Identifier identifier : registry.keySet()) {
|
||||
keys.add(identifier.toString());
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError(error);
|
||||
} catch (RuntimeException error) {
|
||||
throw new IllegalStateException("Iris failed to read modded registry '"
|
||||
+ registryKey.identifier() + "'", error);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
private MinecraftServer requireServer(String operation) {
|
||||
MinecraftServer instance;
|
||||
try {
|
||||
instance = server.get();
|
||||
} catch (RuntimeException error) {
|
||||
throw new IllegalStateException("Iris failed to access the Minecraft server to " + operation, error);
|
||||
}
|
||||
if (instance == null) {
|
||||
throw new IllegalStateException("Iris cannot " + operation + " before the Minecraft server is available");
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
private static ServerLevel requireLevel(PlatformWorld world, String operation) {
|
||||
ServerLevel level = level(world);
|
||||
if (level == null) {
|
||||
throw new IllegalStateException("Iris cannot " + operation
|
||||
+ " without a bound modded ServerLevel");
|
||||
}
|
||||
return level;
|
||||
}
|
||||
}
|
||||
+217
-118
@@ -19,6 +19,7 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.engine.framework.StructureVerticalBounds;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||
@@ -27,7 +28,6 @@ import art.arcane.volmlib.util.json.JSONObject;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
@@ -43,14 +43,17 @@ import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement;
|
||||
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
|
||||
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -74,6 +77,7 @@ public final class ModdedWorldCheck {
|
||||
private static final int EXIT_FAILURE = 1;
|
||||
private static final int MAX_FOOTPRINT_CHUNKS = 96;
|
||||
private static final int MAX_START_REFERENCE_CHUNKS = 16;
|
||||
private static final int MAX_STRUCTURE_CANDIDATES = 1024;
|
||||
private static final long SERVER_WAIT_TIMEOUT_MILLIS = 600000L;
|
||||
private static final long SERVER_WAIT_INTERVAL_MILLIS = 250L;
|
||||
private static final List<StructureCheck> STRUCTURE_CHECKS = List.of(
|
||||
@@ -136,8 +140,9 @@ public final class ModdedWorldCheck {
|
||||
}
|
||||
|
||||
MinecraftServer serverRef = server;
|
||||
WorldCheckPreparation preparation = serverRef.submit(() -> run(serverRef)).join();
|
||||
exitCode = serverRef.submit(() -> runAndRequestStop(
|
||||
() -> run(serverRef),
|
||||
() -> completeWorldCheck(preparation),
|
||||
() -> {
|
||||
stopRequested.set(true);
|
||||
serverRef.halt(false);
|
||||
@@ -195,11 +200,12 @@ public final class ModdedWorldCheck {
|
||||
processExit.exit(exitCode);
|
||||
}
|
||||
|
||||
private static boolean run(MinecraftServer server) {
|
||||
private static WorldCheckPreparation run(MinecraftServer server) {
|
||||
ServerLevel level = targetLevel(server);
|
||||
if (level == null) {
|
||||
LOGGER.error("[worldcheck] no Iris dimension is loaded");
|
||||
return false;
|
||||
return new WorldCheckPreparation(false, false, false, false,
|
||||
new NativeStructureGate(false, 0, false, null));
|
||||
}
|
||||
|
||||
String levelId = level.dimension().identifier().toString();
|
||||
@@ -268,16 +274,43 @@ public final class ModdedWorldCheck {
|
||||
}
|
||||
|
||||
boolean entityMixinsOk = checkEntityMixins(level);
|
||||
boolean structureOk = false;
|
||||
if (generator != null) {
|
||||
structureOk = checkNativeStructures(level, generator, spawn);
|
||||
}
|
||||
NativeStructureGate structureGate = generator == null
|
||||
? new NativeStructureGate(false, 0, false, null)
|
||||
: checkNativeStructures(level, generator, spawn);
|
||||
boolean terrainOk = sectionsOk && varietyOk;
|
||||
boolean nonStructurePass = irisGenerator && dimensionTypeOk && terrainOk && entityMixinsOk;
|
||||
return new WorldCheckPreparation(nonStructurePass, terrainOk, dimensionTypeOk,
|
||||
entityMixinsOk, structureGate);
|
||||
}
|
||||
|
||||
boolean pass = irisGenerator && dimensionTypeOk && sectionsOk && varietyOk && entityMixinsOk && structureOk;
|
||||
private static boolean completeWorldCheck(WorldCheckPreparation preparation) {
|
||||
NativeStructureGate structureGate = preparation.structureGate();
|
||||
boolean poiOk = false;
|
||||
PendingVillagePoi pendingPoi = structureGate.pendingPoi();
|
||||
if (pendingPoi != null) {
|
||||
PoiAudit poi = auditStructurePois(pendingPoi.level(), pendingPoi.start());
|
||||
poiOk = villagePoiPass(poi.inBounds(), poi.outOfBounds());
|
||||
qaEvent("village_poi_metric", "village", poiOk,
|
||||
"inBounds=" + poi.inBounds() + ",outOfBounds=" + poi.outOfBounds());
|
||||
if (!poiOk) {
|
||||
LOGGER.error("[worldcheck] village POI audit failed: inBounds={} outOfBounds={}",
|
||||
poi.inBounds(), poi.outOfBounds());
|
||||
}
|
||||
} else {
|
||||
qaEvent("village_poi_metric", "village", false, "skipped=structure");
|
||||
}
|
||||
int passed = structureGate.nonVillagePassed()
|
||||
+ (structureGate.villagePassBeforePoi() && poiOk ? 1 : 0);
|
||||
boolean structurePass = structureGate.passBeforePoi() && poiOk;
|
||||
LOGGER.info("[worldcheck] native structure gate: {}/{} passed", passed, STRUCTURE_CHECKS.size());
|
||||
qaEvent("structure_aggregate", "all", structurePass,
|
||||
"passed=" + passed + ",total=" + STRUCTURE_CHECKS.size());
|
||||
boolean pass = preparation.nonStructurePass() && structurePass;
|
||||
LOGGER.info("[worldcheck] {}", pass ? "PASS" : "FAIL");
|
||||
qaEvent("worldcheck_final", "all", pass,
|
||||
"structures=" + STRUCTURE_CHECKS.size() + ",terrain=" + (sectionsOk && varietyOk)
|
||||
+ ",dimensionType=" + dimensionTypeOk + ",entityMixins=" + entityMixinsOk);
|
||||
"structures=" + STRUCTURE_CHECKS.size() + ",terrain=" + preparation.terrainOk()
|
||||
+ ",dimensionType=" + preparation.dimensionTypeOk()
|
||||
+ ",entityMixins=" + preparation.entityMixinsOk());
|
||||
return pass;
|
||||
}
|
||||
|
||||
@@ -354,25 +387,32 @@ public final class ModdedWorldCheck {
|
||||
return pass;
|
||||
}
|
||||
|
||||
private static boolean checkNativeStructures(ServerLevel level, IrisModdedChunkGenerator generator, BlockPos origin) {
|
||||
private static NativeStructureGate checkNativeStructures(ServerLevel level,
|
||||
IrisModdedChunkGenerator generator,
|
||||
BlockPos origin) {
|
||||
boolean pass = true;
|
||||
int passed = 0;
|
||||
int nonVillagePassed = 0;
|
||||
boolean villagePass = false;
|
||||
PendingVillagePoi pendingPoi = null;
|
||||
for (StructureCheck check : STRUCTURE_CHECKS) {
|
||||
boolean structurePass = checkNativeStructure(level, generator, origin, check);
|
||||
if (structurePass) {
|
||||
passed++;
|
||||
} else {
|
||||
StructureCheckResult result = checkNativeStructure(level, generator, origin, check);
|
||||
if (!result.pass()) {
|
||||
pass = false;
|
||||
}
|
||||
if (check.label().equals("village")) {
|
||||
villagePass = result.pass();
|
||||
pendingPoi = result.pendingPoi();
|
||||
} else if (result.pass()) {
|
||||
nonVillagePassed++;
|
||||
}
|
||||
}
|
||||
LOGGER.info("[worldcheck] native structure gate: {}/{} passed", passed, STRUCTURE_CHECKS.size());
|
||||
qaEvent("structure_aggregate", "all", pass,
|
||||
"passed=" + passed + ",total=" + STRUCTURE_CHECKS.size());
|
||||
return pass;
|
||||
return new NativeStructureGate(pass, nonVillagePassed, villagePass, pendingPoi);
|
||||
}
|
||||
|
||||
private static boolean checkNativeStructure(ServerLevel level, IrisModdedChunkGenerator generator,
|
||||
BlockPos origin, StructureCheck check) {
|
||||
private static StructureCheckResult checkNativeStructure(ServerLevel level,
|
||||
IrisModdedChunkGenerator generator,
|
||||
BlockPos origin,
|
||||
StructureCheck check) {
|
||||
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
List<Holder<Structure>> registered = new ArrayList<>(check.registryKeys().size());
|
||||
LinkedHashSet<String> registeredKeys = new LinkedHashSet<>();
|
||||
@@ -398,7 +438,7 @@ public final class ModdedWorldCheck {
|
||||
emitSkipped(check, "registry", "structure_reachability", "structure_locate",
|
||||
"structure_start_reference", "structure_footprint", "structure_material",
|
||||
"structure_block_entity");
|
||||
return false;
|
||||
return new StructureCheckResult(false, null);
|
||||
}
|
||||
|
||||
List<Holder<Structure>> reachable = new ArrayList<>(registered.size());
|
||||
@@ -422,35 +462,35 @@ public final class ModdedWorldCheck {
|
||||
LOGGER.error("[worldcheck] {} cannot generate in any biome produced by this Iris pack", check.label());
|
||||
emitSkipped(check, "reachability", "structure_locate", "structure_start_reference",
|
||||
"structure_footprint", "structure_material", "structure_block_entity");
|
||||
return false;
|
||||
return new StructureCheckResult(false, null);
|
||||
}
|
||||
|
||||
long locateStart = System.nanoTime();
|
||||
Pair<BlockPos, Holder<Structure>> found = generator.findNearestMapStructure(
|
||||
level, HolderSet.direct(reachable), origin, check.locateRadius(), false);
|
||||
Pair<BlockPos, Holder<Structure>> found = findGeneratedStructureCandidate(
|
||||
level, reachable, origin, check.locateRadius());
|
||||
long locateMillis = (System.nanoTime() - locateStart) / 1_000_000L;
|
||||
Identifier foundKey = found == null ? null : registry.getKey(found.getSecond().value());
|
||||
boolean locateOk = found != null && foundKey != null && reachableKeys.contains(foundKey.toString());
|
||||
qaEvent("structure_locate", check.label(), locateOk,
|
||||
"millis=" + locateMillis + ",radius=" + check.locateRadius()
|
||||
"method=placement_candidates,millis=" + locateMillis + ",radius=" + check.locateRadius()
|
||||
+ ",result=" + (foundKey == null ? "none" : foundKey));
|
||||
if (found == null) {
|
||||
LOGGER.error("[worldcheck] {} locate found no result within {} chunks after {}ms",
|
||||
LOGGER.error("[worldcheck] {} native placement candidates produced no valid start within {} rings after {}ms",
|
||||
check.label(), check.locateRadius(), locateMillis);
|
||||
emitSkipped(check, "locate", "structure_start_reference", "structure_footprint",
|
||||
"structure_material", "structure_block_entity");
|
||||
return false;
|
||||
return new StructureCheckResult(false, null);
|
||||
}
|
||||
|
||||
BlockPos position = found.getFirst();
|
||||
LOGGER.info("[worldcheck] {} locate: {} {} {} in {}ms (radius={}, unexplored=false, result={})",
|
||||
LOGGER.info("[worldcheck] {} generated candidate: {} {} {} in {}ms (radius={}, result={})",
|
||||
check.label(), position.getX(), position.getY(), position.getZ(), locateMillis,
|
||||
check.locateRadius(), foundKey);
|
||||
if (!locateOk) {
|
||||
LOGGER.error("[worldcheck] {} locate returned unexpected structure {}", check.label(), foundKey);
|
||||
LOGGER.error("[worldcheck] {} candidate scan returned unexpected structure {}", check.label(), foundKey);
|
||||
emitSkipped(check, "locate", "structure_start_reference", "structure_footprint",
|
||||
"structure_material", "structure_block_entity");
|
||||
return false;
|
||||
return new StructureCheckResult(false, null);
|
||||
}
|
||||
|
||||
int chunkX = position.getX() >> 4;
|
||||
@@ -471,11 +511,12 @@ public final class ModdedWorldCheck {
|
||||
check.label(), chunkX, chunkZ);
|
||||
emitSkipped(check, "start_reference", "structure_footprint", "structure_material",
|
||||
"structure_block_entity");
|
||||
return false;
|
||||
return new StructureCheckResult(false, null);
|
||||
}
|
||||
|
||||
IrisNativeStructureDecision decision = generator.commandEngine().getDimension().getImportedStructures()
|
||||
.resolve(foundKey.toString(), NativeStructurePostProcessor.isUndergroundStep(structure.step()));
|
||||
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(
|
||||
generator.commandEngine(), foundKey.toString(),
|
||||
NativeStructurePostProcessor.isUndergroundStep(structure.step()));
|
||||
Integer appliedShift = generator.worldCheckStructureShift(foundKey.toString(), start.getChunkPos());
|
||||
BoundingBox shiftedBounds = start.getBoundingBox();
|
||||
boolean verticalShiftOk = verticalShiftMatches(
|
||||
@@ -522,22 +563,17 @@ public final class ModdedWorldCheck {
|
||||
}
|
||||
|
||||
boolean foundationOk = true;
|
||||
boolean poiOk = true;
|
||||
PendingVillagePoi pendingPoi = null;
|
||||
if (check.label().equals("village")) {
|
||||
foundationOk = villageFoundationPass(footprint.foundationGapColumns());
|
||||
LOGGER.info("[worldcheck] village foundation metric: cobblestone below piece bases={} columns={} unsupported={}",
|
||||
footprint.foundationBlocks(), footprint.foundationColumns(), footprint.foundationGapColumns());
|
||||
LOGGER.info("[worldcheck] village foundation metric: bases={} cobblestone={} columns={} unsupported={}",
|
||||
footprint.foundationBaseColumns(), footprint.foundationBlocks(),
|
||||
footprint.foundationColumns(), footprint.foundationGapColumns());
|
||||
qaEvent("village_foundation_metric", check.label(), foundationOk,
|
||||
"cobblestoneBelowBase=" + footprint.foundationBlocks() + ",columns="
|
||||
"bases=" + footprint.foundationBaseColumns() + ",cobblestoneBelowBase="
|
||||
+ footprint.foundationBlocks() + ",columns="
|
||||
+ footprint.foundationColumns() + ",unsupported=" + footprint.foundationGapColumns());
|
||||
PoiAudit poi = auditStructurePois(level, start);
|
||||
poiOk = villagePoiPass(poi.inBounds(), poi.outOfBounds());
|
||||
qaEvent("village_poi_metric", check.label(), poiOk,
|
||||
"inBounds=" + poi.inBounds() + ",outOfBounds=" + poi.outOfBounds());
|
||||
if (!poiOk) {
|
||||
LOGGER.error("[worldcheck] village POI audit failed: inBounds={} outOfBounds={}",
|
||||
poi.inBounds(), poi.outOfBounds());
|
||||
}
|
||||
pendingPoi = new PendingVillagePoi(level, start);
|
||||
}
|
||||
|
||||
boolean blockEntityOk = footprint.blockEntityStates() == footprint.blockEntitiesPresent();
|
||||
@@ -562,8 +598,9 @@ public final class ModdedWorldCheck {
|
||||
if (!foundationOk) {
|
||||
LOGGER.error("[worldcheck] village has unsupported foundation columns after stilt placement");
|
||||
}
|
||||
return verticalShiftOk && footprintOk && materialOk && blockEntityOk
|
||||
&& vegetationOk && foundationOk && poiOk;
|
||||
boolean pass = verticalShiftOk && footprintOk && materialOk && blockEntityOk
|
||||
&& vegetationOk && foundationOk;
|
||||
return new StructureCheckResult(pass, pass ? pendingPoi : null);
|
||||
}
|
||||
|
||||
private static StructureStart resolveStructureStart(ServerLevel level, ChunkAccess targetChunk,
|
||||
@@ -586,6 +623,96 @@ public final class ModdedWorldCheck {
|
||||
return direct;
|
||||
}
|
||||
|
||||
private static Pair<BlockPos, Holder<Structure>> findGeneratedStructureCandidate(
|
||||
ServerLevel level, List<Holder<Structure>> structures, BlockPos origin, int maxRadius) {
|
||||
ChunkGeneratorStructureState state = level.getChunkSource().getGeneratorState();
|
||||
Set<Long> attempted = new LinkedHashSet<>();
|
||||
for (Holder<Structure> structure : structures) {
|
||||
for (StructurePlacement placement : state.getPlacementsForStructure(structure)) {
|
||||
if (!(placement instanceof ConcentricRingsStructurePlacement rings)) {
|
||||
continue;
|
||||
}
|
||||
List<ChunkPos> positions = state.getRingPositionsFor(rings);
|
||||
if (positions == null) {
|
||||
continue;
|
||||
}
|
||||
List<ChunkPos> sorted = new ArrayList<>(positions);
|
||||
sorted.sort(Comparator.comparingLong(position -> distanceSquared(origin, position)));
|
||||
for (ChunkPos position : sorted) {
|
||||
Pair<BlockPos, Holder<Structure>> found = inspectStructureCandidate(
|
||||
level, structures, placement, position, attempted);
|
||||
if (found != null) {
|
||||
return found;
|
||||
}
|
||||
if (attempted.size() >= MAX_STRUCTURE_CANDIDATES) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int originChunkX = origin.getX() >> 4;
|
||||
int originChunkZ = origin.getZ() >> 4;
|
||||
for (int radius = 0; radius <= maxRadius; radius++) {
|
||||
for (Holder<Structure> structure : structures) {
|
||||
for (StructurePlacement placement : state.getPlacementsForStructure(structure)) {
|
||||
if (!(placement instanceof RandomSpreadStructurePlacement randomSpread)) {
|
||||
continue;
|
||||
}
|
||||
for (int x = -radius; x <= radius; x++) {
|
||||
boolean xEdge = x == -radius || x == radius;
|
||||
for (int z = -radius; z <= radius; z++) {
|
||||
if (!xEdge && z != -radius && z != radius) {
|
||||
continue;
|
||||
}
|
||||
int sectorX = originChunkX + randomSpread.spacing() * x;
|
||||
int sectorZ = originChunkZ + randomSpread.spacing() * z;
|
||||
ChunkPos candidate = randomSpread.getPotentialStructureChunk(
|
||||
state.getLevelSeed(), sectorX, sectorZ);
|
||||
if (!placement.isStructureChunk(state, candidate.x(), candidate.z())) {
|
||||
continue;
|
||||
}
|
||||
Pair<BlockPos, Holder<Structure>> found = inspectStructureCandidate(
|
||||
level, structures, placement, candidate, attempted);
|
||||
if (found != null) {
|
||||
return found;
|
||||
}
|
||||
if (attempted.size() >= MAX_STRUCTURE_CANDIDATES) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Pair<BlockPos, Holder<Structure>> inspectStructureCandidate(
|
||||
ServerLevel level, List<Holder<Structure>> structures, StructurePlacement placement,
|
||||
ChunkPos candidate, Set<Long> attempted) {
|
||||
if (!attempted.add(candidate.pack())) {
|
||||
return null;
|
||||
}
|
||||
ChunkAccess chunk = level.getChunk(candidate.x(), candidate.z());
|
||||
for (Holder<Structure> structure : structures) {
|
||||
StructureStart start = resolveStructureStart(level, chunk, structure.value());
|
||||
if (start == null || !start.isValid()) {
|
||||
continue;
|
||||
}
|
||||
BlockPos locate = placement.getLocatePos(start.getChunkPos());
|
||||
BlockPos resolved = new BlockPos(locate.getX(), start.getBoundingBox().minY(), locate.getZ());
|
||||
return Pair.of(resolved, structure);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static long distanceSquared(BlockPos origin, ChunkPos position) {
|
||||
long x = (long) position.getMinBlockX() - origin.getX();
|
||||
long z = (long) position.getMinBlockZ() - origin.getZ();
|
||||
return x * x + z * z;
|
||||
}
|
||||
|
||||
private static FootprintAudit auditFootprint(ServerLevel level, Structure structure, StructureStart start,
|
||||
StructureCheck check, Identifier structureKey) {
|
||||
List<StructurePiece> pieces = start.getPieces();
|
||||
@@ -600,11 +727,11 @@ public final class ModdedWorldCheck {
|
||||
int characteristicChunks = 0;
|
||||
int vegetationBlocks = 0;
|
||||
int vegetationColumns = 0;
|
||||
int foundationBaseColumns = 0;
|
||||
int foundationBlocks = 0;
|
||||
int foundationColumns = 0;
|
||||
int foundationGapColumns = 0;
|
||||
BitSet visited = new BitSet(level.getHeight() << 8);
|
||||
int[] minimumPieceY = new int[256];
|
||||
int[] maximumPieceY = new int[256];
|
||||
for (ChunkPos chunkPos : selected) {
|
||||
ChunkAccess chunk = level.getChunk(chunkPos.x(), chunkPos.z());
|
||||
@@ -617,8 +744,8 @@ public final class ModdedWorldCheck {
|
||||
BlockEntityAudit blockEntities = auditBlockEntities(level, chunk);
|
||||
blockEntityStates += blockEntities.states();
|
||||
blockEntitiesPresent += blockEntities.present();
|
||||
StructureMaterialAudit material = auditStructureMaterial(level, chunk, pieces, check,
|
||||
structureKey, visited, minimumPieceY, maximumPieceY);
|
||||
StructureMaterialAudit material = auditStructureMaterial(level, chunk, start, check,
|
||||
structureKey, visited, maximumPieceY);
|
||||
if (material.scanned()) {
|
||||
materialScannedChunks++;
|
||||
}
|
||||
@@ -628,6 +755,7 @@ public final class ModdedWorldCheck {
|
||||
}
|
||||
vegetationBlocks += material.vegetationBlocks();
|
||||
vegetationColumns += material.vegetationColumns();
|
||||
foundationBaseColumns += material.foundationBaseColumns();
|
||||
foundationBlocks += material.foundationBlocks();
|
||||
foundationColumns += material.foundationColumns();
|
||||
foundationGapColumns += material.foundationGapColumns();
|
||||
@@ -648,19 +776,18 @@ public final class ModdedWorldCheck {
|
||||
return new FootprintAudit(selected.size(), availableChunks, evidenceChunks,
|
||||
coveredPieces, pieces.size(), blockEntityStates, blockEntitiesPresent,
|
||||
materialScannedChunks, characteristicBlocks, characteristicChunks,
|
||||
vegetationBlocks, vegetationColumns, foundationBlocks, foundationColumns,
|
||||
foundationGapColumns);
|
||||
vegetationBlocks, vegetationColumns, foundationBaseColumns, foundationBlocks,
|
||||
foundationColumns, foundationGapColumns);
|
||||
}
|
||||
|
||||
private static StructureMaterialAudit auditStructureMaterial(ServerLevel level, ChunkAccess chunk,
|
||||
List<StructurePiece> pieces,
|
||||
StructureStart start,
|
||||
StructureCheck check,
|
||||
Identifier structureKey,
|
||||
BitSet visited,
|
||||
int[] minimumPieceY,
|
||||
int[] maximumPieceY) {
|
||||
List<StructurePiece> pieces = start.getPieces();
|
||||
visited.clear();
|
||||
Arrays.fill(minimumPieceY, Integer.MAX_VALUE);
|
||||
Arrays.fill(maximumPieceY, Integer.MIN_VALUE);
|
||||
int characteristicBlocks = 0;
|
||||
int minimumWorldY = level.getMinY();
|
||||
@@ -683,20 +810,10 @@ public final class ModdedWorldCheck {
|
||||
continue;
|
||||
}
|
||||
scanned = true;
|
||||
int metricMinimumY = minimumY;
|
||||
boolean metricBaseValid = true;
|
||||
if (check.label().equals("village") && piece instanceof PoolElementStructurePiece poolPiece) {
|
||||
int groundY = bounds.minY() + poolPiece.getGroundLevelDelta();
|
||||
metricBaseValid = groundY >= bounds.minY();
|
||||
metricMinimumY = Math.max(minimumWorldY, Math.min(maximumWorldY, groundY));
|
||||
}
|
||||
for (int z = minimumZ; z <= maximumZ; z++) {
|
||||
int localZ = z - minimumChunkZ;
|
||||
for (int x = minimumX; x <= maximumX; x++) {
|
||||
int column = (localZ << 4) | (x - minimumChunkX);
|
||||
if (metricBaseValid) {
|
||||
minimumPieceY[column] = Math.min(minimumPieceY[column], metricMinimumY);
|
||||
}
|
||||
maximumPieceY[column] = Math.max(maximumPieceY[column], maximumY);
|
||||
}
|
||||
}
|
||||
@@ -747,55 +864,26 @@ public final class ModdedWorldCheck {
|
||||
}
|
||||
}
|
||||
|
||||
int foundationBaseColumns = 0;
|
||||
int foundationBlocks = 0;
|
||||
int foundationColumns = 0;
|
||||
int foundationGapColumns = 0;
|
||||
if (check.label().equals("village")) {
|
||||
for (int column = 0; column < minimumPieceY.length; column++) {
|
||||
int minimumY = minimumPieceY[column];
|
||||
if (minimumY == Integer.MAX_VALUE) {
|
||||
continue;
|
||||
}
|
||||
boolean foundationColumn = false;
|
||||
int x = minimumChunkX + (column & 15);
|
||||
int z = minimumChunkZ + (column >> 4);
|
||||
int foundationY = Integer.MIN_VALUE;
|
||||
int foundationMaximumY = Math.min(maximumWorldY, minimumY + 1);
|
||||
for (int y = minimumY; y <= foundationMaximumY; y++) {
|
||||
if (chunk.getBlockState(position.set(x, y, z)).isSolid()) {
|
||||
foundationY = y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
boolean basePresent = foundationY != Integer.MIN_VALUE;
|
||||
boolean supportSolid = false;
|
||||
boolean vegetationSupport = false;
|
||||
if (basePresent && foundationY > minimumWorldY) {
|
||||
BlockState support = chunk.getBlockState(position.set(x, foundationY - 1, z));
|
||||
supportSolid = support.isSolid();
|
||||
vegetationSupport = support.is(BlockTags.LOGS) || support.is(BlockTags.LEAVES);
|
||||
}
|
||||
boolean worldFloorBase = basePresent && foundationY == minimumWorldY;
|
||||
if (!villageFoundationSupported(basePresent, worldFloorBase, supportSolid, vegetationSupport)) {
|
||||
foundationGapColumns++;
|
||||
continue;
|
||||
}
|
||||
for (int y = foundationY - 1; y >= minimumWorldY; y--) {
|
||||
BlockState state = chunk.getBlockState(position.set(x, y, z));
|
||||
if (!state.is(Blocks.COBBLESTONE)) {
|
||||
break;
|
||||
}
|
||||
foundationBlocks++;
|
||||
foundationColumn = true;
|
||||
}
|
||||
if (foundationColumn) {
|
||||
foundationColumns++;
|
||||
}
|
||||
}
|
||||
BoundingBox area = new BoundingBox(
|
||||
minimumChunkX, minimumWorldY, minimumChunkZ,
|
||||
maximumChunkX, maximumWorldY, maximumChunkZ);
|
||||
NativeStructurePostProcessor.StiltSupportAudit foundation =
|
||||
NativeStructurePostProcessor.auditStiltSupport(
|
||||
level, area, start, Blocks.COBBLESTONE.defaultBlockState());
|
||||
foundationBaseColumns = foundation.baseColumns();
|
||||
foundationBlocks = foundation.stiltBlocks();
|
||||
foundationColumns = foundation.stiltColumns();
|
||||
foundationGapColumns = foundation.unsupportedColumns();
|
||||
}
|
||||
|
||||
return new StructureMaterialAudit(scanned, characteristicBlocks, vegetationBlocks,
|
||||
vegetationColumns, foundationBlocks, foundationColumns, foundationGapColumns);
|
||||
vegetationColumns, foundationBaseColumns, foundationBlocks, foundationColumns,
|
||||
foundationGapColumns);
|
||||
}
|
||||
|
||||
private static List<ChunkPos> selectFootprintChunks(StructureStart start, int limit) {
|
||||
@@ -1036,11 +1124,6 @@ public final class ModdedWorldCheck {
|
||||
return vegetation && blockY > highestPieceY;
|
||||
}
|
||||
|
||||
static boolean villageFoundationSupported(boolean basePresent, boolean worldFloorBase,
|
||||
boolean supportSolid, boolean vegetationSupport) {
|
||||
return basePresent && (worldFloorBase || (supportSolid && !vegetationSupport));
|
||||
}
|
||||
|
||||
static boolean villageFoundationPass(int unsupportedColumns) {
|
||||
return unsupportedColumns == 0;
|
||||
}
|
||||
@@ -1194,12 +1277,27 @@ public final class ModdedWorldCheck {
|
||||
private record StructureCheck(String label, List<String> registryKeys, int locateRadius) {
|
||||
}
|
||||
|
||||
private record WorldCheckPreparation(boolean nonStructurePass, boolean terrainOk,
|
||||
boolean dimensionTypeOk, boolean entityMixinsOk,
|
||||
NativeStructureGate structureGate) {
|
||||
}
|
||||
|
||||
private record NativeStructureGate(boolean passBeforePoi, int nonVillagePassed,
|
||||
boolean villagePassBeforePoi, PendingVillagePoi pendingPoi) {
|
||||
}
|
||||
|
||||
private record StructureCheckResult(boolean pass, PendingVillagePoi pendingPoi) {
|
||||
}
|
||||
|
||||
private record PendingVillagePoi(ServerLevel level, StructureStart start) {
|
||||
}
|
||||
|
||||
private record FootprintAudit(int inspectedChunks, int availableChunks, int evidenceChunks,
|
||||
int coveredPieces, int totalPieces, int blockEntityStates,
|
||||
int blockEntitiesPresent, int materialScannedChunks,
|
||||
int characteristicBlocks, int characteristicChunks,
|
||||
int vegetationBlocks, int vegetationColumns,
|
||||
int foundationBlocks, int foundationColumns,
|
||||
int foundationBaseColumns, int foundationBlocks, int foundationColumns,
|
||||
int foundationGapColumns) {
|
||||
}
|
||||
|
||||
@@ -1208,7 +1306,8 @@ public final class ModdedWorldCheck {
|
||||
|
||||
private record StructureMaterialAudit(boolean scanned, int characteristicBlocks,
|
||||
int vegetationBlocks, int vegetationColumns,
|
||||
int foundationBlocks, int foundationColumns,
|
||||
int foundationBaseColumns, int foundationBlocks,
|
||||
int foundationColumns,
|
||||
int foundationGapColumns) {
|
||||
}
|
||||
|
||||
|
||||
+52
-26
@@ -19,6 +19,7 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.engine.IrisEngine;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineTarget;
|
||||
@@ -35,6 +36,7 @@ import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class ModdedWorldEngines {
|
||||
@@ -57,22 +59,48 @@ public final class ModdedWorldEngines {
|
||||
}
|
||||
|
||||
public static void evict(ServerLevel level) {
|
||||
Engine removed = ENGINES.remove(level);
|
||||
if (removed == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!removed.isClosed()) {
|
||||
removed.close();
|
||||
}
|
||||
LOGGER.info("Iris engine evicted for {}", level.dimension().identifier());
|
||||
evictOrThrow(level);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris engine evict close failed for {}", level.dimension().identifier(), e);
|
||||
}
|
||||
}
|
||||
|
||||
static void evictOrThrow(ServerLevel level) {
|
||||
Engine[] removed = new Engine[1];
|
||||
ENGINES.computeIfPresent(level, (ServerLevel ignored, Engine current) -> {
|
||||
close(current);
|
||||
removed[0] = current;
|
||||
return null;
|
||||
});
|
||||
if (removed[0] == null) {
|
||||
return;
|
||||
}
|
||||
LOGGER.info("Iris engine evicted for {}", level.dimension().identifier());
|
||||
}
|
||||
|
||||
static Engine prepareReplacement(ServerLevel level, String pack, String dimensionKey, long seedOverride) {
|
||||
return create(level, pack, dimensionKey, seedOverride);
|
||||
}
|
||||
|
||||
static void installReplacement(ServerLevel level, Engine replacement) {
|
||||
ServerLevel activeLevel = Objects.requireNonNull(level);
|
||||
Engine activeReplacement = Objects.requireNonNull(replacement);
|
||||
ENGINES.compute(activeLevel, (ServerLevel ignored, Engine current) -> {
|
||||
if (current != null && current != activeReplacement) {
|
||||
close(current);
|
||||
}
|
||||
return activeReplacement;
|
||||
});
|
||||
}
|
||||
|
||||
static void closeUnregistered(Engine engine) {
|
||||
close(engine);
|
||||
}
|
||||
|
||||
private static Engine create(ServerLevel level, String pack, String dimensionKey, long seedOverride) {
|
||||
ModdedEngineBootstrap.bind();
|
||||
PackValidationRegistry.requireLoadable(pack);
|
||||
File packDir = resolvePack(pack, dimensionKey);
|
||||
IrisData data = IrisData.get(packDir);
|
||||
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
|
||||
@@ -95,6 +123,16 @@ public final class ModdedWorldEngines {
|
||||
.platformWorld(new ModdedPlatformWorld(level))
|
||||
.build();
|
||||
Engine engine = new IrisEngine(new EngineTarget(world, dimension, data), false);
|
||||
if (engine.isClosed() || engine.getComplex() == null) {
|
||||
IllegalStateException failure = new IllegalStateException("Iris engine for "
|
||||
+ level.dimension().identifier() + " did not initialize a ready biome complex");
|
||||
try {
|
||||
close(engine);
|
||||
} catch (Throwable cleanupError) {
|
||||
failure.addSuppressed(cleanupError);
|
||||
}
|
||||
throw failure;
|
||||
}
|
||||
|
||||
LOGGER.info("Iris engine up for {}: pack={} dim={} seed={} height={}..{}",
|
||||
level.dimension().identifier(), packDir.getAbsolutePath(), dimension.getLoadKey(), seed, dimension.getMinHeight(), dimension.getMaxHeight());
|
||||
@@ -131,24 +169,6 @@ public final class ModdedWorldEngines {
|
||||
return packDir;
|
||||
}
|
||||
|
||||
String parity = System.getProperty("iris.parity");
|
||||
if (parity != null) {
|
||||
String parityPath = parity;
|
||||
int lastColon = parity.lastIndexOf(':');
|
||||
if (lastColon > 0) {
|
||||
try {
|
||||
Integer.parseInt(parity.substring(lastColon + 1));
|
||||
parityPath = parity.substring(0, lastColon);
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
File parityPack = new File(parityPath);
|
||||
if (parityPack.isDirectory()) {
|
||||
LOGGER.warn("Iris pack missing at {}; falling back to parity pack {}", packDir.getAbsolutePath(), parityPack.getAbsolutePath());
|
||||
return parityPack;
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.error("===============================================================");
|
||||
LOGGER.error("Iris pack '{}' is not installed.", pack);
|
||||
LOGGER.error("Expected a pack folder at: {}", packDir.getAbsolutePath());
|
||||
@@ -157,6 +177,12 @@ public final class ModdedWorldEngines {
|
||||
throw new IllegalStateException("Iris pack not installed: " + packDir.getAbsolutePath());
|
||||
}
|
||||
|
||||
private static void close(Engine engine) {
|
||||
if (engine != null && !engine.isClosed()) {
|
||||
engine.close();
|
||||
}
|
||||
}
|
||||
|
||||
public static void shutdown() {
|
||||
for (Map.Entry<ServerLevel, Engine> entry : ENGINES.entrySet()) {
|
||||
Engine engine = entry.getValue();
|
||||
|
||||
+11
-4
@@ -23,6 +23,7 @@ import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineWorldManager;
|
||||
import art.arcane.iris.engine.framework.LootResolver;
|
||||
import art.arcane.iris.engine.object.IRare;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisEntity;
|
||||
@@ -492,7 +493,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
|
||||
int min = entry.getMinSpawns();
|
||||
int max = entry.getMaxSpawns();
|
||||
int count = min == max ? min : RNG.r.i(Math.min(min, max), Math.max(min, max));
|
||||
int count = LootResolver.inclusive(RNG.r, min, max);
|
||||
if (count <= 0) {
|
||||
return 0;
|
||||
}
|
||||
@@ -516,8 +517,8 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
worldY = caveFloor.getY() + 1;
|
||||
worldZ = caveFloor.getZ();
|
||||
} else {
|
||||
worldX = (chunkX << 4) + RNG.r.i(15);
|
||||
worldZ = (chunkZ << 4) + RNG.r.i(15);
|
||||
worldX = (chunkX << 4) + RNG.r.i(16);
|
||||
worldZ = (chunkZ << 4) + RNG.r.i(16);
|
||||
int surfaceY = level.getMinY() + engine.getHeight(worldX, worldZ, false);
|
||||
int solidY = level.getMinY() + engine.getHeight(worldX, worldZ, true);
|
||||
worldY = group == IrisSpawnGroup.NORMAL
|
||||
@@ -527,6 +528,9 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
if (worldY <= level.getMinY() || worldY >= level.getMaxY()) {
|
||||
continue;
|
||||
}
|
||||
if (!LootResolver.oneIn(entityRng, entry.getRarity())) {
|
||||
continue;
|
||||
}
|
||||
if (!lightAllowed(spawner, level, worldX, worldY, worldZ)) {
|
||||
continue;
|
||||
}
|
||||
@@ -551,7 +555,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
|
||||
int min = entry.getMinSpawns();
|
||||
int max = entry.getMaxSpawns();
|
||||
int count = min == max ? min : RNG.r.i(Math.min(min, max), Math.max(min, max));
|
||||
int count = LootResolver.inclusive(RNG.r, min, max);
|
||||
if (count <= 0) {
|
||||
return 0;
|
||||
}
|
||||
@@ -564,6 +568,9 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
int worldZ = position.getZ();
|
||||
int spawned = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (!LootResolver.oneIn(entityRng, entry.getRarity())) {
|
||||
continue;
|
||||
}
|
||||
if (!lightAllowed(spawner, level, worldX, worldY, worldZ)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
+135
-9
@@ -28,9 +28,11 @@ import net.minecraft.world.level.block.state.BlockState;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
@@ -40,11 +42,12 @@ public final class ModdedCustomContentRegistry {
|
||||
private static final List<ModdedDataProvider> PROVIDERS = new CopyOnWriteArrayList<>();
|
||||
private static final Map<String, BlockState> CUSTOM_BLOCKS = new ConcurrentHashMap<>();
|
||||
private static volatile boolean scanned = false;
|
||||
private static DiscoveryBatch discoveryBatch;
|
||||
|
||||
private ModdedCustomContentRegistry() {
|
||||
}
|
||||
|
||||
public static void registerCustomBlockData(String namespace, String key, String state) {
|
||||
public static synchronized void registerCustomBlockData(String namespace, String key, String state) {
|
||||
if (namespace == null || key == null || state == null) {
|
||||
return;
|
||||
}
|
||||
@@ -60,14 +63,24 @@ public final class ModdedCustomContentRegistry {
|
||||
LOGGER.error("Iris custom block data '{}:{}' has unparseable state '{}'", namespace, key, state, error);
|
||||
return;
|
||||
}
|
||||
CUSTOM_BLOCKS.put(identifier.toString(), parsed);
|
||||
DiscoveryBatch activeBatch = discoveryBatch;
|
||||
if (activeBatch == null) {
|
||||
CUSTOM_BLOCKS.put(identifier.toString(), parsed);
|
||||
} else {
|
||||
activeBatch.customBlocks.put(identifier.toString(), parsed);
|
||||
}
|
||||
LOGGER.info("Iris registered custom block data {}:{} -> {}", namespace, key, state);
|
||||
}
|
||||
|
||||
public static void register(ModdedDataProvider provider) {
|
||||
public static synchronized void register(ModdedDataProvider provider) {
|
||||
if (provider == null) {
|
||||
return;
|
||||
}
|
||||
DiscoveryBatch activeBatch = discoveryBatch;
|
||||
if (activeBatch != null) {
|
||||
activeBatch.add(provider);
|
||||
return;
|
||||
}
|
||||
for (ModdedDataProvider existing : PROVIDERS) {
|
||||
if (existing.modId().equals(provider.modId())) {
|
||||
LOGGER.warn("Iris custom content provider for '{}' already registered; ignoring duplicate", provider.modId());
|
||||
@@ -83,17 +96,66 @@ public final class ModdedCustomContentRegistry {
|
||||
LOGGER.info("Iris registered custom content provider '{}'", provider.modId());
|
||||
}
|
||||
|
||||
public static void discover() {
|
||||
public static synchronized Discovery discover() {
|
||||
if (scanned) {
|
||||
return;
|
||||
return Discovery.unchanged();
|
||||
}
|
||||
scanned = true;
|
||||
ServiceLoader<ModdedDataProvider> loader = ServiceLoader.load(ModdedDataProvider.class, ModdedCustomContentRegistry.class.getClassLoader());
|
||||
for (ModdedDataProvider provider : loader) {
|
||||
register(provider);
|
||||
return discover(ServiceLoader.load(
|
||||
ModdedDataProvider.class, ModdedCustomContentRegistry.class.getClassLoader()));
|
||||
}
|
||||
|
||||
static synchronized Discovery discover(Iterable<ModdedDataProvider> discoveredProviders) {
|
||||
List<ModdedDataProvider> previousProviders = List.copyOf(PROVIDERS);
|
||||
Map<String, BlockState> previousCustomBlocks = new LinkedHashMap<>(CUSTOM_BLOCKS);
|
||||
boolean previousDiscoveryComplete = scanned;
|
||||
DiscoveryBatch batch = new DiscoveryBatch(previousProviders, previousCustomBlocks);
|
||||
discoveryBatch = batch;
|
||||
try {
|
||||
for (ModdedDataProvider provider : discoveredProviders) {
|
||||
batch.add(provider);
|
||||
}
|
||||
PROVIDERS.addAll(batch.additions);
|
||||
CUSTOM_BLOCKS.putAll(batch.customBlocks);
|
||||
scanned = true;
|
||||
for (ModdedDataProvider provider : batch.additions) {
|
||||
LOGGER.info("Iris registered custom content provider '{}'", provider.modId());
|
||||
}
|
||||
return new Discovery(previousProviders, previousCustomBlocks,
|
||||
previousDiscoveryComplete, true);
|
||||
} catch (Throwable failure) {
|
||||
try {
|
||||
restore(previousProviders, previousCustomBlocks, previousDiscoveryComplete);
|
||||
} catch (Throwable rollbackFailure) {
|
||||
if (rollbackFailure != failure) {
|
||||
failure.addSuppressed(rollbackFailure);
|
||||
}
|
||||
}
|
||||
LOGGER.error("Iris custom content provider discovery failed", failure);
|
||||
if (failure instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
if (failure instanceof Error fatalError) {
|
||||
throw fatalError;
|
||||
}
|
||||
throw new IllegalStateException("Iris custom content provider discovery failed", failure);
|
||||
} finally {
|
||||
discoveryBatch = null;
|
||||
}
|
||||
}
|
||||
|
||||
static synchronized boolean hasProvider(String modId) {
|
||||
for (ModdedDataProvider provider : PROVIDERS) {
|
||||
if (Objects.equals(provider.modId(), modId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static boolean discoveryComplete() {
|
||||
return scanned;
|
||||
}
|
||||
|
||||
public static boolean hasProviders() {
|
||||
return !PROVIDERS.isEmpty() || !CUSTOM_BLOCKS.isEmpty();
|
||||
}
|
||||
@@ -203,4 +265,68 @@ public final class ModdedCustomContentRegistry {
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
private static synchronized void restore(List<ModdedDataProvider> providers,
|
||||
Map<String, BlockState> customBlocks,
|
||||
boolean discoveryComplete) {
|
||||
PROVIDERS.clear();
|
||||
PROVIDERS.addAll(providers);
|
||||
CUSTOM_BLOCKS.clear();
|
||||
CUSTOM_BLOCKS.putAll(customBlocks);
|
||||
scanned = discoveryComplete;
|
||||
}
|
||||
|
||||
public static final class Discovery {
|
||||
private final List<ModdedDataProvider> providers;
|
||||
private final Map<String, BlockState> customBlocks;
|
||||
private final boolean discoveryComplete;
|
||||
private boolean active;
|
||||
|
||||
private Discovery(List<ModdedDataProvider> providers, Map<String, BlockState> customBlocks,
|
||||
boolean discoveryComplete, boolean active) {
|
||||
this.providers = providers;
|
||||
this.customBlocks = customBlocks;
|
||||
this.discoveryComplete = discoveryComplete;
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
private static Discovery unchanged() {
|
||||
return new Discovery(List.of(), Map.of(), true, false);
|
||||
}
|
||||
|
||||
public synchronized void rollback() {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
restore(providers, customBlocks, discoveryComplete);
|
||||
active = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class DiscoveryBatch {
|
||||
private final List<ModdedDataProvider> providers;
|
||||
private final List<ModdedDataProvider> additions = new ArrayList<>();
|
||||
private final Map<String, BlockState> customBlocks;
|
||||
|
||||
private DiscoveryBatch(List<ModdedDataProvider> providers, Map<String, BlockState> customBlocks) {
|
||||
this.providers = new ArrayList<>(providers);
|
||||
this.customBlocks = new LinkedHashMap<>(customBlocks);
|
||||
}
|
||||
|
||||
private void add(ModdedDataProvider provider) {
|
||||
ModdedDataProvider candidate = Objects.requireNonNull(provider,
|
||||
"Iris discovered a null custom content provider");
|
||||
String modId = Objects.requireNonNull(candidate.modId(),
|
||||
"Iris custom content provider returned a null mod id");
|
||||
for (ModdedDataProvider existing : providers) {
|
||||
if (modId.equals(existing.modId())) {
|
||||
LOGGER.warn("Iris custom content provider for '{}' already registered; ignoring duplicate", modId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
providers.add(candidate);
|
||||
additions.add(candidate);
|
||||
candidate.init();
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
-15
@@ -25,9 +25,11 @@ import art.arcane.iris.core.pack.PackDownloader;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.framework.Locator;
|
||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.engine.framework.WrongEngineBroException;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisImportedStructureControl;
|
||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
||||
import art.arcane.iris.engine.object.IrisPosition;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
@@ -889,16 +891,27 @@ public final class IrisModdedCommands {
|
||||
fail(source, "This command can only be used by players.");
|
||||
return 0;
|
||||
}
|
||||
if (IrisStructureLocator.isPlaced(engine, key)) {
|
||||
locateIrisStructure(source, level, engine, player, key);
|
||||
return 1;
|
||||
}
|
||||
Optional<NativeStructureTarget> resolved = resolveNativeStructure(source, level, engine, key);
|
||||
if (resolved.isEmpty()) {
|
||||
if (IrisStructureLocator.isPlaced(engine, key)) {
|
||||
locateIrisStructure(source, level, engine, player, key);
|
||||
return 1;
|
||||
}
|
||||
fail(source, "Unknown structure '" + key + "'. Use tab completion to choose an Iris placement or a registered native/datapack structure.");
|
||||
return 0;
|
||||
}
|
||||
NativeStructureTarget target = resolved.get();
|
||||
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, target.key(), false);
|
||||
if (!decision.generate()
|
||||
&& decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
|
||||
fail(source, NativeStructureGenerationPolicy.generationStatusMessage(
|
||||
target.key(), decision.status()));
|
||||
return 0;
|
||||
}
|
||||
if (decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
|
||||
locateIrisStructure(source, level, engine, player, target.key());
|
||||
return 1;
|
||||
}
|
||||
if (target.availability() != NativeStructureAvailability.AVAILABLE) {
|
||||
fail(source, nativeUnavailableMessage(target.key(), target.availability()));
|
||||
return 0;
|
||||
@@ -1035,7 +1048,7 @@ public final class IrisModdedCommands {
|
||||
}
|
||||
}
|
||||
int irisPlaced = IrisStructureLocator.placedKeys(engine).size();
|
||||
ok(source, "Structure reachability: " + available + " native locatable, " + irisPlaced
|
||||
ok(source, "Structure reachability: " + available + " native generation-eligible, " + irisPlaced
|
||||
+ " Iris-placed, " + disabled + " native disabled, " + suppressed
|
||||
+ " native replaced by Iris placements, " + unreachableBiomes
|
||||
+ " native excluded by this pack's biomes, and " + unsupported
|
||||
@@ -1044,16 +1057,22 @@ public final class IrisModdedCommands {
|
||||
}
|
||||
|
||||
private static int verifyStructure(CommandSourceStack source, ServerLevel level, Engine engine, String key) {
|
||||
if (IrisStructureLocator.isPlaced(engine, key)) {
|
||||
ok(source, "Structure " + key + " is Iris-placed and locatable with /iris goto structure " + key + ".");
|
||||
return 1;
|
||||
}
|
||||
Optional<NativeStructureTarget> target = resolveNativeStructure(source, level, engine, key);
|
||||
if (target.isEmpty()) {
|
||||
if (IrisStructureLocator.isPlaced(engine, key)) {
|
||||
ok(source, "Structure " + key + " is Iris-placed and locatable with /iris goto structure " + key + ".");
|
||||
return 1;
|
||||
}
|
||||
fail(source, "Unknown structure '" + key + "'. It is neither Iris-placed nor registered by vanilla or a datapack.");
|
||||
return 0;
|
||||
}
|
||||
NativeStructureTarget resolved = target.get();
|
||||
if (resolved.availability() == NativeStructureAvailability.IRIS_SUPPRESSED) {
|
||||
ok(source, "Structure " + resolved.key()
|
||||
+ " is explicitly replaced by an Iris placement and locatable with /iris goto structure "
|
||||
+ resolved.key() + ".");
|
||||
return 1;
|
||||
}
|
||||
if (resolved.availability() != NativeStructureAvailability.AVAILABLE) {
|
||||
fail(source, nativeUnavailableMessage(resolved.key(), resolved.availability()));
|
||||
return 0;
|
||||
@@ -1084,9 +1103,9 @@ public final class IrisModdedCommands {
|
||||
Engine engine, String key,
|
||||
Holder.Reference<Structure> holder) {
|
||||
boolean worldEnabled = source.getServer().getWorldGenSettings().options().generateStructures();
|
||||
IrisImportedStructureControl control = engine.getDimension().getImportedStructures();
|
||||
boolean selected = control != null && control.active() && control.shouldGenerate(key);
|
||||
boolean suppressed = IrisStructureLocator.suppressesVanilla(engine, key);
|
||||
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, key, false);
|
||||
boolean selected = decision.status() != NativeStructureGenerationStatus.DISABLED_BY_PACK;
|
||||
boolean suppressed = decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS;
|
||||
ChunkGenerator chunkGenerator = level.getChunkSource().getGenerator();
|
||||
boolean biomeReachable = chunkGenerator instanceof IrisModdedChunkGenerator irisGenerator
|
||||
&& irisGenerator.isNativeStructureReachable(holder);
|
||||
@@ -1121,8 +1140,10 @@ public final class IrisModdedCommands {
|
||||
private static String nativeUnavailableMessage(String key, NativeStructureAvailability availability) {
|
||||
return switch (availability) {
|
||||
case WORLD_DISABLED -> "Native structure generation is disabled for this world, so " + key + " cannot generate or be located.";
|
||||
case FILTERED -> "Native structure " + key + " is disabled by this dimension's importedStructures settings.";
|
||||
case IRIS_SUPPRESSED -> "Native structure " + key + " is replaced by an Iris placement in this pack; locate the Iris key instead.";
|
||||
case FILTERED -> NativeStructureGenerationPolicy.generationStatusMessage(
|
||||
key, NativeStructureGenerationStatus.DISABLED_BY_PACK);
|
||||
case IRIS_SUPPRESSED -> NativeStructureGenerationPolicy.generationStatusMessage(
|
||||
key, NativeStructureGenerationStatus.REPLACED_BY_IRIS);
|
||||
case BIOME_UNREACHABLE -> "Native structure " + key + " cannot generate because none of its required biomes are produced by this Iris pack.";
|
||||
case NO_PLACEMENT -> "Native structure " + key + " is registered, but its structure set has no placement supported by this dimension's generator state.";
|
||||
case AVAILABLE -> "Native structure " + key + " is available.";
|
||||
|
||||
+1
@@ -109,6 +109,7 @@ final class ModdedCommandHelp {
|
||||
Entry.command("x&y", "", "Autoselect up, down and out", "xay"),
|
||||
Entry.command("analyze", "<key>", "Show object composition"),
|
||||
Entry.command("shrink", "<key>", "Shrink an object to its minimum size"),
|
||||
Entry.command("plausibilize", "<key|prefix/> [dryrun=true] [reach=N]", "Grow branches so tree leaves survive vanilla decay"),
|
||||
Entry.command("undo", "[amount]", "Undo pasted objects", "u")
|
||||
));
|
||||
SECTIONS.put("o", SECTIONS.get("object"));
|
||||
|
||||
+49
-1
@@ -19,6 +19,8 @@
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.tools.TreePlausibilizeBatch;
|
||||
import art.arcane.iris.core.tools.TreePlausibilizer;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
@@ -27,6 +29,7 @@ import art.arcane.iris.engine.object.TileData;
|
||||
import art.arcane.iris.modded.ModdedBlockState;
|
||||
import art.arcane.iris.modded.ModdedTileData;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
@@ -43,6 +46,7 @@ import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.NbtUtils;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
@@ -157,7 +161,9 @@ public final class ModdedObjectCommands {
|
||||
root.then(bukkitOnly("we", "WorldEdit selection import requires the Bukkit plugin with WorldEdit installed."));
|
||||
root.then(bukkitOnly("studio", "The object studio world requires the Bukkit studio toolchain; it is not available on modded servers."));
|
||||
root.then(bukkitOnly("convert", "Schematic conversion (.schem -> .iob) requires the Bukkit plugin."));
|
||||
root.then(bukkitOnly("plausibilize", "Tree plausibilization requires the Bukkit plugin (its block-data tooling is not ported to modded yet)."));
|
||||
root.then(Commands.literal("plausibilize")
|
||||
.then(Commands.argument("args", StringArgumentType.greedyString()).suggests(OBJECT_KEYS)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> plausibilize(context.getSource(), StringArgumentType.getString(context, "args")))));
|
||||
|
||||
return root;
|
||||
}
|
||||
@@ -645,6 +651,48 @@ public final class ModdedObjectCommands {
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int plausibilize(CommandSourceStack source, String raw) {
|
||||
Engine engine = IrisModdedCommands.engineFor(source.getLevel());
|
||||
IrisData data = engine == null ? null : engine.getData();
|
||||
boolean dryRun = false;
|
||||
int reach = TreePlausibilizer.DEFAULT_REACH;
|
||||
StringBuilder targetBuilder = new StringBuilder();
|
||||
for (String token : raw.trim().split("\\s+")) {
|
||||
String lower = token.toLowerCase(Locale.ROOT);
|
||||
if (lower.startsWith("dryrun=")) {
|
||||
dryRun = Boolean.parseBoolean(lower.substring("dryrun=".length()));
|
||||
continue;
|
||||
}
|
||||
if (lower.startsWith("reach=")) {
|
||||
try {
|
||||
reach = Integer.parseInt(lower.substring("reach=".length()));
|
||||
} catch (NumberFormatException e) {
|
||||
IrisModdedCommands.fail(source, "Invalid reach: " + token);
|
||||
return 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!targetBuilder.isEmpty()) {
|
||||
targetBuilder.append(' ');
|
||||
}
|
||||
targetBuilder.append(token);
|
||||
}
|
||||
String target = targetBuilder.toString();
|
||||
List<TreePlausibilizeBatch.Target> targets = TreePlausibilizeBatch.resolve(target, data);
|
||||
if (targets.isEmpty()) {
|
||||
IrisModdedCommands.fail(source, "No objects matched: " + target);
|
||||
return 0;
|
||||
}
|
||||
IrisModdedCommands.ok(source, "Plausibilize [reach=" + reach + (dryRun ? ", DRY" : "")
|
||||
+ "] queued " + targets.size() + " object(s)");
|
||||
boolean dry = dryRun;
|
||||
int reachFinal = reach;
|
||||
MinecraftServer server = source.getServer();
|
||||
J.a(() -> TreePlausibilizeBatch.run(targets, dry, reachFinal, data, (String line) ->
|
||||
server.execute(() -> IrisModdedCommands.ok(source, line))));
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int undo(CommandSourceStack source, int amount) {
|
||||
ServerPlayer player = source.getPlayer();
|
||||
UUID owner = player == null ? ModdedObjectUndo.CONSOLE : player.getUUID();
|
||||
|
||||
+7
-4
@@ -24,6 +24,7 @@ import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.PlacedStructurePiece;
|
||||
import art.arcane.iris.engine.framework.StructureAssembler;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisPosition;
|
||||
import art.arcane.iris.engine.object.IrisStructure;
|
||||
import art.arcane.iris.engine.object.ObjectPlaceMode;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
@@ -137,12 +138,13 @@ public final class ModdedStructureCommands {
|
||||
return 0;
|
||||
}
|
||||
String key = keyRaw.trim();
|
||||
IrisStructure structure = IrisData.loadAnyStructure(key, data);
|
||||
IrisStructure structure = data.load(IrisStructure.class, key, false);
|
||||
if (structure == null) {
|
||||
IrisModdedCommands.fail(source, "No iris structure '" + key + "' in this pack");
|
||||
return 0;
|
||||
}
|
||||
StructureAssembler assembler = new StructureAssembler(data, structure, 0, 64, 0);
|
||||
StructureAssembler assembler = StructureAssembler.forData(
|
||||
data, structure, new IrisPosition(0, 64, 0));
|
||||
KList<PlacedStructurePiece> pieces = assembler.assemble(new RNG(1234));
|
||||
if (pieces == null || pieces.isEmpty()) {
|
||||
IrisModdedCommands.fail(source, "Structure '" + key + "' assembled 0 pieces (check startPool '" + structure.getStartPool() + "')");
|
||||
@@ -176,7 +178,7 @@ public final class ModdedStructureCommands {
|
||||
return 0;
|
||||
}
|
||||
String key = keyRaw.trim();
|
||||
IrisStructure structure = IrisData.loadAnyStructure(key, data);
|
||||
IrisStructure structure = data.load(IrisStructure.class, key, false);
|
||||
if (structure == null) {
|
||||
IrisModdedCommands.fail(source, "No iris structure '" + key + "' in this pack");
|
||||
return 0;
|
||||
@@ -184,7 +186,8 @@ public final class ModdedStructureCommands {
|
||||
int originX = player.blockPosition().getX();
|
||||
int originY = player.blockPosition().getY();
|
||||
int originZ = player.blockPosition().getZ();
|
||||
StructureAssembler assembler = new StructureAssembler(data, structure, originX, originY, originZ);
|
||||
StructureAssembler assembler = StructureAssembler.forData(
|
||||
data, structure, new IrisPosition(originX, originY, originZ));
|
||||
RNG rng = new RNG((long) originX * 341873128712L + originZ);
|
||||
KList<PlacedStructurePiece> pieces = assembler.assemble(rng);
|
||||
if (pieces == null || pieces.isEmpty()) {
|
||||
|
||||
+25
@@ -23,6 +23,7 @@ import art.arcane.iris.core.gui.GuiHost;
|
||||
import art.arcane.iris.core.gui.NoiseExplorerGUI;
|
||||
import art.arcane.iris.core.gui.VisionGUI;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.pack.StructurePackageClosure;
|
||||
import art.arcane.iris.core.project.IrisProjectCopier;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
@@ -33,6 +34,7 @@ import art.arcane.iris.engine.object.IrisGenerator;
|
||||
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.modded.ModdedDimensionManager;
|
||||
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
||||
import art.arcane.iris.modded.ModdedPackInstaller;
|
||||
@@ -635,10 +637,12 @@ public final class ModdedStudioCommands {
|
||||
LinkedHashSet<String> generatorKeys = new LinkedHashSet<>();
|
||||
LinkedHashSet<String> lootKeys = new LinkedHashSet<>();
|
||||
LinkedHashSet<String> objectKeys = new LinkedHashSet<>();
|
||||
LinkedHashSet<String> structureKeys = new LinkedHashSet<>();
|
||||
|
||||
regionKeys.addAll(dimension.getRegions());
|
||||
lootKeys.addAll(dimension.getLoot().getTables());
|
||||
spawnerKeys.addAll(dimension.getEntitySpawners());
|
||||
collectStructureKeys(structureKeys, dimension.getStructures());
|
||||
|
||||
for (String regionKey : regionKeys) {
|
||||
IrisRegion region = dm.getRegionLoader().load(regionKey);
|
||||
@@ -652,6 +656,7 @@ public final class ModdedStudioCommands {
|
||||
});
|
||||
lootKeys.addAll(region.getLoot().getTables());
|
||||
spawnerKeys.addAll(region.getEntitySpawners());
|
||||
collectStructureKeys(structureKeys, region.getStructures());
|
||||
}
|
||||
for (String biomeKey : biomeKeys) {
|
||||
IrisBiome biome = dm.getBiomeLoader().load(biomeKey);
|
||||
@@ -661,6 +666,7 @@ public final class ModdedStudioCommands {
|
||||
biome.getGenerators().forEach((IrisBiomeGeneratorLink link) -> generatorKeys.add(link.getGenerator()));
|
||||
lootKeys.addAll(biome.getLoot().getTables());
|
||||
spawnerKeys.addAll(biome.getEntitySpawners());
|
||||
collectStructureKeys(structureKeys, biome.getStructures());
|
||||
for (IrisObjectPlacement placement : biome.getObjects()) {
|
||||
objectKeys.addAll(placement.getPlace());
|
||||
}
|
||||
@@ -674,6 +680,11 @@ public final class ModdedStudioCommands {
|
||||
}
|
||||
|
||||
StringBuilder hashes = new StringBuilder();
|
||||
StructurePackageClosure structureClosure = StructurePackageClosure.collect(packFolder, structureKeys);
|
||||
if (!structureClosure.isValid()) {
|
||||
throw new IOException("Structure package closure is invalid: " + String.join("; ", structureClosure.errors()));
|
||||
}
|
||||
hashes.append(structureClosure.writeTo(folder, true));
|
||||
for (String objectKey : objectKeys) {
|
||||
try {
|
||||
File objectFile = dm.getObjectLoader().findFile(objectKey);
|
||||
@@ -716,6 +727,20 @@ public final class ModdedStudioCommands {
|
||||
return output;
|
||||
}
|
||||
|
||||
private static void collectStructureKeys(LinkedHashSet<String> keys, List<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String copyJson(File folder, String category, String key, File file) {
|
||||
if (file == null || !file.isFile()) {
|
||||
return "";
|
||||
|
||||
+19
-17
@@ -19,8 +19,7 @@
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.core.pack.PackValidationResult;
|
||||
import art.arcane.iris.core.pack.BrokenPackException;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.MainWorldService;
|
||||
@@ -29,6 +28,7 @@ import art.arcane.iris.modded.ModdedEngineBootstrap;
|
||||
import art.arcane.iris.modded.ModdedModConfig;
|
||||
import art.arcane.iris.modded.ModdedPackInstaller;
|
||||
import art.arcane.iris.modded.ModdedPrimaryWorldRouter;
|
||||
import art.arcane.iris.modded.ModdedStartup;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
@@ -187,10 +187,10 @@ public final class ModdedWorldCommands {
|
||||
}
|
||||
|
||||
private static int enableInstalled(CommandSourceStack source, MinecraftServer server, String dimensionId, String pack, String packDimension, long seed) {
|
||||
if (!loadPackDimension(source, pack, packDimension)) {
|
||||
if (blockIfPackBroken(source, dimensionId, pack)) {
|
||||
return 0;
|
||||
}
|
||||
if (blockIfPackBroken(source, dimensionId, pack)) {
|
||||
if (!loadPackDimension(source, pack, packDimension)) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
@@ -218,10 +218,10 @@ public final class ModdedWorldCommands {
|
||||
return 0;
|
||||
}
|
||||
String dimensionId = DEFAULT_NAMESPACE + ":primary";
|
||||
if (!loadPackDimension(source, pack, packDimension)) {
|
||||
if (blockIfPackBroken(source, dimensionId, pack)) {
|
||||
return 0;
|
||||
}
|
||||
if (blockIfPackBroken(source, dimensionId, pack)) {
|
||||
if (!loadPackDimension(source, pack, packDimension)) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
@@ -290,6 +290,9 @@ public final class ModdedWorldCommands {
|
||||
}
|
||||
|
||||
private static int applyMainWorld(CommandSourceStack source, String pack, String packDimension, String packRef, long seed) {
|
||||
if (blockIfPackBroken(source, "the main world", pack)) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
if (!loadPackDimension(source, pack, packDimension)) {
|
||||
return 0;
|
||||
@@ -299,9 +302,6 @@ public final class ModdedWorldCommands {
|
||||
IrisModdedCommands.fail(source, "Pack '" + pack + "' is not ready yet (still loading or validating). Try the command again in a moment.");
|
||||
return 0;
|
||||
}
|
||||
if (blockIfPackBroken(source, "the main world", pack)) {
|
||||
return 0;
|
||||
}
|
||||
ModdedModConfig.setMainWorld(packRef, seed);
|
||||
if (!MainWorldService.stage(packRef, seed)) {
|
||||
IrisModdedCommands.fail(source, "Failed to write server.properties; check file permissions and set level-type manually.");
|
||||
@@ -321,16 +321,18 @@ public final class ModdedWorldCommands {
|
||||
}
|
||||
|
||||
private static boolean blockIfPackBroken(CommandSourceStack source, String dimensionId, String pack) {
|
||||
PackValidationResult validation = PackValidationRegistry.get(pack);
|
||||
if (validation == null || validation.isLoadable()) {
|
||||
try {
|
||||
ModdedStartup.requirePackForWorldCreation(pack);
|
||||
return false;
|
||||
} catch (BrokenPackException e) {
|
||||
IrisModdedCommands.fail(source, "Refusing to create world '" + dimensionId
|
||||
+ "' using pack '" + pack + "' because required validation failed:");
|
||||
for (String reason : e.getReasons()) {
|
||||
IrisModdedCommands.fail(source, " - " + reason);
|
||||
}
|
||||
IrisModdedCommands.fail(source, "Fix the pack and run /iris pack validate " + pack + " to revalidate.");
|
||||
return true;
|
||||
}
|
||||
IrisModdedCommands.fail(source, "Refusing to create world '" + dimensionId + "' using broken pack '" + pack + "':");
|
||||
for (String reason : validation.getBlockingErrors()) {
|
||||
IrisModdedCommands.fail(source, " - " + reason);
|
||||
}
|
||||
IrisModdedCommands.fail(source, "Fix the pack and run /iris pack validate " + pack + " to revalidate.");
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean loadPackDimension(CommandSourceStack source, String pack, String packDimension) {
|
||||
|
||||
+4
-8
@@ -34,8 +34,6 @@ import art.arcane.iris.util.project.matter.TileWrapper;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.math.BlockPosition;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import art.arcane.volmlib.util.matter.MatterUpdate;
|
||||
@@ -287,7 +285,6 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||
}
|
||||
}
|
||||
|
||||
RNG rng = new RNG(Cache.key(chunkX, chunkZ));
|
||||
chunk.iterate(MatterCavern.class, (Integer x, Integer yf, Integer z, MatterCavern v) -> {
|
||||
int y = yf + minHeight;
|
||||
if (y < level.getMinY() || y >= level.getMaxY()) {
|
||||
@@ -315,20 +312,20 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||
if (grid[x][z] == Integer.MIN_VALUE) {
|
||||
continue;
|
||||
}
|
||||
update(engine, level, x, grid[x][z], z, baseX, baseZ, chunk, rng);
|
||||
update(engine, level, x, grid[x][z], z, baseX, baseZ, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
chunk.iterate(MatterUpdate.class, (Integer x, Integer yf, Integer z, MatterUpdate v) -> {
|
||||
if (v != null && v.isUpdate()) {
|
||||
update(engine, level, x, yf + minHeight, z, baseX, baseZ, chunk, rng);
|
||||
update(engine, level, x, yf + minHeight, z, baseX, baseZ, chunk);
|
||||
}
|
||||
});
|
||||
chunk.deleteSlices(MatterUpdate.class);
|
||||
engine.getMetrics().getUpdates().put(stopwatch.getMilliseconds());
|
||||
}
|
||||
|
||||
private void update(Engine engine, ServerLevel level, int x, int y, int z, int baseX, int baseZ, MantleChunk<Matter> chunk, RNG rng) {
|
||||
private void update(Engine engine, ServerLevel level, int x, int y, int z, int baseX, int baseZ, MantleChunk<Matter> chunk) {
|
||||
if (y < level.getMinY() || y >= level.getMaxY()) {
|
||||
return;
|
||||
}
|
||||
@@ -339,9 +336,8 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||
if (!ModdedBlockResolution.isStorageChest(state)) {
|
||||
return;
|
||||
}
|
||||
RNG rx = rng.nextParallelRNG(BlockPosition.toLong(x, y, z));
|
||||
try {
|
||||
ModdedLootApplier.apply(engine, level, pos, state, chunk, rx, x & 15, z & 15);
|
||||
ModdedLootApplier.apply(engine, level, pos, state, chunk);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
|
||||
+620
@@ -0,0 +1,620 @@
|
||||
/*
|
||||
* 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.modded.structure;
|
||||
|
||||
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.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.TileData;
|
||||
import art.arcane.iris.util.common.math.IrisBlockVector;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.Vec3i;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.NbtOps;
|
||||
import net.minecraft.nbt.NbtUtils;
|
||||
import net.minecraft.nbt.Tag;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.RegistryOps;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.JigsawBlock;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.EmptyPoolElement;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.FeaturePoolElement;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.LegacySinglePoolElement;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.alias.PoolAliasBinding;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
final class ModdedJigsawStructureCapture {
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
|
||||
private final MinecraftServer server;
|
||||
private final StructureKey sourceKey;
|
||||
private final StructureKey targetKey;
|
||||
private final StructureSource.Kind sourceKind;
|
||||
private final Registry<Structure> structureRegistry;
|
||||
private final Registry<StructureTemplatePool> poolRegistry;
|
||||
private final Registry<Block> blockRegistry;
|
||||
private final StructureTemplateManager templateManager;
|
||||
private final RegistryOps<Tag> registryOps;
|
||||
private final Map<ResourceKey<StructureTemplatePool>, ResourceKey<StructureTemplatePool>> aliases;
|
||||
private final Map<String, byte[]> objects;
|
||||
private final Map<String, Map<String, Object>> pieces;
|
||||
private final Map<String, Map<String, Object>> pools;
|
||||
private final EnumSet<StructureCapability> capabilities;
|
||||
private final List<StructureLoss> losses;
|
||||
private final Set<String> recordedLosses;
|
||||
private final Set<String> visitedPools;
|
||||
private final Deque<String> pendingPools;
|
||||
private int blocks;
|
||||
|
||||
private ModdedJigsawStructureCapture(
|
||||
MinecraftServer server,
|
||||
StructureKey sourceKey,
|
||||
StructureKey targetKey,
|
||||
StructureSource.Kind sourceKind
|
||||
) {
|
||||
this.server = Objects.requireNonNull(server);
|
||||
this.sourceKey = Objects.requireNonNull(sourceKey);
|
||||
this.targetKey = Objects.requireNonNull(targetKey);
|
||||
this.sourceKind = Objects.requireNonNull(sourceKind);
|
||||
structureRegistry = server.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
poolRegistry = server.registryAccess().lookupOrThrow(Registries.TEMPLATE_POOL);
|
||||
blockRegistry = server.registryAccess().lookupOrThrow(Registries.BLOCK);
|
||||
templateManager = server.getStructureManager();
|
||||
registryOps = RegistryOps.create(NbtOps.INSTANCE, server.registryAccess());
|
||||
aliases = new HashMap<>();
|
||||
objects = new LinkedHashMap<>();
|
||||
pieces = new LinkedHashMap<>();
|
||||
pools = new LinkedHashMap<>();
|
||||
capabilities = EnumSet.of(
|
||||
StructureCapability.BLOCKS,
|
||||
StructureCapability.CONNECTORS,
|
||||
StructureCapability.IRIS_PLACEMENT
|
||||
);
|
||||
losses = new ArrayList<>();
|
||||
recordedLosses = new HashSet<>();
|
||||
visitedPools = new HashSet<>();
|
||||
pendingPools = new ArrayDeque<>();
|
||||
}
|
||||
|
||||
static Capture capture(
|
||||
MinecraftServer server,
|
||||
StructureKey sourceKey,
|
||||
StructureKey targetKey,
|
||||
StructureSource.Kind sourceKind
|
||||
) throws IOException {
|
||||
return new ModdedJigsawStructureCapture(server, sourceKey, targetKey, sourceKind).capture();
|
||||
}
|
||||
|
||||
private Capture capture() throws IOException {
|
||||
Identifier sourceIdentifier = identifier(sourceKey);
|
||||
Structure structure = structureRegistry.getValue(sourceIdentifier);
|
||||
if (structure == null) {
|
||||
throw new IllegalArgumentException("No registered structure exists for " + sourceKey);
|
||||
}
|
||||
if (!(structure instanceof JigsawStructure jigsaw)) {
|
||||
throw new UnsupportedStructureTypeException("Structure " + sourceKey + " uses "
|
||||
+ structure.getClass().getSimpleName() + "; only jigsaw structure graphs can be converted to Iris assembly resources");
|
||||
}
|
||||
|
||||
CompoundTag encodedStructure = encodeStructure(structure);
|
||||
configureAliases(jigsaw);
|
||||
recordRootLosses(jigsaw);
|
||||
String startPoolKey = poolKey(jigsaw.getStartPool().value());
|
||||
pendingPools.add(startPoolKey);
|
||||
while (!pendingPools.isEmpty()) {
|
||||
capturePool(pendingPools.removeFirst());
|
||||
}
|
||||
if (pieces.isEmpty()) {
|
||||
throw new IllegalStateException("Structure " + sourceKey + " produced no importable pieces");
|
||||
}
|
||||
|
||||
int maxDepth = Math.max(1, encodedStructure.getIntOr("size", 1));
|
||||
int maxDistance = readHorizontalDistance(encodedStructure);
|
||||
StructureSource source = StructureSource.identified(
|
||||
sourceKind,
|
||||
sourceKey,
|
||||
SharedConstants.getCurrentVersion().name(),
|
||||
NbtUtils.structureToSnbt(encodedStructure).getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
StructureResourceBundle bundle = buildBundle(source, startPoolKey, maxDepth, maxDistance);
|
||||
return new Capture(bundle, blocks, pieces.size(), pools.size());
|
||||
}
|
||||
|
||||
private CompoundTag encodeStructure(Structure structure) {
|
||||
Tag encoded = Structure.DIRECT_CODEC.encodeStart(registryOps, structure).getOrThrow();
|
||||
if (!(encoded instanceof CompoundTag compound)) {
|
||||
throw new IllegalStateException("Structure codec did not produce a compound for " + sourceKey);
|
||||
}
|
||||
return compound;
|
||||
}
|
||||
|
||||
private void configureAliases(JigsawStructure structure) {
|
||||
List<PoolAliasBinding> bindings = structure.getPoolAliases();
|
||||
if (bindings.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
RandomSource random = RandomSource.create(stableSeed(sourceKey.value()));
|
||||
for (PoolAliasBinding binding : bindings) {
|
||||
binding.forEachResolved(random, aliases::put);
|
||||
}
|
||||
addLossOnce(
|
||||
"pool_aliases_resolved_once",
|
||||
StructureLoss.warning(
|
||||
StructureCapability.CONNECTORS,
|
||||
"pool_aliases_resolved_once",
|
||||
bindings.size() + " native pool alias binding(s) were resolved deterministically for the imported graph; per-placement alias variation is not represented."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private void recordRootLosses(JigsawStructure structure) {
|
||||
losses.add(StructureLoss.warning(
|
||||
StructureCapability.NATIVE_PLACEMENT,
|
||||
"native_placement_settings_not_imported",
|
||||
"Native start height, heightmap projection, expansion, padding, and placement-set settings are not represented by Iris assembly placement."
|
||||
));
|
||||
losses.add(StructureLoss.warning(
|
||||
StructureCapability.LIQUID_SETTINGS,
|
||||
"native_liquid_settings_not_imported",
|
||||
"Native structure liquid placement behavior is not represented beyond the captured block and waterlogged states."
|
||||
));
|
||||
if (structure.terrainAdaptation() != TerrainAdjustment.NONE) {
|
||||
losses.add(StructureLoss.warning(
|
||||
StructureCapability.TERRAIN_ADAPTATION,
|
||||
"terrain_adaptation_not_imported",
|
||||
"Native terrain adaptation '" + structure.terrainAdaptation().getSerializedName()
|
||||
+ "' is not represented by the Iris assembly."
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private void capturePool(String sourcePoolKey) throws IOException {
|
||||
if (!visitedPools.add(sourcePoolKey)) {
|
||||
return;
|
||||
}
|
||||
Identifier sourcePoolIdentifier = Identifier.tryParse(sourcePoolKey);
|
||||
StructureTemplatePool pool = sourcePoolIdentifier == null ? null : poolRegistry.getValue(sourcePoolIdentifier);
|
||||
if (pool == null) {
|
||||
throw new IllegalStateException("Jigsaw graph references missing template pool " + sourcePoolKey);
|
||||
}
|
||||
String irisPoolName = poolName(targetKey.path(), sourcePoolKey);
|
||||
List<Object> entries = new ArrayList<>();
|
||||
List<Pair<StructurePoolElement, Integer>> templates = pool.getTemplates();
|
||||
for (int index = 0; index < templates.size(); index++) {
|
||||
Pair<StructurePoolElement, Integer> weighted = templates.get(index);
|
||||
StructurePoolElement element = weighted.getFirst();
|
||||
int weight = Math.max(1, weighted.getSecond());
|
||||
Map<String, Object> entry = new LinkedHashMap<>();
|
||||
if (element == EmptyPoolElement.INSTANCE) {
|
||||
entry.put("empty", true);
|
||||
} else {
|
||||
entry.put("piece", captureElement(sourcePoolKey, index, element));
|
||||
}
|
||||
entry.put("weight", weight);
|
||||
entries.add(entry);
|
||||
}
|
||||
|
||||
Map<String, Object> poolJson = new LinkedHashMap<>();
|
||||
poolJson.put("pieces", entries);
|
||||
String fallback = resolvedPoolKey(pool.getFallback().value());
|
||||
if (!fallback.equals(sourcePoolKey)) {
|
||||
poolJson.put("fallback", poolName(targetKey.path(), fallback));
|
||||
pendingPools.addLast(fallback);
|
||||
}
|
||||
pools.put(irisPoolName, poolJson);
|
||||
}
|
||||
|
||||
private String captureElement(String sourcePoolKey, int index, StructurePoolElement element) throws IOException {
|
||||
Objects.requireNonNull(element, "pool element");
|
||||
if (element instanceof SinglePoolElement single) {
|
||||
return captureSingle(single);
|
||||
}
|
||||
String generatedName = generatedPieceName(targetKey.path(), sourcePoolKey, index, element);
|
||||
if (pieces.containsKey(generatedName)) {
|
||||
return generatedName;
|
||||
}
|
||||
if (element instanceof ListPoolElement list) {
|
||||
capabilities.add(StructureCapability.LIST_ELEMENTS);
|
||||
CompositeCapture composite = captureList(list, generatedName);
|
||||
blocks += composite.object().getBlocks().size();
|
||||
emitPiece(generatedName, composite.object(), composite.losses(), element);
|
||||
return generatedName;
|
||||
}
|
||||
IrisObject object = emptyObject(element);
|
||||
StructureCapability unsupported = element instanceof FeaturePoolElement
|
||||
? StructureCapability.FEATURE_ELEMENTS : StructureCapability.BLOCKS;
|
||||
StructureLoss loss = StructureLoss.warning(
|
||||
unsupported,
|
||||
"unsupported_pool_element",
|
||||
"Pool element " + element.getClass().getSimpleName() + " was represented as an empty Iris piece."
|
||||
);
|
||||
emitPiece(generatedName, object, List.of(loss), element);
|
||||
return generatedName;
|
||||
}
|
||||
|
||||
private String captureSingle(SinglePoolElement element) throws IOException {
|
||||
Identifier templateIdentifier = element.getTemplateLocation();
|
||||
StructureKey templateKey = StructureKey.parse(templateIdentifier.toString());
|
||||
boolean legacy = element instanceof LegacySinglePoolElement;
|
||||
String pieceName = legacy
|
||||
? legacyPieceName(targetKey.path(), templateKey.value())
|
||||
: pieceName(targetKey.path(), templateKey.value());
|
||||
if (pieces.containsKey(pieceName)) {
|
||||
return pieceName;
|
||||
}
|
||||
StructureTemplate template = templateManager.get(templateIdentifier)
|
||||
.orElseThrow(() -> new IllegalStateException("Missing structure template " + templateIdentifier));
|
||||
ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.capture(
|
||||
templateKey, template, blockRegistry, true, !legacy);
|
||||
capabilities.addAll(capture.capabilities());
|
||||
blocks += capture.blocks();
|
||||
List<StructureLoss> pieceLosses = new ArrayList<>(capture.losses());
|
||||
pieceLosses.addAll(elementLosses(element));
|
||||
emitPiece(pieceName, capture.object(), pieceLosses, element);
|
||||
return pieceName;
|
||||
}
|
||||
|
||||
private CompositeCapture captureList(ListPoolElement list, String pieceName) throws IOException {
|
||||
Vec3i size = list.getSize(templateManager, Rotation.NONE);
|
||||
IrisObject composite = new IrisObject(
|
||||
Math.max(1, size.getX()),
|
||||
Math.max(1, size.getY()),
|
||||
Math.max(1, size.getZ())
|
||||
);
|
||||
List<StructureLoss> compositeLosses = new ArrayList<>();
|
||||
for (StructurePoolElement child : list.getElements()) {
|
||||
if (child == EmptyPoolElement.INSTANCE) {
|
||||
continue;
|
||||
}
|
||||
if (child instanceof SinglePoolElement single) {
|
||||
Identifier templateIdentifier = single.getTemplateLocation();
|
||||
StructureTemplate template = templateManager.get(templateIdentifier)
|
||||
.orElseThrow(() -> new IllegalStateException("Missing structure template " + templateIdentifier));
|
||||
ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.capture(
|
||||
StructureKey.parse(templateIdentifier.toString()),
|
||||
template,
|
||||
blockRegistry,
|
||||
true,
|
||||
!(single instanceof LegacySinglePoolElement)
|
||||
);
|
||||
merge(composite, capture.object());
|
||||
capabilities.addAll(capture.capabilities());
|
||||
compositeLosses.addAll(capture.losses());
|
||||
compositeLosses.addAll(elementLosses(single));
|
||||
continue;
|
||||
}
|
||||
if (child instanceof ListPoolElement nested) {
|
||||
CompositeCapture nestedCapture = captureList(nested, pieceName);
|
||||
merge(composite, nestedCapture.object());
|
||||
compositeLosses.addAll(nestedCapture.losses());
|
||||
continue;
|
||||
}
|
||||
StructureCapability unsupported = child instanceof FeaturePoolElement
|
||||
? StructureCapability.FEATURE_ELEMENTS : StructureCapability.LIST_ELEMENTS;
|
||||
compositeLosses.add(StructureLoss.warning(
|
||||
unsupported,
|
||||
"list_child_not_imported",
|
||||
"List element child " + child.getClass().getSimpleName() + " could not be flattened into the Iris object."
|
||||
));
|
||||
}
|
||||
compositeLosses.addAll(elementLosses(list));
|
||||
return new CompositeCapture(composite, compositeLosses);
|
||||
}
|
||||
|
||||
private List<StructureLoss> elementLosses(StructurePoolElement element) {
|
||||
List<StructureLoss> elementLosses = new ArrayList<>();
|
||||
if (element.getProjection() != StructureTemplatePool.Projection.RIGID) {
|
||||
elementLosses.add(StructureLoss.warning(
|
||||
StructureCapability.PROJECTION,
|
||||
"terrain_matching_projection_not_imported",
|
||||
"Pool projection '" + element.getProjection().getSerializedName()
|
||||
+ "' was converted to rigid Iris piece placement."
|
||||
));
|
||||
}
|
||||
Tag encoded = StructurePoolElement.CODEC.encodeStart(registryOps, element).getOrThrow();
|
||||
if (encoded instanceof CompoundTag compound) {
|
||||
String processors = compound.getStringOr("processors", "");
|
||||
if (!processors.isEmpty() && !processors.equals("minecraft:empty")) {
|
||||
elementLosses.add(StructureLoss.warning(
|
||||
StructureCapability.PROCESSORS,
|
||||
"native_processors_not_imported",
|
||||
"Native processor list '" + processors + "' was not applied to the captured template."
|
||||
));
|
||||
}
|
||||
if (compound.contains("override_liquid_settings")) {
|
||||
elementLosses.add(StructureLoss.warning(
|
||||
StructureCapability.LIQUID_SETTINGS,
|
||||
"element_liquid_settings_not_imported",
|
||||
"The pool element's liquid setting override is not represented by Iris placement."
|
||||
));
|
||||
}
|
||||
}
|
||||
return elementLosses;
|
||||
}
|
||||
|
||||
private void emitPiece(
|
||||
String pieceName,
|
||||
IrisObject object,
|
||||
List<StructureLoss> pieceLosses,
|
||||
StructurePoolElement element
|
||||
) throws IOException {
|
||||
String objectResource = "objects/" + pieceName + ".iob";
|
||||
objects.put(pieceName, serialize(object));
|
||||
for (StructureLoss loss : pieceLosses) {
|
||||
losses.add(loss.affecting(objectResource));
|
||||
}
|
||||
List<Map<String, Object>> connectors = connectors(element, pieceName);
|
||||
Map<String, Object> pieceJson = new LinkedHashMap<>();
|
||||
pieceJson.put("object", pieceName);
|
||||
pieceJson.put("connectors", connectors);
|
||||
pieceJson.put("rotatable", true);
|
||||
pieces.put(pieceName, pieceJson);
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> connectors(StructurePoolElement element, String pieceName) {
|
||||
List<StructureTemplate.JigsawBlockInfo> sourceConnectors = element.getShuffledJigsawBlocks(
|
||||
templateManager,
|
||||
BlockPos.ZERO,
|
||||
Rotation.NONE,
|
||||
RandomSource.create(stableSeed(sourceKey.value() + ":" + pieceName))
|
||||
);
|
||||
List<Map<String, Object>> connectors = new ArrayList<>(sourceConnectors.size());
|
||||
for (StructureTemplate.JigsawBlockInfo source : sourceConnectors) {
|
||||
recordConnectorLosses(source, pieceName);
|
||||
ResourceKey<StructureTemplatePool> resolvedPool = aliases.getOrDefault(source.pool(), source.pool());
|
||||
String sourcePoolKey = resolvedPool.identifier().toString();
|
||||
pendingPools.addLast(sourcePoolKey);
|
||||
Map<String, Object> position = new LinkedHashMap<>();
|
||||
position.put("x", source.info().pos().getX());
|
||||
position.put("y", source.info().pos().getY());
|
||||
position.put("z", source.info().pos().getZ());
|
||||
Map<String, Object> connector = new LinkedHashMap<>();
|
||||
connector.put("position", position);
|
||||
connector.put("direction", directionName(JigsawBlock.getFrontFacing(source.info().state())));
|
||||
connector.put("top", directionName(JigsawBlock.getTopFacing(source.info().state())));
|
||||
connector.put("pool", poolName(targetKey.path(), sourcePoolKey));
|
||||
connector.put("name", source.name().toString());
|
||||
connector.put("targetName", source.target().toString());
|
||||
connector.put("joint", source.jointType().getSerializedName().equals("aligned") ? "ALIGNED" : "ROLLABLE");
|
||||
connectors.add(connector);
|
||||
}
|
||||
return connectors;
|
||||
}
|
||||
|
||||
private void recordConnectorLosses(StructureTemplate.JigsawBlockInfo connector, String pieceName) {
|
||||
String pieceResource = "jigsaw-pieces/" + pieceName + ".json";
|
||||
if (connector.placementPriority() != 0 || connector.selectionPriority() != 0) {
|
||||
addLossOnce(
|
||||
"connector-priority:" + pieceName,
|
||||
StructureLoss.warning(
|
||||
StructureCapability.CONNECTORS,
|
||||
"connector_priorities_not_imported",
|
||||
"Native connector placement and selection priorities are not represented by Iris assembly ordering."
|
||||
).affecting(pieceResource)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private StructureResourceBundle buildBundle(
|
||||
StructureSource source,
|
||||
String startPoolKey,
|
||||
int maxDepth,
|
||||
int maxDistance
|
||||
) {
|
||||
StructureResourceBundle.Builder bundle = StructureResourceBundle.builder(targetKey)
|
||||
.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/" + targetKey.path() + ".json",
|
||||
GSON.toJson(structureJson(sourceKey.value(), poolName(targetKey.path(), startPoolKey), maxDepth, maxDistance))
|
||||
);
|
||||
return bundle.build();
|
||||
}
|
||||
|
||||
private String resolvedPoolKey(StructureTemplatePool pool) {
|
||||
ResourceKey<StructureTemplatePool> raw = poolResourceKey(pool);
|
||||
return aliases.getOrDefault(raw, raw).identifier().toString();
|
||||
}
|
||||
|
||||
private String poolKey(StructureTemplatePool pool) {
|
||||
return poolResourceKey(pool).identifier().toString();
|
||||
}
|
||||
|
||||
private ResourceKey<StructureTemplatePool> poolResourceKey(StructureTemplatePool pool) {
|
||||
Identifier identifier = poolRegistry.getKey(pool);
|
||||
if (identifier == null) {
|
||||
throw new IllegalStateException("Jigsaw structure references an unregistered template pool");
|
||||
}
|
||||
return ResourceKey.create(Registries.TEMPLATE_POOL, identifier);
|
||||
}
|
||||
|
||||
private void addLossOnce(String key, StructureLoss loss) {
|
||||
if (recordedLosses.add(key)) {
|
||||
losses.add(loss);
|
||||
}
|
||||
}
|
||||
|
||||
private static void merge(IrisObject target, IrisObject source) {
|
||||
for (IrisBlockVector position : source.getBlocks().keys()) {
|
||||
int x = position.getBlockX() + source.getCenter().getX();
|
||||
int y = position.getBlockY() + source.getCenter().getY();
|
||||
int z = position.getBlockZ() + source.getCenter().getZ();
|
||||
if (x < 0 || y < 0 || z < 0 || x >= target.getW() || y >= target.getH() || z >= target.getD()) {
|
||||
continue;
|
||||
}
|
||||
target.setUnsignedTile(x, y, z, null);
|
||||
target.setUnsigned(x, y, z, source.getBlocks().get(position));
|
||||
TileData tile = source.getStates().get(position);
|
||||
if (tile != null) {
|
||||
target.setUnsignedTile(x, y, z, tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IrisObject emptyObject() {
|
||||
return new IrisObject(1, 1, 1);
|
||||
}
|
||||
|
||||
private IrisObject emptyObject(StructurePoolElement element) {
|
||||
Vec3i size = element.getSize(templateManager, Rotation.NONE);
|
||||
return new IrisObject(
|
||||
Math.max(1, size.getX()),
|
||||
Math.max(1, size.getY()),
|
||||
Math.max(1, size.getZ())
|
||||
);
|
||||
}
|
||||
|
||||
private static byte[] serialize(IrisObject object) throws IOException {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
object.write(output);
|
||||
return output.toByteArray();
|
||||
}
|
||||
|
||||
private static int readHorizontalDistance(CompoundTag encodedStructure) {
|
||||
int scalar = encodedStructure.getIntOr("max_distance_from_center", -1);
|
||||
if (scalar > 0) {
|
||||
return scalar;
|
||||
}
|
||||
CompoundTag compound = encodedStructure.getCompoundOrEmpty("max_distance_from_center");
|
||||
return Math.max(1, compound.getIntOr("horizontal", 80));
|
||||
}
|
||||
|
||||
static Map<String, Object> structureJson(String source, String startPool, int maxDepth, int maxDistance) {
|
||||
Map<String, Object> root = new LinkedHashMap<>();
|
||||
root.put("startPool", startPool);
|
||||
root.put("maxDepth", Math.max(1, Math.min(30, maxDepth)));
|
||||
root.put("maxSizeChunks", Math.max(1, Math.min(32, (Math.max(1, maxDistance) + 15) / 16)));
|
||||
root.put("placeMode", "STRUCTURE_PIECE");
|
||||
root.put("vanillaSource", source);
|
||||
return root;
|
||||
}
|
||||
|
||||
static String poolName(String base, String sourcePoolKey) {
|
||||
StructureKey key = StructureKey.parse(sourcePoolKey);
|
||||
return base + "/pool/" + key.namespace() + "/" + key.path();
|
||||
}
|
||||
|
||||
static String pieceName(String base, String templateKey) {
|
||||
StructureKey key = StructureKey.parse(templateKey);
|
||||
return base + "/piece/" + key.namespace() + "/" + key.path();
|
||||
}
|
||||
|
||||
static String legacyPieceName(String base, String templateKey) {
|
||||
StructureKey key = StructureKey.parse(templateKey);
|
||||
return base + "/piece/generated/legacy/" + key.namespace() + "/" + key.path();
|
||||
}
|
||||
|
||||
static String directionName(Direction direction) {
|
||||
return switch (direction) {
|
||||
case UP -> "UP_POSITIVE_Y";
|
||||
case DOWN -> "DOWN_NEGATIVE_Y";
|
||||
case SOUTH -> "SOUTH_POSITIVE_Z";
|
||||
case EAST -> "EAST_POSITIVE_X";
|
||||
case WEST -> "WEST_NEGATIVE_X";
|
||||
case NORTH -> "NORTH_NEGATIVE_Z";
|
||||
};
|
||||
}
|
||||
|
||||
private static String generatedPieceName(String base, String sourcePoolKey, int index, StructurePoolElement element) {
|
||||
StructureKey poolKey = StructureKey.parse(sourcePoolKey);
|
||||
String type = element instanceof ListPoolElement ? "list" : element instanceof FeaturePoolElement ? "feature" : "unsupported";
|
||||
return base + "/piece/generated/" + type + "/" + poolKey.namespace() + "/" + poolKey.path() + "/" + index;
|
||||
}
|
||||
|
||||
private static long stableSeed(String value) {
|
||||
long hash = 0xcbf29ce484222325L;
|
||||
for (int index = 0; index < value.length(); index++) {
|
||||
hash ^= value.charAt(index);
|
||||
hash *= 0x100000001b3L;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
private static Identifier identifier(StructureKey key) {
|
||||
return Identifier.fromNamespaceAndPath(key.namespace(), key.path());
|
||||
}
|
||||
|
||||
record Capture(StructureResourceBundle bundle, int blocks, int pieces, int pools) {
|
||||
Capture {
|
||||
Objects.requireNonNull(bundle);
|
||||
}
|
||||
}
|
||||
|
||||
static final class UnsupportedStructureTypeException extends IllegalArgumentException {
|
||||
UnsupportedStructureTypeException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
private record CompositeCapture(IrisObject object, List<StructureLoss> losses) {
|
||||
CompositeCapture {
|
||||
Objects.requireNonNull(object);
|
||||
losses = List.copyOf(losses);
|
||||
}
|
||||
}
|
||||
}
|
||||
+426
@@ -0,0 +1,426 @@
|
||||
/*
|
||||
* 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.modded.structure;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.structure.authoring.IrisStructureBundleFactory;
|
||||
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.StructureWriteOptions;
|
||||
import art.arcane.iris.core.structure.authoring.StructureWriteResult;
|
||||
import art.arcane.iris.engine.framework.structure.StructureResourceBundleGraphCompiler;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.nbt.NbtUtils;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public final class ModdedStructureImportService {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
|
||||
private final Supplier<MinecraftServer> server;
|
||||
|
||||
public ModdedStructureImportService(Supplier<MinecraftServer> server) {
|
||||
this.server = Objects.requireNonNull(server);
|
||||
}
|
||||
|
||||
public List<StructureKey> templateKeys() throws StructureImportException {
|
||||
try {
|
||||
MinecraftServer activeServer = requireServerThread();
|
||||
return activeServer.getStructureManager().listTemplates()
|
||||
.map((Identifier identifier) -> StructureKey.parse(identifier.toString()))
|
||||
.sorted()
|
||||
.toList();
|
||||
} catch (RuntimeException failure) {
|
||||
throw report("Failed to list native structure templates", failure);
|
||||
}
|
||||
}
|
||||
|
||||
public List<StructureKey> jigsawStructureKeys() throws StructureImportException {
|
||||
try {
|
||||
MinecraftServer activeServer = requireServerThread();
|
||||
Registry<Structure> registry = activeServer.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
List<StructureKey> keys = new ArrayList<>();
|
||||
for (Identifier identifier : registry.keySet()) {
|
||||
if (registry.getValue(identifier) instanceof JigsawStructure) {
|
||||
keys.add(StructureKey.parse(identifier.toString()));
|
||||
}
|
||||
}
|
||||
keys.sort(Comparator.naturalOrder());
|
||||
return List.copyOf(keys);
|
||||
} catch (RuntimeException failure) {
|
||||
throw report("Failed to list registered jigsaw structures", failure);
|
||||
}
|
||||
}
|
||||
|
||||
public PreparedImport prepareTemplate(TemplateImportOptions options) throws StructureImportException {
|
||||
Objects.requireNonNull(options);
|
||||
try {
|
||||
MinecraftServer activeServer = requireServerThread();
|
||||
Identifier sourceIdentifier = identifier(options.sourceKey());
|
||||
StructureTemplate template = activeServer.getStructureManager().get(sourceIdentifier)
|
||||
.orElseThrow(() -> new IllegalArgumentException("No structure template exists for " + options.sourceKey()));
|
||||
ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.capture(
|
||||
options.sourceKey(),
|
||||
template,
|
||||
activeServer.registryAccess().lookupOrThrow(Registries.BLOCK),
|
||||
false
|
||||
);
|
||||
StructureSource source = StructureSource.identified(
|
||||
options.sourceKind(),
|
||||
options.sourceKey(),
|
||||
SharedConstants.getCurrentVersion().name(),
|
||||
NbtUtils.structureToSnbt(capture.sourceTag()).getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
IrisStructureBundleFactory.SinglePieceOptions bundleOptions = new IrisStructureBundleFactory.SinglePieceOptions(
|
||||
options.targetKey(),
|
||||
source,
|
||||
options.targetKey().path(),
|
||||
capture.object(),
|
||||
Math.max(capture.width(), capture.depth()),
|
||||
options.placeMode(),
|
||||
options.objectOnly(),
|
||||
capture.capabilities(),
|
||||
capture.losses()
|
||||
);
|
||||
StructureResourceBundle bundle = IrisStructureBundleFactory.singlePiece(bundleOptions);
|
||||
if (!options.objectOnly()) {
|
||||
StructureResourceBundleGraphCompiler.requireViable(bundle);
|
||||
}
|
||||
return new PreparedImport(
|
||||
options.packRoot(),
|
||||
options.writeOptions(),
|
||||
ImportKind.TEMPLATE,
|
||||
bundle,
|
||||
capture.blocks(),
|
||||
1,
|
||||
options.objectOnly() ? 0 : 1
|
||||
);
|
||||
} catch (IOException | RuntimeException failure) {
|
||||
throw report("Failed to prepare structure template import " + options.sourceKey(), failure);
|
||||
}
|
||||
}
|
||||
|
||||
public PreparedImport prepareJigsawStructure(JigsawImportOptions options) throws StructureImportException {
|
||||
Objects.requireNonNull(options);
|
||||
try {
|
||||
MinecraftServer activeServer = requireServerThread();
|
||||
ModdedJigsawStructureCapture.Capture capture = ModdedJigsawStructureCapture.capture(
|
||||
activeServer,
|
||||
options.sourceKey(),
|
||||
options.targetKey(),
|
||||
options.sourceKind()
|
||||
);
|
||||
StructureResourceBundleGraphCompiler.requireViable(capture.bundle());
|
||||
return new PreparedImport(
|
||||
options.packRoot(),
|
||||
options.writeOptions(),
|
||||
ImportKind.JIGSAW_STRUCTURE,
|
||||
capture.bundle(),
|
||||
capture.blocks(),
|
||||
capture.pieces(),
|
||||
capture.pools()
|
||||
);
|
||||
} catch (ModdedJigsawStructureCapture.UnsupportedStructureTypeException failure) {
|
||||
StructureLoss loss = StructureLoss.error(
|
||||
StructureCapability.IRIS_PLACEMENT,
|
||||
"unsupported_native_structure_type",
|
||||
failure.getMessage()
|
||||
);
|
||||
throw report("Cannot import native structure graph " + options.sourceKey(), failure, List.of(loss));
|
||||
} catch (IOException | RuntimeException failure) {
|
||||
throw report("Failed to prepare jigsaw structure import " + options.sourceKey(), failure);
|
||||
}
|
||||
}
|
||||
|
||||
public ImportResult write(PreparedImport prepared) {
|
||||
Objects.requireNonNull(prepared);
|
||||
StructureWriteResult writeResult = new StructureTransactionWriter(prepared.packRoot())
|
||||
.write(prepared.bundle(), prepared.writeOptions());
|
||||
writeResult.failure().ifPresent((Throwable failure) -> LOGGER.error(
|
||||
"Failed to commit modded structure import {} to {}",
|
||||
prepared.bundle().key(),
|
||||
prepared.packRoot(),
|
||||
failure
|
||||
));
|
||||
if (writeResult.committed()) {
|
||||
IrisData.getLoaded(prepared.packRoot().toFile()).ifPresent(IrisData::invalidateStructureResources);
|
||||
}
|
||||
String message = writeMessage(prepared, writeResult);
|
||||
return new ImportResult(
|
||||
writeResult.successful(),
|
||||
message,
|
||||
prepared.kind(),
|
||||
prepared.bundle().key(),
|
||||
prepared.blocks(),
|
||||
prepared.pieces(),
|
||||
prepared.pools(),
|
||||
prepared.bundle().capabilities(),
|
||||
prepared.bundle().losses(),
|
||||
Optional.of(writeResult)
|
||||
);
|
||||
}
|
||||
|
||||
public ImportResult importTemplate(TemplateImportOptions options) {
|
||||
try {
|
||||
return write(prepareTemplate(options));
|
||||
} catch (StructureImportException failure) {
|
||||
return failed(options.targetKey(), ImportKind.TEMPLATE, failure.getMessage(), failure.losses());
|
||||
}
|
||||
}
|
||||
|
||||
public ImportResult importJigsawStructure(JigsawImportOptions options) {
|
||||
try {
|
||||
return write(prepareJigsawStructure(options));
|
||||
} catch (StructureImportException failure) {
|
||||
return failed(options.targetKey(), ImportKind.JIGSAW_STRUCTURE, failure.getMessage(), failure.losses());
|
||||
}
|
||||
}
|
||||
|
||||
private MinecraftServer requireServerThread() {
|
||||
MinecraftServer activeServer = server.get();
|
||||
if (activeServer == null) {
|
||||
throw new IllegalStateException("Minecraft server is not available");
|
||||
}
|
||||
if (!activeServer.isSameThread()) {
|
||||
throw new IllegalStateException("Structure registry capture must run on the logical server thread");
|
||||
}
|
||||
return activeServer;
|
||||
}
|
||||
|
||||
private StructureImportException report(String context, Exception failure) {
|
||||
return report(context, failure, List.of());
|
||||
}
|
||||
|
||||
private StructureImportException report(String context, Exception failure, List<StructureLoss> losses) {
|
||||
LOGGER.error(context, failure);
|
||||
return new StructureImportException(context + ": " + failureDetail(failure), failure, losses);
|
||||
}
|
||||
|
||||
private static ImportResult failed(
|
||||
StructureKey targetKey,
|
||||
ImportKind kind,
|
||||
String message,
|
||||
List<StructureLoss> losses
|
||||
) {
|
||||
return new ImportResult(
|
||||
false,
|
||||
message,
|
||||
kind,
|
||||
targetKey,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
Set.of(),
|
||||
losses,
|
||||
Optional.empty()
|
||||
);
|
||||
}
|
||||
|
||||
private static String writeMessage(PreparedImport prepared, StructureWriteResult result) {
|
||||
if (result.successful()) {
|
||||
return switch (result.status()) {
|
||||
case DRY_RUN -> "Validated import of '" + prepared.bundle().key() + "' without writing files";
|
||||
case ADDED -> "Imported '" + prepared.bundle().key() + "'";
|
||||
case OVERWRITTEN -> "Overwrote owned import '" + prepared.bundle().key() + "'";
|
||||
case UNCHANGED -> "Import '" + prepared.bundle().key() + "' is already current";
|
||||
case COMMITTED_CLEANUP_REQUIRED -> "Imported '" + prepared.bundle().key()
|
||||
+ "'; obsolete staging cleanup is still required";
|
||||
default -> "Imported '" + prepared.bundle().key() + "'";
|
||||
};
|
||||
}
|
||||
if (!result.conflicts().isEmpty()) {
|
||||
StructureWriteResult.Conflict conflict = result.conflicts().getFirst();
|
||||
return "Import conflict for '" + prepared.bundle().key() + "': " + conflict.relativePath()
|
||||
+ " is " + conflict.reason().name().toLowerCase() + "; existing files were preserved";
|
||||
}
|
||||
return "Import failed for '" + prepared.bundle().key() + "': "
|
||||
+ result.failure().map(ModdedStructureImportService::failureDetail).orElse(result.status().name());
|
||||
}
|
||||
|
||||
private static String failureDetail(Throwable failure) {
|
||||
String message = failure.getMessage();
|
||||
return message == null || message.isBlank() ? failure.getClass().getSimpleName() : message;
|
||||
}
|
||||
|
||||
private static Identifier identifier(StructureKey key) {
|
||||
return Identifier.fromNamespaceAndPath(key.namespace(), key.path());
|
||||
}
|
||||
|
||||
public enum ImportKind {
|
||||
TEMPLATE,
|
||||
JIGSAW_STRUCTURE
|
||||
}
|
||||
|
||||
public record TemplateImportOptions(
|
||||
Path packRoot,
|
||||
StructureKey sourceKey,
|
||||
StructureKey targetKey,
|
||||
StructureSource.Kind sourceKind,
|
||||
StructureWriteOptions writeOptions,
|
||||
boolean objectOnly,
|
||||
String placeMode
|
||||
) {
|
||||
public TemplateImportOptions {
|
||||
packRoot = normalizedRoot(packRoot);
|
||||
Objects.requireNonNull(sourceKey);
|
||||
Objects.requireNonNull(targetKey);
|
||||
Objects.requireNonNull(sourceKind);
|
||||
Objects.requireNonNull(writeOptions);
|
||||
Objects.requireNonNull(placeMode);
|
||||
if (placeMode.isBlank()) {
|
||||
throw new IllegalArgumentException("Structure place mode cannot be blank");
|
||||
}
|
||||
}
|
||||
|
||||
public static TemplateImportOptions create(
|
||||
Path packRoot,
|
||||
StructureKey sourceKey,
|
||||
StructureKey targetKey,
|
||||
StructureWriteMode mode
|
||||
) {
|
||||
return new TemplateImportOptions(
|
||||
packRoot,
|
||||
sourceKey,
|
||||
targetKey,
|
||||
inferredSourceKind(sourceKey),
|
||||
new StructureWriteOptions(mode, false),
|
||||
false,
|
||||
"CENTER_HEIGHT"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public record JigsawImportOptions(
|
||||
Path packRoot,
|
||||
StructureKey sourceKey,
|
||||
StructureKey targetKey,
|
||||
StructureSource.Kind sourceKind,
|
||||
StructureWriteOptions writeOptions
|
||||
) {
|
||||
public JigsawImportOptions {
|
||||
packRoot = normalizedRoot(packRoot);
|
||||
Objects.requireNonNull(sourceKey);
|
||||
Objects.requireNonNull(targetKey);
|
||||
Objects.requireNonNull(sourceKind);
|
||||
Objects.requireNonNull(writeOptions);
|
||||
}
|
||||
|
||||
public static JigsawImportOptions create(
|
||||
Path packRoot,
|
||||
StructureKey sourceKey,
|
||||
StructureKey targetKey,
|
||||
StructureWriteMode mode
|
||||
) {
|
||||
return new JigsawImportOptions(
|
||||
packRoot,
|
||||
sourceKey,
|
||||
targetKey,
|
||||
inferredSourceKind(sourceKey),
|
||||
new StructureWriteOptions(mode, false)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public record PreparedImport(
|
||||
Path packRoot,
|
||||
StructureWriteOptions writeOptions,
|
||||
ImportKind kind,
|
||||
StructureResourceBundle bundle,
|
||||
int blocks,
|
||||
int pieces,
|
||||
int pools
|
||||
) {
|
||||
public PreparedImport {
|
||||
packRoot = normalizedRoot(packRoot);
|
||||
Objects.requireNonNull(writeOptions);
|
||||
Objects.requireNonNull(kind);
|
||||
Objects.requireNonNull(bundle);
|
||||
if (blocks < 0 || pieces < 0 || pools < 0) {
|
||||
throw new IllegalArgumentException("Prepared import counts cannot be negative");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record ImportResult(
|
||||
boolean success,
|
||||
String message,
|
||||
ImportKind kind,
|
||||
StructureKey targetKey,
|
||||
int blocks,
|
||||
int pieces,
|
||||
int pools,
|
||||
Set<StructureCapability> capabilities,
|
||||
List<StructureLoss> losses,
|
||||
Optional<StructureWriteResult> writeResult
|
||||
) {
|
||||
public ImportResult {
|
||||
Objects.requireNonNull(message);
|
||||
Objects.requireNonNull(kind);
|
||||
Objects.requireNonNull(targetKey);
|
||||
capabilities = Set.copyOf(capabilities);
|
||||
losses = List.copyOf(losses);
|
||||
Objects.requireNonNull(writeResult);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class StructureImportException extends Exception {
|
||||
private final List<StructureLoss> losses;
|
||||
|
||||
public StructureImportException(String message, Throwable cause, List<StructureLoss> losses) {
|
||||
super(message, cause);
|
||||
this.losses = List.copyOf(losses);
|
||||
}
|
||||
|
||||
public List<StructureLoss> losses() {
|
||||
return losses;
|
||||
}
|
||||
}
|
||||
|
||||
private static Path normalizedRoot(Path path) {
|
||||
return Objects.requireNonNull(path).toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
private static StructureSource.Kind inferredSourceKind(StructureKey sourceKey) {
|
||||
return sourceKey.namespace().equals("minecraft")
|
||||
? StructureSource.Kind.VANILLA : StructureSource.Kind.DATAPACK;
|
||||
}
|
||||
}
|
||||
+339
@@ -0,0 +1,339 @@
|
||||
/*
|
||||
* 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.modded.structure;
|
||||
|
||||
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.engine.object.IrisObject;
|
||||
import art.arcane.iris.modded.ModdedBlockResolution;
|
||||
import art.arcane.iris.modded.ModdedBlockState;
|
||||
import art.arcane.iris.modded.ModdedTileData;
|
||||
import net.minecraft.core.HolderGetter;
|
||||
import net.minecraft.core.Vec3i;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.nbt.NbtUtils;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
final class ModdedStructureTemplateCapture {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
|
||||
private ModdedStructureTemplateCapture() {
|
||||
}
|
||||
|
||||
static Capture capture(
|
||||
StructureKey sourceKey,
|
||||
StructureTemplate template,
|
||||
HolderGetter<Block> blockLookup,
|
||||
boolean connectorsPreserved
|
||||
) {
|
||||
return capture(sourceKey, template, blockLookup, connectorsPreserved, true);
|
||||
}
|
||||
|
||||
static Capture capture(
|
||||
StructureKey sourceKey,
|
||||
StructureTemplate template,
|
||||
HolderGetter<Block> blockLookup,
|
||||
boolean connectorsPreserved,
|
||||
boolean includeAir
|
||||
) {
|
||||
Objects.requireNonNull(template);
|
||||
CompoundTag sourceTag = template.save(new CompoundTag());
|
||||
return captureTag(sourceKey, sourceTag, blockLookup, connectorsPreserved, includeAir);
|
||||
}
|
||||
|
||||
static Capture captureTag(
|
||||
StructureKey sourceKey,
|
||||
CompoundTag sourceTag,
|
||||
HolderGetter<Block> blockLookup,
|
||||
boolean connectorsPreserved
|
||||
) {
|
||||
return captureTag(sourceKey, sourceTag, blockLookup, connectorsPreserved, true);
|
||||
}
|
||||
|
||||
static Capture captureTag(
|
||||
StructureKey sourceKey,
|
||||
CompoundTag sourceTag,
|
||||
HolderGetter<Block> blockLookup,
|
||||
boolean connectorsPreserved,
|
||||
boolean includeAir
|
||||
) {
|
||||
Objects.requireNonNull(sourceKey);
|
||||
Objects.requireNonNull(sourceTag);
|
||||
Objects.requireNonNull(blockLookup);
|
||||
Vec3i size = readSize(sourceTag);
|
||||
IrisObject object = new IrisObject(size.getX(), size.getY(), size.getZ());
|
||||
List<StructureLoss> losses = new ArrayList<>();
|
||||
ListTag palette = firstPalette(sourceTag, losses);
|
||||
ListTag blocks = sourceTag.getListOrEmpty(StructureTemplate.BLOCKS_TAG);
|
||||
CaptureCounts counts = captureBlocks(sourceKey, palette, blocks, blockLookup, object, losses, includeAir);
|
||||
recordEntityLoss(sourceTag, losses);
|
||||
recordMarkerLosses(counts, connectorsPreserved, losses);
|
||||
|
||||
EnumSet<StructureCapability> capabilities = EnumSet.of(StructureCapability.BLOCKS);
|
||||
if (counts.tiles() > 0) {
|
||||
capabilities.add(StructureCapability.BLOCK_ENTITIES);
|
||||
}
|
||||
if (connectorsPreserved && counts.jigsaws() > 0) {
|
||||
capabilities.add(StructureCapability.CONNECTORS);
|
||||
}
|
||||
return new Capture(
|
||||
object,
|
||||
counts.blocks(),
|
||||
counts.tiles(),
|
||||
counts.jigsaws(),
|
||||
counts.dataMarkers(),
|
||||
size.getX(),
|
||||
size.getY(),
|
||||
size.getZ(),
|
||||
capabilities,
|
||||
losses,
|
||||
sourceTag.copy()
|
||||
);
|
||||
}
|
||||
|
||||
private static Vec3i readSize(CompoundTag sourceTag) {
|
||||
ListTag size = sourceTag.getListOrEmpty(StructureTemplate.SIZE_TAG);
|
||||
int width = size.getIntOr(0, 0);
|
||||
int height = size.getIntOr(1, 0);
|
||||
int depth = size.getIntOr(2, 0);
|
||||
if (width < 1 || height < 1 || depth < 1) {
|
||||
throw new IllegalArgumentException("Structure template has invalid dimensions "
|
||||
+ width + "x" + height + "x" + depth);
|
||||
}
|
||||
return new Vec3i(width, height, depth);
|
||||
}
|
||||
|
||||
private static ListTag firstPalette(CompoundTag sourceTag, List<StructureLoss> losses) {
|
||||
ListTag palettes = sourceTag.getList(StructureTemplate.PALETTE_LIST_TAG).orElse(null);
|
||||
if (palettes != null) {
|
||||
if (palettes.isEmpty()) {
|
||||
throw new IllegalArgumentException("Structure template has no palettes");
|
||||
}
|
||||
if (palettes.size() > 1) {
|
||||
losses.add(StructureLoss.warning(
|
||||
StructureCapability.BLOCKS,
|
||||
"palette_variants_not_imported",
|
||||
"Only palette 0 was converted; " + (palettes.size() - 1)
|
||||
+ " additional palette(s) remain native-only."
|
||||
));
|
||||
}
|
||||
return palettes.getListOrEmpty(0);
|
||||
}
|
||||
ListTag palette = sourceTag.getListOrEmpty(StructureTemplate.PALETTE_TAG);
|
||||
if (palette.isEmpty() && !sourceTag.getListOrEmpty(StructureTemplate.BLOCKS_TAG).isEmpty()) {
|
||||
throw new IllegalArgumentException("Structure template has blocks but no palette");
|
||||
}
|
||||
return palette;
|
||||
}
|
||||
|
||||
private static CaptureCounts captureBlocks(
|
||||
StructureKey sourceKey,
|
||||
ListTag palette,
|
||||
ListTag blocks,
|
||||
HolderGetter<Block> blockLookup,
|
||||
IrisObject object,
|
||||
List<StructureLoss> losses,
|
||||
boolean includeAir
|
||||
) {
|
||||
int blockCount = 0;
|
||||
int tiles = 0;
|
||||
int jigsaws = 0;
|
||||
int dataMarkers = 0;
|
||||
for (int index = 0; index < blocks.size(); index++) {
|
||||
CompoundTag blockTag = blocks.getCompoundOrEmpty(index);
|
||||
BlockPosition position = readPosition(blockTag);
|
||||
if (!withinObject(position, object)) {
|
||||
losses.add(StructureLoss.warning(
|
||||
StructureCapability.BLOCKS,
|
||||
"out_of_bounds_block_not_imported",
|
||||
"Block " + index + " at " + position.x() + "," + position.y() + "," + position.z()
|
||||
+ " is outside the declared template bounds."
|
||||
));
|
||||
continue;
|
||||
}
|
||||
int paletteIndex = blockTag.getIntOr(StructureTemplate.BLOCK_TAG_STATE, 0);
|
||||
BlockState state = NbtUtils.readBlockState(blockLookup, palette.getCompoundOrEmpty(paletteIndex));
|
||||
CompoundTag blockEntityTag = blockTag.getCompound(StructureTemplate.BLOCK_TAG_NBT).orElse(null);
|
||||
if (state.is(Blocks.STRUCTURE_VOID)) {
|
||||
continue;
|
||||
}
|
||||
if (state.is(Blocks.STRUCTURE_BLOCK)) {
|
||||
dataMarkers++;
|
||||
continue;
|
||||
}
|
||||
if (state.is(Blocks.JIGSAW)) {
|
||||
jigsaws++;
|
||||
BlockState finalState = resolveJigsawFinalState(sourceKey, position, blockEntityTag, losses);
|
||||
if (finalState == null || finalState.isAir()) {
|
||||
continue;
|
||||
}
|
||||
state = finalState;
|
||||
blockEntityTag = null;
|
||||
}
|
||||
if (!includeAir && state.isAir()) {
|
||||
continue;
|
||||
}
|
||||
object.setUnsigned(position.x(), position.y(), position.z(), ModdedBlockState.of(state, null));
|
||||
blockCount++;
|
||||
if (blockEntityTag != null && captureTile(sourceKey, position, state, blockEntityTag, object, losses)) {
|
||||
tiles++;
|
||||
}
|
||||
}
|
||||
return new CaptureCounts(blockCount, tiles, jigsaws, dataMarkers);
|
||||
}
|
||||
|
||||
private static BlockPosition readPosition(CompoundTag blockTag) {
|
||||
ListTag position = blockTag.getListOrEmpty(StructureTemplate.BLOCK_TAG_POS);
|
||||
return new BlockPosition(
|
||||
position.getIntOr(0, Integer.MIN_VALUE),
|
||||
position.getIntOr(1, Integer.MIN_VALUE),
|
||||
position.getIntOr(2, Integer.MIN_VALUE)
|
||||
);
|
||||
}
|
||||
|
||||
private static boolean withinObject(BlockPosition position, IrisObject object) {
|
||||
return position.x() >= 0 && position.x() < object.getW()
|
||||
&& position.y() >= 0 && position.y() < object.getH()
|
||||
&& position.z() >= 0 && position.z() < object.getD();
|
||||
}
|
||||
|
||||
private static BlockState resolveJigsawFinalState(
|
||||
StructureKey sourceKey,
|
||||
BlockPosition position,
|
||||
CompoundTag blockEntityTag,
|
||||
List<StructureLoss> losses
|
||||
) {
|
||||
String finalState = blockEntityTag == null
|
||||
? "minecraft:air"
|
||||
: blockEntityTag.getStringOr("final_state", "minecraft:air");
|
||||
try {
|
||||
return ModdedBlockResolution.strictParse(finalState).handle();
|
||||
} catch (IllegalArgumentException failure) {
|
||||
LOGGER.error("Failed to parse jigsaw final state '{}' in {} at {},{},{}",
|
||||
finalState, sourceKey, position.x(), position.y(), position.z(), failure);
|
||||
losses.add(StructureLoss.warning(
|
||||
StructureCapability.BLOCKS,
|
||||
"jigsaw_final_state_not_imported",
|
||||
"Jigsaw final state '" + finalState + "' at " + position.x() + "," + position.y() + ","
|
||||
+ position.z() + " could not be parsed."
|
||||
));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean captureTile(
|
||||
StructureKey sourceKey,
|
||||
BlockPosition position,
|
||||
BlockState state,
|
||||
CompoundTag blockEntityTag,
|
||||
IrisObject object,
|
||||
List<StructureLoss> losses
|
||||
) {
|
||||
try {
|
||||
String blockKey = ModdedBlockState.serialize(state);
|
||||
ModdedTileData tile = ModdedTileData.capture(blockKey, NbtUtils.structureToSnbt(blockEntityTag));
|
||||
object.setUnsignedTile(position.x(), position.y(), position.z(), tile);
|
||||
return true;
|
||||
} catch (IOException | RuntimeException failure) {
|
||||
LOGGER.error("Failed to capture block entity in {} at {},{},{}",
|
||||
sourceKey, position.x(), position.y(), position.z(), failure);
|
||||
losses.add(StructureLoss.warning(
|
||||
StructureCapability.BLOCK_ENTITIES,
|
||||
"block_entity_not_imported",
|
||||
"Block entity data at " + position.x() + "," + position.y() + "," + position.z()
|
||||
+ " could not be encoded."
|
||||
));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void recordEntityLoss(CompoundTag sourceTag, List<StructureLoss> losses) {
|
||||
int entityCount = sourceTag.getListOrEmpty(StructureTemplate.ENTITIES_TAG).size();
|
||||
if (entityCount > 0) {
|
||||
losses.add(StructureLoss.warning(
|
||||
StructureCapability.ENTITIES,
|
||||
"entities_not_imported",
|
||||
entityCount + " structure entit" + (entityCount == 1 ? "y was" : "ies were")
|
||||
+ " not converted into the Iris snapshot."
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private static void recordMarkerLosses(
|
||||
CaptureCounts counts,
|
||||
boolean connectorsPreserved,
|
||||
List<StructureLoss> losses
|
||||
) {
|
||||
if (counts.dataMarkers() > 0) {
|
||||
losses.add(StructureLoss.warning(
|
||||
StructureCapability.PROCESSORS,
|
||||
"data_markers_not_imported",
|
||||
counts.dataMarkers() + " structure data marker(s) require native pool-element handlers and were omitted."
|
||||
));
|
||||
}
|
||||
if (!connectorsPreserved && counts.jigsaws() > 0) {
|
||||
losses.add(StructureLoss.warning(
|
||||
StructureCapability.CONNECTORS,
|
||||
"connectors_not_imported",
|
||||
counts.jigsaws() + " jigsaw connector(s) were resolved to final blocks without importing their pool graph."
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
record Capture(
|
||||
IrisObject object,
|
||||
int blocks,
|
||||
int tiles,
|
||||
int jigsaws,
|
||||
int dataMarkers,
|
||||
int width,
|
||||
int height,
|
||||
int depth,
|
||||
Set<StructureCapability> capabilities,
|
||||
List<StructureLoss> losses,
|
||||
CompoundTag sourceTag
|
||||
) {
|
||||
Capture {
|
||||
Objects.requireNonNull(object);
|
||||
capabilities = Set.copyOf(capabilities);
|
||||
losses = List.copyOf(losses);
|
||||
sourceTag = sourceTag.copy();
|
||||
}
|
||||
}
|
||||
|
||||
private record CaptureCounts(int blocks, int tiles, int jigsaws, int dataMarkers) {
|
||||
}
|
||||
|
||||
private record BlockPosition(int x, int y, int z) {
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.nativegen.NativeStructureLocateResults;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertSame;
|
||||
|
||||
public class IrisModdedLocateRoutingTest {
|
||||
@Test
|
||||
public void mixedLocateSelectsNearestProviderAndPrefersNativeOnTie() {
|
||||
BlockPos origin = BlockPos.ZERO;
|
||||
Pair<BlockPos, Holder<Structure>> irisNear = Pair.of(new BlockPos(4, 70, 0), null);
|
||||
Pair<BlockPos, Holder<Structure>> nativeFar = Pair.of(new BlockPos(8, 70, 0), null);
|
||||
Pair<BlockPos, Holder<Structure>> nativeNear = Pair.of(new BlockPos(2, 70, 0), null);
|
||||
Pair<BlockPos, Holder<Structure>> nativeTie = Pair.of(new BlockPos(0, 70, 4), null);
|
||||
|
||||
assertSame(irisNear, NativeStructureLocateResults.nearest(origin, irisNear, nativeFar));
|
||||
assertSame(nativeNear, NativeStructureLocateResults.nearest(origin, irisNear, nativeNear));
|
||||
assertSame(nativeTie, NativeStructureLocateResults.nearest(origin, irisNear, nativeTie));
|
||||
}
|
||||
}
|
||||
+33
-7
@@ -23,6 +23,7 @@ import java.util.concurrent.TimeUnit;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisModdedStructureParityTest {
|
||||
@@ -120,14 +121,18 @@ public class IrisModdedStructureParityTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void possibleBiomeFallbackIsOnlyRequiredForMissingOrEmptyConfigurations() {
|
||||
Set<String> registered = Set.of("minecraft:deep_ocean", "minecraft:dark_forest", "minecraft:plains");
|
||||
public void structureBiomeContractRejectsAnEmptyConfiguredSet() {
|
||||
IllegalStateException error = assertThrows(IllegalStateException.class,
|
||||
() -> IrisModdedBiomeSource.requireConfiguredStructureBiomeKeys(Set.of()));
|
||||
|
||||
assertFalse(IrisModdedBiomeSource.requiresPossibleBiomeFallback(
|
||||
Set.of("minecraft:deep_ocean", "minecraft:dark_forest"), registered));
|
||||
assertTrue(IrisModdedBiomeSource.requiresPossibleBiomeFallback(
|
||||
Set.of("minecraft:deep_ocean", "overworld:missing"), registered));
|
||||
assertTrue(IrisModdedBiomeSource.requiresPossibleBiomeFallback(Set.of(), registered));
|
||||
assertEquals("Iris has no configured structure biomes", error.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structureBiomeContractPreservesConfiguredKeysDuringBootstrap() {
|
||||
Set<String> configured = Set.of("minecraft:deep_ocean", "minecraft:dark_forest");
|
||||
|
||||
assertSame(configured, IrisModdedBiomeSource.requireConfiguredStructureBiomeKeys(configured));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -185,6 +190,27 @@ public class IrisModdedStructureParityTest {
|
||||
throw new AssertionError("Expected failed engine binding to propagate");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structureBiomeBootstrapAllowsOnlyPendingBindingsToUseMetadata() {
|
||||
IrisModdedChunkGenerator.EngineBinding<String> binding =
|
||||
new IrisModdedChunkGenerator.EngineBinding<>(1L, TimeUnit.SECONDS);
|
||||
|
||||
binding.throwIfFailed("overworld:overworld");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structureBiomeBootstrapPropagatesBindingFailure() {
|
||||
IrisModdedChunkGenerator.EngineBinding<String> binding =
|
||||
new IrisModdedChunkGenerator.EngineBinding<>(1L, TimeUnit.SECONDS);
|
||||
IllegalArgumentException failure = new IllegalArgumentException("broken pack");
|
||||
binding.fail(failure);
|
||||
|
||||
IllegalStateException error = assertThrows(IllegalStateException.class,
|
||||
() -> binding.throwIfFailed("overworld:overworld"));
|
||||
|
||||
assertSame(failure, error.getCause());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initialEntitySpawnsUseThePaperCompletionMarker() {
|
||||
assertSame(MantleFlag.INITIAL_SPAWNED_MARKER, ModdedWorldManager.INITIAL_SPAWN_COMPLETION_FLAG);
|
||||
|
||||
+66
@@ -35,6 +35,7 @@ import java.util.stream.Stream;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ModdedForcedDatapackTest {
|
||||
@@ -75,6 +76,71 @@ public class ModdedForcedDatapackTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void publishesCompleteStagingDirectoryOverExistingPack() throws IOException {
|
||||
Path root = Files.createTempDirectory("iris-forced-pack-publish");
|
||||
try {
|
||||
Path published = Files.createDirectory(root.resolve("iris"));
|
||||
Files.writeString(published.resolve("old.txt"), "old", StandardCharsets.UTF_8);
|
||||
Path staging = Files.createDirectory(root.resolve("staging"));
|
||||
Files.writeString(staging.resolve("new.txt"), "new", StandardCharsets.UTF_8);
|
||||
|
||||
ModdedForcedDatapack.publishDirectory(staging, published);
|
||||
|
||||
assertFalse(Files.exists(staging));
|
||||
assertFalse(Files.exists(published.resolve("old.txt")));
|
||||
assertEquals("new", Files.readString(published.resolve("new.txt"), StandardCharsets.UTF_8));
|
||||
assertEquals(List.of("iris"), directoryEntries(root));
|
||||
} finally {
|
||||
deleteTree(root);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void publishesStagingDirectoryWhenNoPriorPackExists() throws IOException {
|
||||
Path root = Files.createTempDirectory("iris-forced-pack-first-publish");
|
||||
try {
|
||||
Path published = root.resolve("iris");
|
||||
Path staging = Files.createDirectory(root.resolve("staging"));
|
||||
Files.writeString(staging.resolve("pack.mcmeta"), "first", StandardCharsets.UTF_8);
|
||||
|
||||
ModdedForcedDatapack.publishDirectory(staging, published);
|
||||
|
||||
assertEquals("first",
|
||||
Files.readString(published.resolve("pack.mcmeta"), StandardCharsets.UTF_8));
|
||||
assertEquals(List.of("iris"), directoryEntries(root));
|
||||
} finally {
|
||||
deleteTree(root);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void restoresPublishedPackWhenAtomicStagingMoveFails() throws IOException {
|
||||
Path root = Files.createTempDirectory("iris-forced-pack-rollback");
|
||||
try {
|
||||
Path published = Files.createDirectory(root.resolve("iris"));
|
||||
Files.writeString(published.resolve("pack.mcmeta"), "known-good", StandardCharsets.UTF_8);
|
||||
Path missingStaging = root.resolve("missing-staging");
|
||||
|
||||
assertThrows(IOException.class,
|
||||
() -> ModdedForcedDatapack.publishDirectory(missingStaging, published));
|
||||
|
||||
assertEquals("known-good",
|
||||
Files.readString(published.resolve("pack.mcmeta"), StandardCharsets.UTF_8));
|
||||
assertEquals(List.of("iris"), directoryEntries(root));
|
||||
} finally {
|
||||
deleteTree(root);
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> directoryEntries(Path root) throws IOException {
|
||||
try (Stream<Path> entries = Files.list(root)) {
|
||||
return entries.map((Path path) -> path.getFileName().toString())
|
||||
.sorted()
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteTree(Path root) throws IOException {
|
||||
List<Path> paths = new ArrayList<>();
|
||||
try (Stream<Path> walk = Files.walk(root)) {
|
||||
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ModdedLifecycleFailureContractTest {
|
||||
private static final String SOURCE_ROOT_PROPERTY = "iris.moddedCommonSources";
|
||||
|
||||
@Test
|
||||
public void existingDimensionRepointBindsBeforeHandlePublication() throws IOException {
|
||||
String managerSource = source("ModdedDimensionManager.java");
|
||||
String create = method(managerSource, "public static Handle create(");
|
||||
int existingStart = requiredIndex(create, "if (existing != null");
|
||||
int presentStart = requiredIndex(create, "if (serverAccess.hasLevel(server, key))", existingStart + 1);
|
||||
int injectionStart = requiredIndex(create, "try {", presentStart);
|
||||
|
||||
assertRepointBeforePublication(create.substring(existingStart, presentStart));
|
||||
assertRepointBeforePublication(create.substring(presentStart, injectionStart));
|
||||
|
||||
String generatorSource = source("IrisModdedChunkGenerator.java");
|
||||
String repointAndBind = method(generatorSource, "void repointAndBind(");
|
||||
assertBefore(repointAndBind, "ModdedWorldEngines.prepareReplacement(",
|
||||
"ModdedWorldEngines.installReplacement(");
|
||||
assertBefore(repointAndBind, "ModdedWorldEngines.installReplacement(", "this.activePack = pack;");
|
||||
String failure = catchBlock(repointAndBind);
|
||||
assertTrue(failure.contains("ModdedWorldEngines.closeUnregistered(replacement);"));
|
||||
assertTrue(failure.contains("addSuppressed("));
|
||||
|
||||
String worldEnginesSource = source("ModdedWorldEngines.java");
|
||||
String installReplacement = method(worldEnginesSource, "static void installReplacement(");
|
||||
assertTrue(installReplacement.contains("ENGINES.compute("));
|
||||
assertBefore(installReplacement, "close(current);", "return activeReplacement;");
|
||||
assertFalse(installReplacement.contains("catch ("));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void injectionBindsBeforeRegistrationAndRollsBackEveryFailure() throws IOException {
|
||||
String source = source("ModdedDimensionManager.java");
|
||||
String injection = method(source, "private static Handle inject(");
|
||||
|
||||
assertBefore(injection, "generator.bindLevel(level);", "serverAccess.putLevelIfAbsent(server, key, level);");
|
||||
assertBefore(injection, "serverAccess.putLevelIfAbsent(server, key, level);",
|
||||
"server.getPlayerList().addWorldborderListener(level);");
|
||||
assertFalse(injection.contains("serverAccess.putLevel(server, key, previous);"));
|
||||
String failure = catchBlock(injection);
|
||||
assertBefore(failure, "rollbackInjection(", "throw ");
|
||||
|
||||
String rollback = method(source, "void rollbackInjection(");
|
||||
assertBefore(rollback, "serverAccess.hasLevel(server, key)", "serverAccess.removeLevel(server, key);");
|
||||
assertTrue(rollback.contains("generator.unbindEngine("));
|
||||
assertTrue(rollback.contains("level.close();"));
|
||||
assertTrue(rollback.contains("addSuppressed("));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void persistentReinjectionRethrowsTheOriginalCause() throws IOException {
|
||||
String source = source("ModdedStartup.java");
|
||||
String reinjection = method(source, "private static void reinjectPersistentDimensions(");
|
||||
String failure = catchBlock(reinjection);
|
||||
|
||||
assertTrue(failure.contains("LOGGER.error("));
|
||||
assertFalse(failure.contains("e.toString()"));
|
||||
assertFalse(failure.contains("continue;"));
|
||||
assertTrue(failure.contains("throw new IllegalStateException("));
|
||||
assertTrue(failure.contains(", e);"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void packDimensionLoadingNeverSuppressesTheFailureAsNull() throws IOException {
|
||||
String source = source("ModdedDimensionManager.java");
|
||||
String loading = method(source, "private static IrisDimension loadPackDimension(");
|
||||
|
||||
assertFalse(loading.contains("return null;"));
|
||||
assertFalse(loading.contains("e.toString()"));
|
||||
int catchIndex = loading.indexOf("catch (");
|
||||
if (catchIndex >= 0) {
|
||||
String failure = blockAt(loading, requiredIndex(loading, "{", catchIndex));
|
||||
assertTrue(failure.contains("throw "));
|
||||
assertTrue(failure.contains(", e);") || failure.contains("throw e;"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void productionPackResolutionNeverUsesTheParityProbeAsData() throws IOException {
|
||||
String source = source("ModdedWorldEngines.java");
|
||||
String resolvePack = method(source, "static File resolvePack(");
|
||||
|
||||
assertFalse(source.contains("System.getProperty(\"iris.parity\")"));
|
||||
assertFalse(source.contains("parityPack"));
|
||||
assertFalse(source.contains("falling back to parity pack"));
|
||||
assertTrue(resolvePack.contains("throw new IllegalStateException("));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void engineBootstrapResetsStartupExactlyOnceDuringStop() throws IOException {
|
||||
String source = source("ModdedEngineBootstrap.java");
|
||||
String stop = method(source, "public static void stop(");
|
||||
|
||||
assertEquals(1, occurrences(stop, "ModdedStartup.reset();"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void engineEvictionRetainsTheMappingUntilCloseSucceeds() throws IOException {
|
||||
String source = source("ModdedWorldEngines.java");
|
||||
String eviction = method(source, "static void evictOrThrow(");
|
||||
|
||||
assertTrue(eviction.contains("ENGINES.computeIfPresent("));
|
||||
assertFalse(eviction.contains("ENGINES.remove("));
|
||||
assertBefore(eviction, "close(current);", "return null;");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generatorUnbindRetainsOwnershipWhenEvictionFails() throws IOException {
|
||||
String source = source("IrisModdedChunkGenerator.java");
|
||||
String unbind = method(source, "synchronized void unbindEngine(ServerLevel level)");
|
||||
|
||||
assertFalse(unbind.contains("finally"));
|
||||
assertBefore(unbind, "ModdedWorldEngines.evictOrThrow(level);", "clearEngineBinding();");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void engineBootstrapRollsBackEveryPublishedBindingBeforeRethrowing() throws IOException {
|
||||
String source = source("ModdedEngineBootstrap.java");
|
||||
String bind = method(source, "public static ModdedPlatform bind(");
|
||||
String failure = catchBlock(bind);
|
||||
|
||||
assertBefore(bind, "createdServices.enableAll();", "runtime = new BoundRuntime(");
|
||||
assertTrue(bind.contains("rollback.add(() -> GuiHost.suppressDesktop("));
|
||||
assertTrue(bind.contains("rollback.add(() -> restorePlatform("));
|
||||
assertTrue(bind.contains("rollback.add(() -> ModdedDimensionManager.restoreAccess("));
|
||||
assertTrue(bind.contains("rollback.add(() -> IrisObjectRotation.restorePlatformRotator("));
|
||||
assertTrue(bind.contains("rollback.add(() -> BlockDataMergeSupport.restorePlatformMerger("));
|
||||
assertTrue(bind.contains("rollback.add(() -> TileData.restorePlatformReader("));
|
||||
assertTrue(bind.contains("rollback.add(() -> TileData.restorePlatformFactory("));
|
||||
assertTrue(bind.contains("rollback.add(() -> GuiHost.set("));
|
||||
assertTrue(bind.contains("rollback.add(() -> DecoratorPlatformHooks.restore("));
|
||||
assertTrue(bind.contains("bindService(PreservationRegistry.class"));
|
||||
assertTrue(bind.contains("bindService(EngineEffectsProvider.class"));
|
||||
assertTrue(bind.contains("bindService(EnginePlatformHooks.class"));
|
||||
assertTrue(bind.contains("bindService(EngineWorldManagerProvider.class"));
|
||||
assertTrue(bind.contains("rollback.add(customContentDiscovery::rollback);"));
|
||||
assertBefore(failure, "createdServices.rollback(failure);", "rollback.restore(failure);");
|
||||
assertBefore(failure, "rollback.restore(failure);", "throw ");
|
||||
}
|
||||
|
||||
private static void assertRepointBeforePublication(String branch) {
|
||||
assertBefore(branch, "repointAndBind(", "new Handle(");
|
||||
assertBefore(branch, "repointAndBind(", "HANDLES.put(");
|
||||
assertBefore(branch, "repointAndBind(", "return ");
|
||||
}
|
||||
|
||||
private static void assertBefore(String source, String first, String second) {
|
||||
int firstIndex = requiredIndex(source, first);
|
||||
int secondIndex = requiredIndex(source, second);
|
||||
assertTrue(first + " must occur before " + second, firstIndex < secondIndex);
|
||||
}
|
||||
|
||||
private static int requiredIndex(String source, String needle) {
|
||||
return requiredIndex(source, needle, 0);
|
||||
}
|
||||
|
||||
private static int requiredIndex(String source, String needle, int start) {
|
||||
int index = source.indexOf(needle, start);
|
||||
assertTrue("Missing source contract token: " + needle, index >= 0);
|
||||
return index;
|
||||
}
|
||||
|
||||
private static String source(String fileName) throws IOException {
|
||||
String sourceRoot = System.getProperty(SOURCE_ROOT_PROPERTY);
|
||||
assertTrue("Missing system property " + SOURCE_ROOT_PROPERTY,
|
||||
sourceRoot != null && !sourceRoot.isBlank());
|
||||
Path sourcePath = Path.of(sourceRoot, "art", "arcane", "iris", "modded", fileName);
|
||||
return Files.readString(sourcePath);
|
||||
}
|
||||
|
||||
private static String method(String source, String signature) {
|
||||
int start = requiredIndex(source, signature);
|
||||
int openBrace = requiredIndex(source, "{", start);
|
||||
return source.substring(start, matchingBrace(source, openBrace) + 1);
|
||||
}
|
||||
|
||||
private static String catchBlock(String source) {
|
||||
int catchIndex = requiredIndex(source, "catch (");
|
||||
int openBrace = requiredIndex(source, "{", catchIndex);
|
||||
return blockAt(source, openBrace);
|
||||
}
|
||||
|
||||
private static String blockAt(String source, int openBrace) {
|
||||
return source.substring(openBrace, matchingBrace(source, openBrace) + 1);
|
||||
}
|
||||
|
||||
private static int matchingBrace(String source, int openBrace) {
|
||||
int depth = 0;
|
||||
boolean quoted = false;
|
||||
boolean character = false;
|
||||
boolean escaped = false;
|
||||
for (int i = openBrace; i < source.length(); i++) {
|
||||
char current = source.charAt(i);
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if ((quoted || character) && current == '\\') {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if (!character && current == '"') {
|
||||
quoted = !quoted;
|
||||
continue;
|
||||
}
|
||||
if (!quoted && current == '\'') {
|
||||
character = !character;
|
||||
continue;
|
||||
}
|
||||
if (quoted || character) {
|
||||
continue;
|
||||
}
|
||||
if (current == '{') {
|
||||
depth++;
|
||||
} else if (current == '}') {
|
||||
depth--;
|
||||
if (depth == 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unclosed source block");
|
||||
}
|
||||
|
||||
private static int occurrences(String source, String needle) {
|
||||
int count = 0;
|
||||
int offset = 0;
|
||||
while (true) {
|
||||
int index = source.indexOf(needle, offset);
|
||||
if (index < 0) {
|
||||
return count;
|
||||
}
|
||||
count++;
|
||||
offset = index + needle.length();
|
||||
}
|
||||
}
|
||||
}
|
||||
+58
-8
@@ -1,9 +1,13 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.engine.framework.LootResolver;
|
||||
import art.arcane.iris.engine.object.IrisLootMode;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.world.RandomizableContainer;
|
||||
import net.minecraft.world.SimpleContainer;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
@@ -12,6 +16,7 @@ import org.junit.Test;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -25,7 +30,7 @@ public class ModdedLootApplierTest {
|
||||
public void addAppendsSourcesInOrder() {
|
||||
List<String> sources = new ArrayList<>(List.of("placement-native"));
|
||||
|
||||
ModdedLootApplier.injectSources(sources, List.of("dimension-iris", "region-iris"), IrisLootMode.ADD, false);
|
||||
LootResolver.injectSources(sources, List.of("dimension-iris", "region-iris"), IrisLootMode.ADD, false);
|
||||
|
||||
assertEquals(List.of("placement-native", "dimension-iris", "region-iris"), sources);
|
||||
}
|
||||
@@ -34,7 +39,7 @@ public class ModdedLootApplierTest {
|
||||
public void clearRemovesNativeAndIrisSourcesBeforeAdding() {
|
||||
List<String> sources = new ArrayList<>(List.of("placement-native", "dimension-iris"));
|
||||
|
||||
ModdedLootApplier.injectSources(sources, List.of("biome-iris"), IrisLootMode.CLEAR, false);
|
||||
LootResolver.injectSources(sources, List.of("biome-iris"), IrisLootMode.CLEAR, false);
|
||||
|
||||
assertEquals(List.of("biome-iris"), sources);
|
||||
}
|
||||
@@ -43,7 +48,7 @@ public class ModdedLootApplierTest {
|
||||
public void replaceRemovesNativeAndIrisSourcesBeforeAdding() {
|
||||
List<String> sources = new ArrayList<>(List.of("placement-native", "dimension-iris"));
|
||||
|
||||
ModdedLootApplier.injectSources(sources, List.of("biome-iris"), IrisLootMode.REPLACE, false);
|
||||
LootResolver.injectSources(sources, List.of("biome-iris"), IrisLootMode.REPLACE, false);
|
||||
|
||||
assertEquals(List.of("biome-iris"), sources);
|
||||
}
|
||||
@@ -58,7 +63,7 @@ public class ModdedLootApplierTest {
|
||||
public void fallbackDoesNotOverrideExistingNativeOrIrisSources() {
|
||||
List<String> sources = new ArrayList<>(List.of("placement-native", "dimension-iris"));
|
||||
|
||||
ModdedLootApplier.injectSources(sources, List.of("fallback-iris"), IrisLootMode.FALLBACK, false);
|
||||
LootResolver.injectSources(sources, List.of("fallback-iris"), IrisLootMode.FALLBACK, false);
|
||||
|
||||
assertEquals(List.of("placement-native", "dimension-iris"), sources);
|
||||
}
|
||||
@@ -67,7 +72,7 @@ public class ModdedLootApplierTest {
|
||||
public void fallbackAddsSourcesWhenPlacementIsEmpty() {
|
||||
List<String> sources = new ArrayList<>();
|
||||
|
||||
ModdedLootApplier.injectSources(sources, List.of("fallback-iris"), IrisLootMode.FALLBACK, true);
|
||||
LootResolver.injectSources(sources, List.of("fallback-iris"), IrisLootMode.FALLBACK, true);
|
||||
|
||||
assertEquals(List.of("fallback-iris"), sources);
|
||||
}
|
||||
@@ -76,7 +81,7 @@ public class ModdedLootApplierTest {
|
||||
public void zeroMultiplierRemovesNativeAndIrisSources() {
|
||||
List<String> sources = new ArrayList<>(List.of("placement-native", "dimension-iris"));
|
||||
|
||||
ModdedLootApplier.scaleSources(sources, 0D, new RNG(17L));
|
||||
LootResolver.scaleSources(sources, 0D, new RNG(17L));
|
||||
|
||||
assertTrue(sources.isEmpty());
|
||||
}
|
||||
@@ -85,7 +90,7 @@ public class ModdedLootApplierTest {
|
||||
public void unitMultiplierPreservesNativeAndIrisSources() {
|
||||
List<String> sources = new ArrayList<>(List.of("placement-native", "dimension-iris"));
|
||||
|
||||
ModdedLootApplier.scaleSources(sources, 1D, new RNG(17L));
|
||||
LootResolver.scaleSources(sources, 1D, new RNG(17L));
|
||||
|
||||
assertEquals(List.of("placement-native", "dimension-iris"), sources);
|
||||
}
|
||||
@@ -94,7 +99,7 @@ public class ModdedLootApplierTest {
|
||||
public void doubleMultiplierScalesTheUnifiedSourceList() {
|
||||
List<String> sources = new ArrayList<>(List.of("placement-native", "dimension-iris"));
|
||||
|
||||
ModdedLootApplier.scaleSources(sources, 2D, new RNG(17L));
|
||||
LootResolver.scaleSources(sources, 2D, new RNG(17L));
|
||||
|
||||
assertEquals(4, sources.size());
|
||||
assertEquals("placement-native", sources.get(0));
|
||||
@@ -142,6 +147,51 @@ public class ModdedLootApplierTest {
|
||||
assertTrue(container.changed);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doubleChestHasExactlyOneCanonicalHalf() {
|
||||
BlockPos leftPos = new BlockPos(0, 64, 0);
|
||||
BlockPos rightPos = new BlockPos(1, 64, 0);
|
||||
|
||||
assertTrue(ModdedLootApplier.isCanonicalPair(leftPos, rightPos));
|
||||
assertFalse(ModdedLootApplier.isCanonicalPair(rightPos, leftPos));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativeLootTableIsDetectedBeforeContainerAccess() {
|
||||
ResourceKey<LootTable> key = ResourceKey.create(
|
||||
Registries.LOOT_TABLE,
|
||||
Identifier.parse("minecraft:chests/simple_dungeon")
|
||||
);
|
||||
RandomizableContainer empty = containerWithLootTable(null);
|
||||
RandomizableContainer nativeLoot = containerWithLootTable(key);
|
||||
|
||||
assertFalse(ModdedLootApplier.hasNativeLootTable(empty));
|
||||
assertTrue(ModdedLootApplier.hasNativeLootTable(nativeLoot));
|
||||
}
|
||||
|
||||
private RandomizableContainer containerWithLootTable(ResourceKey<LootTable> key) {
|
||||
return (RandomizableContainer) Proxy.newProxyInstance(
|
||||
RandomizableContainer.class.getClassLoader(),
|
||||
new Class<?>[]{RandomizableContainer.class},
|
||||
(proxy, method, args) -> {
|
||||
if (method.getName().equals("getLootTable")) {
|
||||
return key;
|
||||
}
|
||||
Class<?> returnType = method.getReturnType();
|
||||
if (returnType == boolean.class) {
|
||||
return false;
|
||||
}
|
||||
if (returnType == int.class) {
|
||||
return 0;
|
||||
}
|
||||
if (returnType == long.class) {
|
||||
return 0L;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static final class TrackingContainer extends SimpleContainer {
|
||||
private boolean changed;
|
||||
|
||||
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.modded.service.ModdedService;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
public class ModdedServiceManagerTest {
|
||||
@Test
|
||||
public void enablesAndDisablesEveryRegisteredService() {
|
||||
ModdedServiceManager manager = new ModdedServiceManager();
|
||||
FirstService first = manager.register(FirstService.class, new FirstService());
|
||||
SecondService second = manager.register(SecondService.class, new SecondService(null, null));
|
||||
|
||||
manager.enableAll();
|
||||
manager.disableAll();
|
||||
|
||||
assertEquals(1, first.enableCount);
|
||||
assertEquals(1, first.disableCount);
|
||||
assertEquals(1, second.enableCount);
|
||||
assertEquals(1, second.disableCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rollsBackEnabledServicesAndPreservesCleanupFailures() {
|
||||
ModdedServiceManager manager = new ModdedServiceManager();
|
||||
FirstService first = manager.register(FirstService.class, new FirstService());
|
||||
RuntimeException original = new RuntimeException("enable failed");
|
||||
RuntimeException cleanup = new RuntimeException("cleanup failed");
|
||||
SecondService second = manager.register(
|
||||
SecondService.class, new SecondService(original, cleanup));
|
||||
|
||||
RuntimeException thrown = assertThrows(RuntimeException.class, manager::enableAll);
|
||||
|
||||
assertSame(original, thrown);
|
||||
assertEquals(1, first.enableCount);
|
||||
assertEquals(1, first.disableCount);
|
||||
assertEquals(1, second.enableCount);
|
||||
assertEquals(1, second.disableCount);
|
||||
assertEquals(1, thrown.getSuppressed().length);
|
||||
assertSame(cleanup, thrown.getSuppressed()[0]);
|
||||
|
||||
manager.rollback(thrown);
|
||||
assertNull(manager.service(FirstService.class));
|
||||
assertNull(manager.service(SecondService.class));
|
||||
}
|
||||
|
||||
private static final class FirstService implements ModdedService {
|
||||
private int enableCount;
|
||||
private int disableCount;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
enableCount++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
disableCount++;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SecondService implements ModdedService {
|
||||
private final RuntimeException enableFailure;
|
||||
private final RuntimeException disableFailure;
|
||||
private int enableCount;
|
||||
private int disableCount;
|
||||
|
||||
private SecondService(RuntimeException enableFailure, RuntimeException disableFailure) {
|
||||
this.enableFailure = enableFailure;
|
||||
this.disableFailure = disableFailure;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
enableCount++;
|
||||
if (enableFailure != null) {
|
||||
throw enableFailure;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
disableCount++;
|
||||
if (disableFailure != null) {
|
||||
throw disableFailure;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -11,6 +11,7 @@ import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ModdedStructureHooksTest {
|
||||
@@ -51,4 +52,47 @@ public class ModdedStructureHooksTest {
|
||||
assertEquals(320, ModdedPlatformWorld.exclusiveMaxHeight(-64, 384));
|
||||
assertEquals(384, ModdedPlatformWorld.exclusiveMaxHeight(0, 384));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structureRegistryHooksRejectUnavailableServer() {
|
||||
ModdedStructureHooks hooks = new ModdedStructureHooks(() -> null);
|
||||
|
||||
IllegalStateException structureKeys = assertThrows(
|
||||
IllegalStateException.class, hooks::structureKeys);
|
||||
IllegalStateException structureSetKeys = assertThrows(
|
||||
IllegalStateException.class, hooks::structureSetKeys);
|
||||
IllegalStateException structureBiomeKeys = assertThrows(
|
||||
IllegalStateException.class, () -> hooks.structureBiomeKeys("minecraft:village"));
|
||||
|
||||
assertTrue(structureKeys.getMessage().contains("before the Minecraft server is available"));
|
||||
assertTrue(structureSetKeys.getMessage().contains("before the Minecraft server is available"));
|
||||
assertTrue(structureBiomeKeys.getMessage().contains("before the Minecraft server is available"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structureReachabilityHooksRejectUnavailableLevel() {
|
||||
ModdedStructureHooks hooks = new ModdedStructureHooks(() -> null);
|
||||
|
||||
IllegalStateException reachable = assertThrows(
|
||||
IllegalStateException.class, () -> hooks.reachableStructureKeys(null));
|
||||
IllegalStateException possibleBiomes = assertThrows(
|
||||
IllegalStateException.class, () -> hooks.possibleBiomeKeys(null));
|
||||
|
||||
assertTrue(reachable.getMessage().contains("without a bound modded ServerLevel"));
|
||||
assertTrue(possibleBiomes.getMessage().contains("without a bound modded ServerLevel"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structureRegistryHooksPreserveServerSupplierFailure() {
|
||||
IllegalStateException cause = new IllegalStateException("server supplier failed");
|
||||
ModdedStructureHooks hooks = new ModdedStructureHooks(() -> {
|
||||
throw cause;
|
||||
});
|
||||
|
||||
IllegalStateException failure = assertThrows(
|
||||
IllegalStateException.class, hooks::structureKeys);
|
||||
|
||||
assertTrue(failure.getMessage().contains("access the Minecraft server"));
|
||||
assertEquals(cause, failure.getCause());
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ModdedStructurePlacementFailureContractTest {
|
||||
@Test
|
||||
public void structurePlacementPropagatesRuntimeFailuresWithContext() throws IOException {
|
||||
Path sourcePath = Path.of(System.getProperty("iris.moddedCommonSources"),
|
||||
"art/arcane/iris/modded/ModdedStructureHooks.java");
|
||||
String source = Files.readString(sourcePath);
|
||||
int methodStart = source.indexOf("public int[] placeStructure(");
|
||||
int methodEnd = source.indexOf("\n @Override", methodStart + 1);
|
||||
String method = source.substring(methodStart, methodEnd);
|
||||
int catchStart = method.indexOf("catch (RuntimeException error)");
|
||||
|
||||
assertTrue(catchStart >= 0);
|
||||
String failurePath = method.substring(catchStart);
|
||||
assertTrue(failurePath.contains("throw NativeStructureGenerationException.failure("));
|
||||
assertTrue(failurePath.contains("\"capture placement\", structureKey, chunkX, chunkZ, error"));
|
||||
assertFalse(failurePath.contains("return null;"));
|
||||
assertFalse(failurePath.contains("catch (Throwable"));
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -39,8 +39,8 @@ public class ModdedTileParityTest {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
registries = RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY);
|
||||
TileData.bindFallbackReader(new ModdedTileReader(() -> null));
|
||||
TileData.bindFallbackFactory(ModdedTileData::fromProperties);
|
||||
TileData.bindPlatformReader(new ModdedTileReader(() -> null));
|
||||
TileData.bindPlatformFactory(ModdedTileData::fromProperties);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+26
-9
@@ -5,6 +5,9 @@ import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -23,6 +26,29 @@ public class ModdedWorldCheckTest {
|
||||
assertFalse(thread.isDaemon());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void poiAuditRunsInASecondServerTaskAfterVillageGeneration() throws IOException {
|
||||
Path sourcePath = Path.of(System.getProperty("iris.moddedCommonSources"),
|
||||
"art/arcane/iris/modded/ModdedWorldCheck.java");
|
||||
String source = Files.readString(sourcePath);
|
||||
int preparationSubmit = source.indexOf(
|
||||
"WorldCheckPreparation preparation = serverRef.submit(() -> run(serverRef)).join();");
|
||||
int completionSubmit = source.indexOf(
|
||||
"exitCode = serverRef.submit(() -> runAndRequestStop(", preparationSubmit);
|
||||
int completionMethod = source.indexOf("private static boolean completeWorldCheck");
|
||||
int deferredAudit = source.indexOf("PoiAudit poi = auditStructurePois", completionMethod);
|
||||
int structureMethod = source.indexOf("private static StructureCheckResult checkNativeStructure");
|
||||
int structureMethodEnd = source.indexOf("private static StructureStart resolveStructureStart",
|
||||
structureMethod);
|
||||
String structureSource = source.substring(structureMethod, structureMethodEnd);
|
||||
|
||||
assertTrue(preparationSubmit >= 0);
|
||||
assertTrue(completionSubmit > preparationSubmit);
|
||||
assertTrue(deferredAudit > completionMethod);
|
||||
assertFalse(structureSource.contains("auditStructurePois"));
|
||||
assertFalse(source.contains("prepareDeferredAudits"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validStructureStartIsGenerationEvidence() {
|
||||
assertTrue(ModdedWorldCheck.hasNativeStructureEvidence(true, 0));
|
||||
@@ -115,15 +141,6 @@ public class ModdedWorldCheckTest {
|
||||
assertFalse(ModdedWorldCheck.villageFoundationPass(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void villageFoundationAuditRejectsMissingBaseAndInvalidSupport() {
|
||||
assertTrue(ModdedWorldCheck.villageFoundationSupported(true, false, true, false));
|
||||
assertTrue(ModdedWorldCheck.villageFoundationSupported(true, true, false, false));
|
||||
assertFalse(ModdedWorldCheck.villageFoundationSupported(false, false, true, false));
|
||||
assertFalse(ModdedWorldCheck.villageFoundationSupported(true, false, false, false));
|
||||
assertFalse(ModdedWorldCheck.villageFoundationSupported(true, false, true, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void villagePoiGateRequiresInBoundsPoiWithoutOutOfBoundsRecords() {
|
||||
assertTrue(ModdedWorldCheck.villagePoiPass(1, 0));
|
||||
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.nativegen.NativeStructureGenerationException;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NativeStructureFailureContractTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structureLocateDoesNotCatchAndFallThroughToAnotherImplementation() throws IOException {
|
||||
Path sourcePath = Path.of(System.getProperty("iris.moddedCommonSources"),
|
||||
"art/arcane/iris/modded/IrisModdedChunkGenerator.java");
|
||||
String source = Files.readString(sourcePath);
|
||||
int locateStart = source.indexOf("public Pair<BlockPos, Holder<Structure>> findNearestMapStructure");
|
||||
int locateEnd = source.indexOf("public boolean isNativeStructureReachable", locateStart);
|
||||
String locate = source.substring(locateStart, locateEnd);
|
||||
int filterStart = source.indexOf("private HolderSet<Structure> filterReachableNativeStructures");
|
||||
int filterEnd = source.indexOf("private ServerLevel boundLevel", filterStart);
|
||||
String filter = source.substring(filterStart, filterEnd);
|
||||
|
||||
assertTrue(locate.contains("Engine current = engine();"));
|
||||
assertFalse(locate.contains("catch (Throwable"));
|
||||
assertFalse(locate.contains("return null;\n } catch"));
|
||||
assertTrue(filter.contains("unregistered structure holder"));
|
||||
assertFalse(filter.contains("catch (Throwable"));
|
||||
assertFalse(filter.contains("failed closed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void globalStructureDisableRefusesGeneratorBinding() {
|
||||
IrisModdedChunkGenerator.requireGlobalStructureGeneration(true, "overworld:overworld");
|
||||
|
||||
IllegalStateException error = assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> IrisModdedChunkGenerator.requireGlobalStructureGeneration(
|
||||
false, "overworld:overworld"));
|
||||
|
||||
assertTrue(error.getMessage().contains("overworld:overworld"));
|
||||
assertTrue(error.getMessage().contains("generate-structures=false"));
|
||||
assertTrue(error.getMessage().contains("importedStructures.disabled"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structureTerrainPreparationPrecedesVegetationAndPlacement() throws IOException {
|
||||
Path sourcePath = Path.of(System.getProperty("iris.moddedCommonSources"),
|
||||
"art/arcane/iris/modded/IrisModdedChunkGenerator.java");
|
||||
String source = Files.readString(sourcePath);
|
||||
int placementStart = source.indexOf("private void placeVanillaStructures");
|
||||
int placementEnd = source.indexOf("private static String nativeStructureBatchContext", placementStart);
|
||||
String placement = source.substring(placementStart, placementEnd);
|
||||
|
||||
assertTrue(placement.contains("\"terrain integration\""));
|
||||
assertTrue(placement.contains("prepareSurfaceStructures"));
|
||||
assertTrue(placement.contains("clearIntersectingVegetation"));
|
||||
assertTrue(placement.indexOf("prepareSurfaceStructures")
|
||||
< placement.indexOf("clearIntersectingVegetation"));
|
||||
assertTrue(placement.indexOf("clearIntersectingVegetation")
|
||||
< placement.indexOf("for (NativePlacementGroup group"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structureFailurePreservesPhaseIdentityChunkAndCause() {
|
||||
IllegalArgumentException cause = new IllegalArgumentException("broken placement");
|
||||
NativeStructureGenerationException error = NativeStructureGenerationException.failure(
|
||||
"placement", "minecraft:monument", 12, -8, cause);
|
||||
|
||||
assertSame(cause, error.getCause());
|
||||
assertTrue(error.getMessage().contains("placement"));
|
||||
assertTrue(error.getMessage().contains("minecraft:monument"));
|
||||
assertTrue(error.getMessage().contains("12,-8"));
|
||||
assertTrue(error.getMessage().contains("aborted"));
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package art.arcane.iris.modded.api;
|
||||
|
||||
import net.minecraft.resources.Identifier;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ModdedCustomContentRegistryTest {
|
||||
@Test
|
||||
public void publishesProvidersOnlyAfterCompleteDiscoveryAndCanRollBack() {
|
||||
String modId = "iris_discovery_success";
|
||||
TestProvider provider = new TestProvider(modId, null);
|
||||
boolean previousDiscoveryComplete = ModdedCustomContentRegistry.discoveryComplete();
|
||||
|
||||
ModdedCustomContentRegistry.Discovery discovery =
|
||||
ModdedCustomContentRegistry.discover(List.of(provider));
|
||||
try {
|
||||
assertTrue(ModdedCustomContentRegistry.discoveryComplete());
|
||||
assertTrue(ModdedCustomContentRegistry.hasProvider(modId));
|
||||
} finally {
|
||||
discovery.rollback();
|
||||
}
|
||||
|
||||
assertEquals(previousDiscoveryComplete, ModdedCustomContentRegistry.discoveryComplete());
|
||||
assertFalse(ModdedCustomContentRegistry.hasProvider(modId));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedDiscoveryPublishesNothingAndPreservesTheCause() {
|
||||
String firstModId = "iris_discovery_staged";
|
||||
String failingModId = "iris_discovery_failure";
|
||||
RuntimeException original = new RuntimeException("provider init failed");
|
||||
boolean previousDiscoveryComplete = ModdedCustomContentRegistry.discoveryComplete();
|
||||
|
||||
RuntimeException thrown = assertThrows(RuntimeException.class,
|
||||
() -> ModdedCustomContentRegistry.discover(List.of(
|
||||
new TestProvider(firstModId, null),
|
||||
new TestProvider(failingModId, original))));
|
||||
|
||||
assertSame(original, thrown);
|
||||
assertEquals(previousDiscoveryComplete, ModdedCustomContentRegistry.discoveryComplete());
|
||||
assertFalse(ModdedCustomContentRegistry.hasProvider(firstModId));
|
||||
assertFalse(ModdedCustomContentRegistry.hasProvider(failingModId));
|
||||
}
|
||||
|
||||
private static final class TestProvider implements ModdedDataProvider {
|
||||
private final String modId;
|
||||
private final RuntimeException failure;
|
||||
|
||||
private TestProvider(String modId, RuntimeException failure) {
|
||||
this.modId = modId;
|
||||
this.failure = failure;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String modId() {
|
||||
return modId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Identifier> getTypes(ModdedDataType type) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(Identifier id, ModdedDataType type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
if (failure != null) {
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+56
-7
@@ -20,6 +20,7 @@ public class IrisModdedStructureCommandTest {
|
||||
assertTrue(source.contains("generator.findNearestMapStructure("));
|
||||
assertTrue(source.contains("NATIVE_STRUCTURE_LOCATE_RADIUS = 100"));
|
||||
assertTrue(source.contains("HolderSet.direct(target.holder())"));
|
||||
assertFalse(source.contains("NativeStructureLocateCapability"));
|
||||
assertTrue(source.contains("boolean teleported = player.teleportTo("));
|
||||
assertTrue(source.contains("combineStructureKeys(irisKeys, nativeKeys)"));
|
||||
assertTrue(source.contains("irisGenerator.isNativeStructureReachable(holder)"));
|
||||
@@ -33,16 +34,64 @@ public class IrisModdedStructureCommandTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generatorLocatePrefersIrisPlacementsAndRejectsDormantNativeStarts() throws IOException {
|
||||
public void generatorLocateUsesIrisOnlyForExplicitReplacement() throws IOException {
|
||||
String source = moddedSource("IrisModdedChunkGenerator.java");
|
||||
int methodStart = source.indexOf("private Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(");
|
||||
int methodEnd = source.indexOf("private HolderSet<Structure> filterReachableNativeStructures(", methodStart);
|
||||
String method = source.substring(methodStart, methodEnd);
|
||||
int unexploredGuard = method.indexOf("if (findUnexplored)");
|
||||
int registryLookup = method.indexOf("level.registryAccess().lookupOrThrow(Registries.STRUCTURE)");
|
||||
int policyResolution = method.indexOf("NativeStructureGenerationPolicy.resolve(current,");
|
||||
int replacementCheck = method.indexOf(
|
||||
"decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS", policyResolution);
|
||||
int irisLocate = method.indexOf("IrisStructureLocator.locate(", replacementCheck);
|
||||
|
||||
assertTrue(source.contains("public Pair<BlockPos, Holder<Structure>> findNearestMapStructure("));
|
||||
assertTrue(source.contains("findNearestIrisStructure("));
|
||||
assertTrue(source.contains("filterReachableNativeStructures("));
|
||||
assertTrue(source.contains("IrisStructureLocator.suppressesVanilla(current, key)"));
|
||||
assertTrue(unexploredGuard >= 0);
|
||||
assertTrue(registryLookup > unexploredGuard);
|
||||
assertTrue(policyResolution > registryLookup);
|
||||
assertTrue(replacementCheck > policyResolution);
|
||||
assertTrue(irisLocate > replacementCheck);
|
||||
assertTrue(method.contains("LocateStatus.SEARCH_LIMIT_REACHED"));
|
||||
assertTrue(method.contains("new BlockPos(result.originX(), result.baseY(), result.originZ())"));
|
||||
assertFalse(method.contains("NativeStructureLocateCapability"));
|
||||
assertTrue(source.contains("structureBiomeSource.isStructureReachable(holder)"));
|
||||
assertTrue(source.contains("LocateStatus.SEARCH_LIMIT_REACHED"));
|
||||
assertTrue(source.contains("new BlockPos(result.originX(), result.baseY(), result.originZ())"));
|
||||
assertFalse(source.contains("isPaperUnavailable"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void commandResolvesNativePolicyBeforeAnyVanillaAliasLookup() throws IOException {
|
||||
String source = source("IrisModdedCommands.java");
|
||||
int methodStart = source.indexOf("private static int gotoStructure(");
|
||||
int methodEnd = source.indexOf("private static void locateIrisStructure(", methodStart);
|
||||
String method = source.substring(methodStart, methodEnd);
|
||||
int nativeResolution = method.indexOf("resolveNativeStructure(source, level, engine, key)");
|
||||
int genericIrisLookup = method.indexOf("IrisStructureLocator.isPlaced(engine, key)", nativeResolution);
|
||||
int policyResolution = method.indexOf("NativeStructureGenerationPolicy.resolve(engine, target.key(), false)", genericIrisLookup);
|
||||
int replacementCheck = method.indexOf(
|
||||
"decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS", policyResolution);
|
||||
int replacementLocate = method.indexOf("locateIrisStructure(source, level, engine, player, target.key())",
|
||||
replacementCheck);
|
||||
|
||||
assertTrue(nativeResolution >= 0);
|
||||
assertTrue(genericIrisLookup > nativeResolution);
|
||||
assertTrue(policyResolution > genericIrisLookup);
|
||||
assertTrue(replacementCheck > policyResolution);
|
||||
assertTrue(replacementLocate > replacementCheck);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyResolvesRegisteredNativeBeforeGenericIrisAliases() throws IOException {
|
||||
String source = source("IrisModdedCommands.java");
|
||||
int methodStart = source.indexOf("private static int verifyStructure(");
|
||||
int methodEnd = source.indexOf("private static Optional<NativeStructureTarget> resolveNativeStructure(",
|
||||
methodStart);
|
||||
String method = source.substring(methodStart, methodEnd);
|
||||
int nativeResolution = method.indexOf("resolveNativeStructure(source, level, engine, key)");
|
||||
int genericIrisLookup = method.indexOf("IrisStructureLocator.isPlaced(engine, key)", nativeResolution);
|
||||
|
||||
assertTrue(nativeResolution >= 0);
|
||||
assertTrue(genericIrisLookup > nativeResolution);
|
||||
assertTrue(method.contains("NativeStructureAvailability.IRIS_SUPPRESSED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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.modded.structure;
|
||||
|
||||
import net.minecraft.core.Direction;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
|
||||
public class ModdedJigsawStructureCaptureTest {
|
||||
@Test
|
||||
public void namespacedSourcePathsRemainDistinctAndPortable() {
|
||||
String nested = ModdedJigsawStructureCapture.pieceName("village", "mod:a/b");
|
||||
String underscored = ModdedJigsawStructureCapture.pieceName("village", "mod_a:b");
|
||||
|
||||
assertEquals("village/piece/mod/a/b", nested);
|
||||
assertEquals("village/piece/mod_a/b", underscored);
|
||||
assertNotEquals(nested, underscored);
|
||||
assertEquals("village/pool/mod/a/b", ModdedJigsawStructureCapture.poolName("village", "mod:a/b"));
|
||||
assertEquals(
|
||||
"village/piece/generated/legacy/mod/a/b",
|
||||
ModdedJigsawStructureCapture.legacyPieceName("village", "mod:a/b")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rootJsonRetainsGraphLimitsAndSourceIdentity() {
|
||||
Map<String, Object> root = ModdedJigsawStructureCapture.structureJson(
|
||||
"minecraft:village_plains",
|
||||
"village/pool/minecraft/village/plains/town_centers",
|
||||
6,
|
||||
81
|
||||
);
|
||||
|
||||
assertEquals("minecraft:village_plains", root.get("vanillaSource"));
|
||||
assertEquals(6, root.get("maxDepth"));
|
||||
assertEquals(6, root.get("maxSizeChunks"));
|
||||
assertEquals("STRUCTURE_PIECE", root.get("placeMode"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void connectorDirectionsUseIrisAxisNames() {
|
||||
assertEquals("UP_POSITIVE_Y", ModdedJigsawStructureCapture.directionName(Direction.UP));
|
||||
assertEquals("DOWN_NEGATIVE_Y", ModdedJigsawStructureCapture.directionName(Direction.DOWN));
|
||||
assertEquals("NORTH_NEGATIVE_Z", ModdedJigsawStructureCapture.directionName(Direction.NORTH));
|
||||
assertEquals("SOUTH_POSITIVE_Z", ModdedJigsawStructureCapture.directionName(Direction.SOUTH));
|
||||
assertEquals("EAST_POSITIVE_X", ModdedJigsawStructureCapture.directionName(Direction.EAST));
|
||||
assertEquals("WEST_NEGATIVE_X", ModdedJigsawStructureCapture.directionName(Direction.WEST));
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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.modded.structure;
|
||||
|
||||
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.StructureResourceBundle;
|
||||
import art.arcane.iris.core.structure.authoring.StructureSource;
|
||||
import art.arcane.iris.core.structure.authoring.StructureWriteMode;
|
||||
import art.arcane.iris.core.structure.authoring.StructureWriteOptions;
|
||||
import art.arcane.iris.core.structure.authoring.StructureWriteResult;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ModdedStructureImportServiceTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void writesThroughOwnedAddOnlyAndOverwriteTransactions() throws Exception {
|
||||
Path root = temporaryFolder.newFolder("modded-import").toPath();
|
||||
ModdedStructureImportService service = new ModdedStructureImportService(() -> null);
|
||||
ModdedStructureImportService.PreparedImport first = prepared(root, "one", StructureWriteMode.ADD_ONLY);
|
||||
|
||||
ModdedStructureImportService.ImportResult added = service.write(first);
|
||||
ModdedStructureImportService.ImportResult conflict = service.write(first);
|
||||
ModdedStructureImportService.ImportResult overwritten = service.write(
|
||||
prepared(root, "two", StructureWriteMode.OVERWRITE)
|
||||
);
|
||||
|
||||
assertTrue(added.success());
|
||||
assertEquals(StructureWriteResult.Status.ADDED, added.writeResult().orElseThrow().status());
|
||||
assertFalse(conflict.success());
|
||||
assertEquals(StructureWriteResult.Status.ADD_ONLY_CONFLICT, conflict.writeResult().orElseThrow().status());
|
||||
assertTrue(overwritten.success());
|
||||
assertEquals(StructureWriteResult.Status.OVERWRITTEN, overwritten.writeResult().orElseThrow().status());
|
||||
assertTrue(overwritten.capabilities().contains(StructureCapability.BLOCKS));
|
||||
}
|
||||
|
||||
private static ModdedStructureImportService.PreparedImport prepared(
|
||||
Path root,
|
||||
String content,
|
||||
StructureWriteMode mode
|
||||
) {
|
||||
StructureKey key = StructureKey.parse("iris:test_structure");
|
||||
StructureResourceBundle bundle = StructureResourceBundle.builder(key)
|
||||
.source(StructureSource.of(StructureSource.Kind.DATAPACK, StructureKey.parse("test:source")))
|
||||
.backend(StructureBackend.SNAPSHOT)
|
||||
.capability(StructureCapability.BLOCKS)
|
||||
.textResource("structures/test_structure.json", content)
|
||||
.build();
|
||||
return new ModdedStructureImportService.PreparedImport(
|
||||
root,
|
||||
new StructureWriteOptions(mode, false),
|
||||
ModdedStructureImportService.ImportKind.TEMPLATE,
|
||||
bundle,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* 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.modded.structure;
|
||||
|
||||
import art.arcane.iris.core.structure.authoring.StructureCapability;
|
||||
import art.arcane.iris.core.structure.authoring.StructureKey;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.IntTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.nbt.NbtUtils;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ModdedStructureTemplateCaptureTest {
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void capturesBlocksTilesAndExplicitStandaloneLosses() throws Exception {
|
||||
CompoundTag template = templateTag();
|
||||
|
||||
ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.captureTag(
|
||||
StructureKey.parse("minecraft:test/template"),
|
||||
template,
|
||||
BuiltInRegistries.BLOCK,
|
||||
false
|
||||
);
|
||||
|
||||
assertEquals(4, capture.width());
|
||||
assertEquals(1, capture.height());
|
||||
assertEquals(1, capture.depth());
|
||||
assertEquals(3, capture.blocks());
|
||||
assertEquals(1, capture.tiles());
|
||||
assertEquals(1, capture.jigsaws());
|
||||
assertEquals(1, capture.dataMarkers());
|
||||
assertEquals(3, capture.object().getBlocks().size());
|
||||
assertEquals(1, capture.object().getStates().size());
|
||||
assertTrue(capture.capabilities().contains(StructureCapability.BLOCKS));
|
||||
assertTrue(capture.capabilities().contains(StructureCapability.BLOCK_ENTITIES));
|
||||
assertFalse(capture.capabilities().contains(StructureCapability.CONNECTORS));
|
||||
assertTrue(hasLoss(capture, "connectors_not_imported"));
|
||||
assertTrue(hasLoss(capture, "data_markers_not_imported"));
|
||||
assertTrue(hasLoss(capture, "entities_not_imported"));
|
||||
ByteArrayOutputStream serialized = new ByteArrayOutputStream();
|
||||
capture.object().write(serialized);
|
||||
assertTrue(serialized.size() > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void graphCaptureReportsConnectorCapabilityWithoutStandaloneConnectorLoss() {
|
||||
ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.captureTag(
|
||||
StructureKey.parse("minecraft:test/template"),
|
||||
templateTag(),
|
||||
BuiltInRegistries.BLOCK,
|
||||
true
|
||||
);
|
||||
|
||||
assertTrue(capture.capabilities().contains(StructureCapability.CONNECTORS));
|
||||
assertFalse(hasLoss(capture, "connectors_not_imported"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reportsAdditionalNativePalettes() {
|
||||
CompoundTag template = templateTag();
|
||||
ListTag palettes = new ListTag();
|
||||
ListTag first = template.getListOrEmpty(StructureTemplate.PALETTE_TAG);
|
||||
palettes.add(first.copy());
|
||||
palettes.add(first.copy());
|
||||
template.remove(StructureTemplate.PALETTE_TAG);
|
||||
template.put(StructureTemplate.PALETTE_LIST_TAG, palettes);
|
||||
|
||||
ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.captureTag(
|
||||
StructureKey.parse("minecraft:test/template"),
|
||||
template,
|
||||
BuiltInRegistries.BLOCK,
|
||||
true
|
||||
);
|
||||
|
||||
assertTrue(hasLoss(capture, "palette_variants_not_imported"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void legacyCaptureOmitsAirBlocks() {
|
||||
CompoundTag template = new CompoundTag();
|
||||
template.put(StructureTemplate.SIZE_TAG, intList(1, 1, 1));
|
||||
ListTag palette = new ListTag();
|
||||
palette.add(NbtUtils.writeBlockState(Blocks.AIR.defaultBlockState()));
|
||||
template.put(StructureTemplate.PALETTE_TAG, palette);
|
||||
ListTag blocks = new ListTag();
|
||||
blocks.add(block(0, 0, 0, 0, null));
|
||||
template.put(StructureTemplate.BLOCKS_TAG, blocks);
|
||||
|
||||
ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.captureTag(
|
||||
StructureKey.parse("minecraft:test/legacy"),
|
||||
template,
|
||||
BuiltInRegistries.BLOCK,
|
||||
true,
|
||||
false
|
||||
);
|
||||
|
||||
assertEquals(0, capture.blocks());
|
||||
assertTrue(capture.object().getBlocks().isEmpty());
|
||||
}
|
||||
|
||||
private static CompoundTag templateTag() {
|
||||
CompoundTag template = new CompoundTag();
|
||||
template.put(StructureTemplate.SIZE_TAG, intList(4, 1, 1));
|
||||
ListTag palette = new ListTag();
|
||||
palette.add(NbtUtils.writeBlockState(Blocks.STONE.defaultBlockState()));
|
||||
palette.add(NbtUtils.writeBlockState(Blocks.CHEST.defaultBlockState()));
|
||||
palette.add(NbtUtils.writeBlockState(Blocks.JIGSAW.defaultBlockState()));
|
||||
palette.add(NbtUtils.writeBlockState(Blocks.STRUCTURE_BLOCK.defaultBlockState()));
|
||||
template.put(StructureTemplate.PALETTE_TAG, palette);
|
||||
|
||||
ListTag blocks = new ListTag();
|
||||
blocks.add(block(0, 0, 0, 0, null));
|
||||
CompoundTag chest = new CompoundTag();
|
||||
chest.putString("id", "minecraft:chest");
|
||||
chest.putString("CustomName", "test");
|
||||
blocks.add(block(1, 0, 0, 1, chest));
|
||||
CompoundTag jigsaw = new CompoundTag();
|
||||
jigsaw.putString("final_state", "minecraft:oak_planks");
|
||||
blocks.add(block(2, 0, 0, 2, jigsaw));
|
||||
blocks.add(block(3, 0, 0, 3, new CompoundTag()));
|
||||
template.put(StructureTemplate.BLOCKS_TAG, blocks);
|
||||
|
||||
ListTag entities = new ListTag();
|
||||
entities.add(new CompoundTag());
|
||||
template.put(StructureTemplate.ENTITIES_TAG, entities);
|
||||
return template;
|
||||
}
|
||||
|
||||
private static CompoundTag block(int x, int y, int z, int state, CompoundTag nbt) {
|
||||
CompoundTag block = new CompoundTag();
|
||||
block.put(StructureTemplate.BLOCK_TAG_POS, intList(x, y, z));
|
||||
block.putInt(StructureTemplate.BLOCK_TAG_STATE, state);
|
||||
if (nbt != null) {
|
||||
block.put(StructureTemplate.BLOCK_TAG_NBT, nbt);
|
||||
}
|
||||
return block;
|
||||
}
|
||||
|
||||
private static ListTag intList(int... values) {
|
||||
ListTag list = new ListTag();
|
||||
for (int value : values) {
|
||||
list.add(IntTag.valueOf(value));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private static boolean hasLoss(ModdedStructureTemplateCapture.Capture capture, String code) {
|
||||
return capture.losses().stream().anyMatch(loss -> loss.code().equals(code));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user