mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 12:41:43 +00:00
🧹
This commit is contained in:
+24
-3
@@ -49,8 +49,10 @@ import java.util.stream.Stream;
|
||||
|
||||
final class IrisModdedBiomeSource extends BiomeSource {
|
||||
private static final int BIOME_CACHE_MAX = 262144;
|
||||
private static final int UNRESOLVED_WARN_KEYS_MAX = 256;
|
||||
|
||||
private final BiomeSource serializedSource;
|
||||
private final Set<String> warnedUnresolvedBiomeKeys = ConcurrentHashMap.newKeySet();
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> visibleBiomeCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> surfaceStructureBiomeCache = new ConcurrentHashMap<>();
|
||||
@@ -70,6 +72,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
visibleBiomeCache.clear();
|
||||
structureBiomeCache.clear();
|
||||
surfaceStructureBiomeCache.clear();
|
||||
warnedUnresolvedBiomeKeys.clear();
|
||||
possibleStructureBiomeKeys = null;
|
||||
for (StructureStateBiomeSource source : structureStateSources) {
|
||||
source.clearCache();
|
||||
@@ -332,7 +335,8 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
IrisBiomeCustom customBiome = resolution.irisBiome().getCustomBiome(
|
||||
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
|
||||
if (customBiome == null) {
|
||||
return fallbackBiome(registry, quartX, quartY, quartZ, sampler);
|
||||
return fallbackBiome(registry, "custom derivative of '"
|
||||
+ resolution.irisBiome().getLoadKey() + "'", quartX, quartY, quartZ, sampler);
|
||||
}
|
||||
biomeKey = ModdedWorldgenIds.biomeRef(engine, customBiome.getId());
|
||||
} else if (resolution.underground()) {
|
||||
@@ -344,7 +348,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
}
|
||||
Holder<Biome> resolved = resolveHolder(registry, biomeKey);
|
||||
return resolved == null
|
||||
? fallbackBiome(registry, quartX, quartY, quartZ, sampler)
|
||||
? fallbackBiome(registry, biomeKey, quartX, quartY, quartZ, sampler)
|
||||
: resolved;
|
||||
}
|
||||
|
||||
@@ -567,12 +571,29 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
return identifier == null ? null : identifier.toString().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private Holder<Biome> fallbackBiome(Registry<Biome> registry, int quartX, int quartY, int quartZ,
|
||||
private Holder<Biome> fallbackBiome(Registry<Biome> registry, String unresolvedKey,
|
||||
int quartX, int quartY, int quartZ,
|
||||
Climate.Sampler sampler) {
|
||||
Holder<Biome> plains = resolveHolder(registry, "minecraft:plains");
|
||||
warnUnresolvedBiome(unresolvedKey, plains == null ? "the serialized biome source" : "minecraft:plains",
|
||||
quartX, quartY, quartZ);
|
||||
return plains == null ? serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler) : plains;
|
||||
}
|
||||
|
||||
private void warnUnresolvedBiome(String unresolvedKey, String fallback,
|
||||
int quartX, int quartY, int quartZ) {
|
||||
String key = unresolvedKey == null || unresolvedKey.isBlank() ? "<blank>" : unresolvedKey;
|
||||
if (!warnedUnresolvedBiomeKeys.add(key)) {
|
||||
return;
|
||||
}
|
||||
if (warnedUnresolvedBiomeKeys.size() > UNRESOLVED_WARN_KEYS_MAX) {
|
||||
warnedUnresolvedBiomeKeys.clear();
|
||||
}
|
||||
ModdedIrisLog.warn("Iris biome " + key + " is not registered; using " + fallback
|
||||
+ " at quart " + quartX + "," + quartY + "," + quartZ
|
||||
+ " (wrong biome generates; regenerate the forced datapack and restart)");
|
||||
}
|
||||
|
||||
private static long packNoiseKey(int x, int y, int z) {
|
||||
return (((long) x & 67108863L) << 38)
|
||||
| (((long) z & 67108863L) << 12)
|
||||
|
||||
+40
-605
@@ -23,27 +23,15 @@ 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.GenerationSessionLease;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.framework.NativeStructurePlacementPlanner;
|
||||
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
|
||||
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.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisMaterialPalette;
|
||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.nativegen.NativeStructureGenerationException;
|
||||
import art.arcane.iris.nativegen.NativeStructureStartInjector;
|
||||
import art.arcane.iris.nativegen.NativeStructureReferenceEnvelope;
|
||||
import art.arcane.iris.nativegen.NativeStructureLocateResults;
|
||||
import art.arcane.iris.nativegen.NativeStructurePostProcessor;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
@@ -54,9 +42,7 @@ import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
@@ -83,15 +69,12 @@ import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.LegacyRandomSource;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.RandomSupport;
|
||||
import net.minecraft.world.level.levelgen.WorldgenRandom;
|
||||
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
|
||||
import net.minecraft.world.level.levelgen.blending.Blender;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
@@ -100,98 +83,41 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
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;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
|
||||
public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
private static final int WORLD_CHECK_SHIFT_RECORD_LIMIT = 4096;
|
||||
private static final boolean WORLD_CHECK_ENABLED = Boolean.getBoolean("iris.worldcheck");
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
public static final MapCodec<IrisModdedChunkGenerator> CODEC = RecordCodecBuilder.mapCodec((RecordCodecBuilder.Instance<IrisModdedChunkGenerator> instance) -> instance.group(
|
||||
BiomeSource.CODEC.fieldOf("biome_source").forGetter((IrisModdedChunkGenerator generator) -> generator.serializedBiomeSource),
|
||||
Codec.STRING.fieldOf("dimension").forGetter((IrisModdedChunkGenerator generator) -> generator.dimensionKey)
|
||||
).apply(instance, IrisModdedChunkGenerator::new));
|
||||
|
||||
private static final AtomicInteger GEN_THREAD_SEQ = new AtomicInteger();
|
||||
private static final boolean PARALLEL_CHUNK_SYSTEM = detectParallelChunkSystem();
|
||||
private static volatile ExecutorService genPool = createGenPool();
|
||||
|
||||
public static void startGenPool() {
|
||||
ExecutorService pool = genPool;
|
||||
if (pool == null || pool.isShutdown()) {
|
||||
genPool = createGenPool();
|
||||
}
|
||||
ModdedGenPool.start();
|
||||
}
|
||||
|
||||
public static void shutdownGenPool() {
|
||||
ExecutorService pool = genPool;
|
||||
if (pool != null) {
|
||||
pool.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean detectParallelChunkSystem() {
|
||||
String[] markers = {
|
||||
"com.ishland.c2me.base.ModProperties",
|
||||
"com.ishland.c2me.base.common.config.C2MEConfig",
|
||||
"com.ishland.c2me.opts.chunkio.ModProperties",
|
||||
"ca.spottedleaf.moonrise.common.util.MoonriseCommon"
|
||||
};
|
||||
for (String marker : markers) {
|
||||
try {
|
||||
Class.forName(marker, false, IrisModdedChunkGenerator.class.getClassLoader());
|
||||
return true;
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ExecutorService createGenPool() {
|
||||
int threads = Math.max(2, Runtime.getRuntime().availableProcessors());
|
||||
ThreadPoolExecutor pool = new ThreadPoolExecutor(
|
||||
threads, threads, 30L, TimeUnit.SECONDS,
|
||||
new LinkedBlockingQueue<>(),
|
||||
runnable -> {
|
||||
Thread thread = new Thread(runnable, "Iris ModGen-" + GEN_THREAD_SEQ.incrementAndGet());
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
pool.allowCoreThreadTimeOut(true);
|
||||
return pool;
|
||||
ModdedGenPool.shutdown();
|
||||
}
|
||||
|
||||
private final String dimensionKey;
|
||||
private final String defaultPack;
|
||||
private final String defaultDimensionKey;
|
||||
private final BiomeSource serializedBiomeSource;
|
||||
private final IrisModdedBiomeSource structureBiomeSource;
|
||||
private final EngineBinding<Engine> engineBinding = new EngineBinding<>(60L, TimeUnit.SECONDS);
|
||||
private final ConcurrentHashMap<Biome, Holder<Biome>> vanillaSpawnBiomes = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<NativeStructureStartKey, Integer> worldCheckStructureShifts = new ConcurrentHashMap<>();
|
||||
final IrisModdedBiomeSource structureBiomeSource;
|
||||
private final ModdedEngineBinding<Engine> engineBinding = new ModdedEngineBinding<>(60L, TimeUnit.SECONDS);
|
||||
private final ModdedNativeStructureStage nativeStructures = new ModdedNativeStructureStage(this);
|
||||
private final ModdedSpawnTableMerger spawnTables = new ModdedSpawnTableMerger(this);
|
||||
private final AtomicBoolean announced = new AtomicBoolean(false);
|
||||
private volatile boolean vanillaSpawnBiomesInitialized;
|
||||
private volatile boolean unloading;
|
||||
private volatile Engine engine;
|
||||
private volatile String activePack;
|
||||
@@ -199,8 +125,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
private volatile long seedOverride = Long.MIN_VALUE;
|
||||
private volatile long lastChunkGenAt = 0L;
|
||||
private volatile Set<String> configuredStructureBiomeKeys;
|
||||
private volatile ConfiguredPack configuredPack;
|
||||
private volatile StructureStepCache structureStepCache;
|
||||
private volatile ModdedDimensionMetadata.ConfiguredPack configuredPack;
|
||||
|
||||
public IrisModdedChunkGenerator(BiomeSource biomeSource, String dimensionKey) {
|
||||
this(biomeSource, dimensionKey, new IrisModdedBiomeSource(biomeSource));
|
||||
@@ -262,8 +187,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
this.engineBinding.complete(replacement);
|
||||
this.announced.set(false);
|
||||
this.structureBiomeSource.clearCaches();
|
||||
this.worldCheckStructureShifts.clear();
|
||||
resetVanillaSpawnBiomes();
|
||||
this.nativeStructures.clearWorldCheckStructureShifts();
|
||||
this.spawnTables.resetVanillaSpawnBiomes();
|
||||
}
|
||||
|
||||
public synchronized void unbindEngine() {
|
||||
@@ -287,8 +212,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
this.engineBinding.reset();
|
||||
this.announced.set(false);
|
||||
this.structureBiomeSource.clearCaches();
|
||||
this.worldCheckStructureShifts.clear();
|
||||
resetVanillaSpawnBiomes();
|
||||
this.nativeStructures.clearWorldCheckStructureShifts();
|
||||
this.spawnTables.resetVanillaSpawnBiomes();
|
||||
}
|
||||
|
||||
private void applyUnboundConfiguration(String pack, String packDimensionKey, long seed) {
|
||||
@@ -301,8 +226,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
this.engineBinding.reset();
|
||||
this.announced.set(false);
|
||||
this.structureBiomeSource.clearCaches();
|
||||
this.worldCheckStructureShifts.clear();
|
||||
resetVanillaSpawnBiomes();
|
||||
this.nativeStructures.clearWorldCheckStructureShifts();
|
||||
this.spawnTables.resetVanillaSpawnBiomes();
|
||||
}
|
||||
|
||||
public synchronized void resetToDefault() {
|
||||
@@ -335,9 +260,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
Engine current = engine();
|
||||
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_structure_locate");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
Pair<BlockPos, Holder<Structure>> irisPlaced = findNearestIrisStructure(
|
||||
Pair<BlockPos, Holder<Structure>> irisPlaced = nativeStructures.findNearestIrisStructure(
|
||||
level, holders, pos, Math.max(1, radius), findUnexplored, current);
|
||||
HolderSet<Structure> reachable = filterReachableNativeStructures(level, holders, current);
|
||||
HolderSet<Structure> reachable = nativeStructures.filterReachableNativeStructures(
|
||||
level, holders, current);
|
||||
Pair<BlockPos, Holder<Structure>> nativeLocated = reachable.size() == 0
|
||||
? null
|
||||
: super.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
|
||||
@@ -349,67 +275,6 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
return structure != null && structureBiomeSource.isStructureReachable(structure);
|
||||
}
|
||||
|
||||
private Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(ServerLevel level,
|
||||
HolderSet<Structure> holders,
|
||||
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;
|
||||
long bestDistance = Long.MAX_VALUE;
|
||||
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();
|
||||
if (!IrisStructureLocator.isPlaced(current, structureId)) {
|
||||
continue;
|
||||
}
|
||||
IrisStructureLocator.LocateResult result = IrisStructureLocator.locate(
|
||||
current, structureId, pos.getX(), pos.getZ(), radius);
|
||||
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
|
||||
throw new IllegalStateException("Iris structure locate reached its safety limit for "
|
||||
+ structureId + " within " + radius + " chunks");
|
||||
}
|
||||
if (!result.found()) {
|
||||
continue;
|
||||
}
|
||||
long dx = (long) result.originX() - pos.getX();
|
||||
long dz = (long) result.originZ() - pos.getZ();
|
||||
long distance = dx * dx + dz * dz;
|
||||
if (distance < bestDistance) {
|
||||
bestDistance = distance;
|
||||
best = new BlockPos(result.originX(), result.baseY(), result.originZ());
|
||||
bestHolder = holder;
|
||||
}
|
||||
}
|
||||
return best == null ? null : Pair.of(best, bestHolder);
|
||||
}
|
||||
|
||||
private HolderSet<Structure> filterReachableNativeStructures(ServerLevel level, HolderSet<Structure> holders,
|
||||
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");
|
||||
}
|
||||
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() {
|
||||
MinecraftServer server = ModdedEngineBootstrap.currentServer();
|
||||
if (server == null) {
|
||||
@@ -423,7 +288,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
return null;
|
||||
}
|
||||
|
||||
private Engine engine() {
|
||||
Engine engine() {
|
||||
requireBindingAllowed();
|
||||
Engine cached = engine;
|
||||
requireCompletedShutdown(cached);
|
||||
@@ -516,7 +381,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
+ "and deny individual structures through importedStructures.disabled");
|
||||
}
|
||||
|
||||
private Engine engineOrNull() {
|
||||
Engine engineOrNull() {
|
||||
requireBindingAllowed();
|
||||
Engine cached = engine;
|
||||
requireCompletedShutdown(cached);
|
||||
@@ -607,7 +472,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
if (current != null && !current.isClosed() && !current.isClosing()) {
|
||||
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_configured_biome_keys");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
Set<String> resolved = collectConfiguredBiomeKeys(
|
||||
Set<String> resolved = ModdedDimensionMetadata.collectConfiguredBiomeKeys(
|
||||
current.getAllBiomes(), current.getDimension().getLoadKey());
|
||||
configuredStructureBiomeKeys = resolved;
|
||||
return resolved;
|
||||
@@ -617,15 +482,16 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
}
|
||||
}
|
||||
ConfiguredPack configured = configuredPack();
|
||||
Set<String> resolved = collectConfiguredBiomeKeys(configured.dimension(), configured.data());
|
||||
ModdedDimensionMetadata.ConfiguredPack configured = configuredPack();
|
||||
Set<String> resolved = ModdedDimensionMetadata.collectConfiguredBiomeKeys(
|
||||
configured.dimension(), configured.data());
|
||||
configuredStructureBiomeKeys = resolved;
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
private ConfiguredPack configuredPack() {
|
||||
ConfiguredPack cached = configuredPack;
|
||||
private ModdedDimensionMetadata.ConfiguredPack configuredPack() {
|
||||
ModdedDimensionMetadata.ConfiguredPack cached = configuredPack;
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
@@ -641,60 +507,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
throw new IllegalStateException("Iris dimension '" + activeDimensionKey
|
||||
+ "' missing from pack " + packDirectory.getAbsolutePath());
|
||||
}
|
||||
ConfiguredPack resolved = new ConfiguredPack(data, dimension, dimensionMetadata(dimension));
|
||||
ModdedDimensionMetadata.ConfiguredPack resolved = new ModdedDimensionMetadata.ConfiguredPack(
|
||||
data, dimension, ModdedDimensionMetadata.dimensionMetadata(dimension));
|
||||
configuredPack = resolved;
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
static DimensionMetadata dimensionMetadata(IrisDimension dimension) {
|
||||
int minY = dimension.getMinHeight();
|
||||
int maxY = dimension.getMaxHeight();
|
||||
if (maxY <= minY) {
|
||||
throw new IllegalStateException("Iris dimension '" + dimension.getLoadKey()
|
||||
+ "' has invalid height range " + minY + ".." + maxY);
|
||||
}
|
||||
return new DimensionMetadata(minY, maxY, minY + dimension.getFluidHeight());
|
||||
}
|
||||
|
||||
static Set<String> collectConfiguredBiomeKeys(IrisDimension dimension, IrisData data) {
|
||||
LinkedHashSet<String> keys = new LinkedHashSet<>(
|
||||
collectConfiguredBiomeKeys(dimension.getReachableBiomes(() -> data), dimension.getLoadKey()));
|
||||
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
|
||||
if (region == null) {
|
||||
continue;
|
||||
}
|
||||
if (!region.getSeaBiomes().isEmpty()) {
|
||||
keys.add("minecraft:the_void");
|
||||
}
|
||||
if (!region.getShoreBiomes().isEmpty()) {
|
||||
keys.add("minecraft:beach");
|
||||
}
|
||||
}
|
||||
return Set.copyOf(keys);
|
||||
}
|
||||
|
||||
static Set<String> collectConfiguredBiomeKeys(Iterable<IrisBiome> biomes, String dimensionLoadKey) {
|
||||
LinkedHashSet<String> keys = new LinkedHashSet<>();
|
||||
String namespace = dimensionLoadKey.toLowerCase(Locale.ROOT);
|
||||
for (IrisBiome irisBiome : biomes) {
|
||||
if (irisBiome == null) {
|
||||
continue;
|
||||
}
|
||||
Identifier derivative = Identifier.tryParse(irisBiome.getStructureDerivativeKey());
|
||||
if (derivative != null) {
|
||||
keys.add(derivative.toString().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
if (!irisBiome.isCustom()) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
keys.add(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
return Set.copyOf(keys);
|
||||
}
|
||||
|
||||
public String dimensionKey() {
|
||||
return dimensionKey;
|
||||
}
|
||||
@@ -715,8 +534,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
public void onHotload() {
|
||||
configuredStructureBiomeKeys = null;
|
||||
structureBiomeSource.clearCaches();
|
||||
worldCheckStructureShifts.clear();
|
||||
resetVanillaSpawnBiomes();
|
||||
nativeStructures.clearWorldCheckStructureShifts();
|
||||
spawnTables.resetVanillaSpawnBiomes();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -730,8 +549,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
|
||||
Registry<Biome> registry = structureManager.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
initializeVanillaSpawnBiomes(registry);
|
||||
Holder<Biome> vanillaSpawnBiome = vanillaSpawnBiomes.get(biome.value());
|
||||
spawnTables.initializeVanillaSpawnBiomes(registry);
|
||||
Holder<Biome> vanillaSpawnBiome = spawnTables.vanillaSpawnBiome(biome.value());
|
||||
if (vanillaSpawnBiome == null) {
|
||||
return explicitSpawns;
|
||||
}
|
||||
@@ -744,57 +563,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
return explicitSpawns;
|
||||
}
|
||||
|
||||
SpawnTableKey key = new SpawnTableKey(biome.value(), category);
|
||||
return mergedSpawnTables.computeIfAbsent(key, ignored -> NativeSpawnTableMerger.merge(vanillaSpawns, explicitSpawns));
|
||||
}
|
||||
|
||||
private synchronized void initializeVanillaSpawnBiomes(Registry<Biome> registry) {
|
||||
if (vanillaSpawnBiomesInitialized) {
|
||||
return;
|
||||
}
|
||||
Engine current = engineOrNull();
|
||||
if (current == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_spawn_biomes");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
String namespace = current.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
|
||||
for (IrisBiome irisBiome : current.getDimension().getReachableBiomes(current)) {
|
||||
if (irisBiome == null || !irisBiome.isCustom()) {
|
||||
continue;
|
||||
}
|
||||
Holder<Biome> vanillaHolder = resolveBiomeHolder(registry, irisBiome.getVanillaDerivativeKey());
|
||||
if (vanillaHolder == null) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
Holder<Biome> customHolder = resolveBiomeHolder(registry, namespace + ":" + customBiome.getId());
|
||||
if (customHolder != null) {
|
||||
vanillaSpawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
vanillaSpawnBiomesInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String key) {
|
||||
if (key == null || key.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
Identifier identifier = Identifier.tryParse(key);
|
||||
if (identifier == null) {
|
||||
return null;
|
||||
}
|
||||
Optional<Holder.Reference<Biome>> reference = registry.get(identifier);
|
||||
return reference.<Holder<Biome>>map((Holder.Reference<Biome> value) -> value).orElse(null);
|
||||
}
|
||||
|
||||
private synchronized void resetVanillaSpawnBiomes() {
|
||||
vanillaSpawnBiomes.clear();
|
||||
mergedSpawnTables.clear();
|
||||
vanillaSpawnBiomesInitialized = false;
|
||||
return spawnTables.mergedSpawnTable(biome.value(), category, vanillaSpawns, explicitSpawns);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -817,13 +586,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
|
||||
PlatformBlockState air = IrisPlatforms.get().registries().air();
|
||||
|
||||
if (PARALLEL_CHUNK_SYSTEM) {
|
||||
if (ModdedGenPool.parallelChunkSystem()) {
|
||||
return CompletableFuture.completedFuture(
|
||||
generateTerrain(chunk, generationEngine, pos, air));
|
||||
}
|
||||
return CompletableFuture.supplyAsync(
|
||||
() -> generateTerrain(chunk, generationEngine, pos, air),
|
||||
genPool);
|
||||
ModdedGenPool.pool());
|
||||
}
|
||||
|
||||
private ChunkAccess generateTerrain(ChunkAccess chunk, Engine generationEngine, ChunkPos pos,
|
||||
@@ -944,7 +713,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
Engine current = engine();
|
||||
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_biome_decoration");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
placeVanillaStructures(level, chunk, structureManager);
|
||||
nativeStructures.placeVanillaStructures(level, chunk, structureManager);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -967,7 +736,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
this,
|
||||
structureBiomeSource
|
||||
));
|
||||
adjustGeneratedStructures(
|
||||
nativeStructures.adjustGeneratedStructures(
|
||||
registryAccess, chunk, previousStarts, configuredStarts, current, templateManager);
|
||||
}
|
||||
}
|
||||
@@ -981,269 +750,17 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess chunk,
|
||||
Map<Structure, StructureStart> previousStarts,
|
||||
Map<Structure, NativeStructureStartPlan> configuredStarts,
|
||||
Engine current,
|
||||
StructureTemplateManager templateManager) {
|
||||
Registry<Structure> registry = registryAccess.lookupOrThrow(Registries.STRUCTURE);
|
||||
ChunkPos chunkPos = chunk.getPos();
|
||||
for (Map.Entry<Structure, StructureStart> entry : chunk.getAllStarts().entrySet()) {
|
||||
Structure structure = entry.getKey();
|
||||
StructureStart start = entry.getValue();
|
||||
if (!start.isValid() || previousStarts.get(structure) == start) {
|
||||
continue;
|
||||
}
|
||||
if (configuredStarts.containsKey(structure)) {
|
||||
recordWorldCheckStructureShift(
|
||||
configuredStarts.get(structure).source().getStructure(), start.getChunkPos(), 0);
|
||||
continue;
|
||||
}
|
||||
Identifier id = registry.getKey(structure);
|
||||
String structureId = id == null ? null : id.toString();
|
||||
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.applyVerticalPlacement(
|
||||
start,
|
||||
structureId,
|
||||
decision.yShift(),
|
||||
getSeaLevel(),
|
||||
chunk.getMinY(),
|
||||
chunk.getMinY() + chunk.getHeight(),
|
||||
undergroundStep,
|
||||
decision.preserveSourceY(),
|
||||
decision.yBand(),
|
||||
(x, z) -> current.getHeight(x, z, true) + current.getMinHeight());
|
||||
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
|
||||
start, structure, start.getReferences(), templateManager,
|
||||
NativeStructurePostProcessor.resolveNativeTerrain(start, decision.terrain()));
|
||||
chunk.setStartForStructure(structure, wrapped);
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"vertical adjustment", structureId, chunkPos.x(), chunkPos.z(), error);
|
||||
}
|
||||
recordWorldCheckStructureShift(structureId, start.getChunkPos(), offsetY);
|
||||
}
|
||||
}
|
||||
|
||||
private void placeVanillaStructures(WorldGenLevel world, ChunkAccess chunk, StructureManager structureManager) {
|
||||
if (!structureManager.shouldGenerateStructures()) {
|
||||
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());
|
||||
BlockPos origin = sectionPos.origin();
|
||||
Registry<Structure> registry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
List<List<Structure>> byStep = structuresByStep(registry);
|
||||
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
|
||||
long decorationSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ());
|
||||
BoundingBox area = writableArea(chunk);
|
||||
int steps = GenerationStep.Decoration.values().length;
|
||||
Engine current = engine();
|
||||
List<NativePlacementGroup> placementGroups = new ArrayList<>();
|
||||
List<StructureStart> nativeStarts = new ArrayList<>();
|
||||
List<NativeStructurePostProcessor.VegetationTarget> vegetationTargets = new ArrayList<>();
|
||||
List<NativeStructurePostProcessor.TerrainTarget> terrainTargets = 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();
|
||||
if (structureId == null) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"resolution", null, chunkPos.x(), chunkPos.z());
|
||||
}
|
||||
try {
|
||||
IrisNativeStructureDecision sourceDecision = NativeStructureGenerationPolicy.resolve(current,
|
||||
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
|
||||
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
|
||||
List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size());
|
||||
for (StructureStart start : starts) {
|
||||
NativeStructureStartPlan plan = NativeStructurePlacementPlanner.matchingPlan(
|
||||
current, structureId, start.getChunkPos().x(), start.getChunkPos().z());
|
||||
IrisNativeStructureDecision decision = plan == null
|
||||
? sourceDecision : NativeStructurePlacementPlanner.decisionFor(plan);
|
||||
if (!decision.generate()) {
|
||||
continue;
|
||||
}
|
||||
resolvedPlacements.add(new NativePlacement(start, decision));
|
||||
terrainTargets.add(new NativeStructurePostProcessor.TerrainTarget(
|
||||
structureId, start,
|
||||
NativeStructurePostProcessor.resolveNativeTerrain(
|
||||
start, decision.terrain())));
|
||||
if (plan == null || !plan.placement().isUnderground()) {
|
||||
nativeStarts.add(start);
|
||||
}
|
||||
boolean clearEntireFootprint = NativeStructurePostProcessor
|
||||
.shouldClearEntireVegetationFootprint(
|
||||
structure.step(), decision.clearVegetation());
|
||||
vegetationTargets.add(new NativeStructurePostProcessor.VegetationTarget(
|
||||
start, clearEntireFootprint));
|
||||
}
|
||||
if (!resolvedPlacements.isEmpty()) {
|
||||
placementGroups.add(new NativePlacementGroup(
|
||||
structureId, index, step, List.copyOf(resolvedPlacements)));
|
||||
}
|
||||
} 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 error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"vegetation cleanup", nativeStructureBatchContext(placementGroups),
|
||||
chunkPos.x(), chunkPos.z(), error);
|
||||
}
|
||||
try {
|
||||
NativeStructurePostProcessor.prepareTerrain(
|
||||
world, area, terrainTargets, this::resolvePaletteBlock);
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"terrain carving", nativeStructureBatchContext(placementGroups),
|
||||
chunkPos.x(), chunkPos.z(), error);
|
||||
}
|
||||
for (NativePlacementGroup group : placementGroups) {
|
||||
random.setFeatureSeed(decorationSeed, group.featureIndex(), group.step());
|
||||
try {
|
||||
for (NativePlacement placement : group.placements()) {
|
||||
placeVanillaStructure(world, structureManager, random, area, chunkPos,
|
||||
group.structureId(), placement.start(), placement.decision());
|
||||
}
|
||||
} 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,
|
||||
IrisNativeStructureDecision decision) {
|
||||
NativeStructurePostProcessor.place(world, structureManager, this, random, area, chunkPos,
|
||||
structureId, start, decision, this::resolvePaletteBlock,
|
||||
(x, z) -> engine().getHeight(x, z, true) + engine().getMinHeight());
|
||||
}
|
||||
|
||||
private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
|
||||
StructureStepCache cached = structureStepCache;
|
||||
if (cached != null && cached.registry() == registry) {
|
||||
return cached.structures();
|
||||
}
|
||||
synchronized (this) {
|
||||
cached = structureStepCache;
|
||||
if (cached != null && cached.registry() == registry) {
|
||||
return cached.structures();
|
||||
}
|
||||
int steps = GenerationStep.Decoration.values().length;
|
||||
List<List<Structure>> grouped = new ArrayList<>(steps);
|
||||
for (int step = 0; step < steps; step++) {
|
||||
grouped.add(new ArrayList<>());
|
||||
}
|
||||
for (Structure structure : registry) {
|
||||
grouped.get(structure.step().ordinal()).add(structure);
|
||||
}
|
||||
for (int step = 0; step < steps; step++) {
|
||||
grouped.set(step, List.copyOf(grouped.get(step)));
|
||||
}
|
||||
List<List<Structure>> resolved = List.copyOf(grouped);
|
||||
structureStepCache = new StructureStepCache(registry, resolved);
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
private void recordWorldCheckStructureShift(String structureId, ChunkPos startChunk, int offsetY) {
|
||||
if (!WORLD_CHECK_ENABLED || structureId == null) {
|
||||
return;
|
||||
}
|
||||
if (worldCheckStructureShifts.size() >= WORLD_CHECK_SHIFT_RECORD_LIMIT) {
|
||||
worldCheckStructureShifts.clear();
|
||||
}
|
||||
worldCheckStructureShifts.put(new NativeStructureStartKey(structureId, startChunk.pack()), offsetY);
|
||||
}
|
||||
|
||||
Integer worldCheckStructureShift(String structureId, ChunkPos startChunk) {
|
||||
if (structureId == null || startChunk == null) {
|
||||
return null;
|
||||
}
|
||||
return worldCheckStructureShifts.get(new NativeStructureStartKey(structureId, startChunk.pack()));
|
||||
}
|
||||
|
||||
private BlockState resolvePaletteBlock(IrisMaterialPalette palette, RNG rng,
|
||||
int x, int y, int z) {
|
||||
PlatformBlockState platformState = palette.get(rng, x, y, z, engine().getData());
|
||||
if (platformState == null || !(platformState.nativeHandle() instanceof BlockState blockState)) {
|
||||
throw new IllegalStateException("Configured native structure palette did not resolve a Minecraft block at "
|
||||
+ x + "," + y + "," + z);
|
||||
}
|
||||
return blockState;
|
||||
}
|
||||
|
||||
private BoundingBox writableArea(ChunkAccess chunk) {
|
||||
ChunkPos chunkPos = chunk.getPos();
|
||||
int minX = chunkPos.getMinBlockX();
|
||||
int minZ = chunkPos.getMinBlockZ();
|
||||
int minY = chunk.getMinY();
|
||||
int maxY = minY + chunk.getHeight() - 1;
|
||||
return new BoundingBox(minX, minY, minZ, minX + 15, maxY, minZ + 15);
|
||||
return nativeStructures.worldCheckStructureShift(structureId, startChunk);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spawnOriginalMobs(WorldGenRegion region) {
|
||||
Registry<Biome> registry = region.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
initializeVanillaSpawnBiomes(registry);
|
||||
spawnTables.initializeVanillaSpawnBiomes(registry);
|
||||
ChunkPos center = region.getCenter();
|
||||
Holder<Biome> visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
|
||||
Holder<Biome> vanillaBiome = vanillaSpawnBiomes.get(visibleBiome.value());
|
||||
Holder<Biome> vanillaBiome = spawnTables.vanillaSpawnBiome(visibleBiome.value());
|
||||
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
|
||||
random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ());
|
||||
NaturalSpawner.spawnMobsForChunkGeneration(
|
||||
@@ -1276,13 +793,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
|
||||
@Override
|
||||
public int getSpawnHeight(LevelHeightAccessor heightAccessor) {
|
||||
return clampSpawnHeight(heightAccessor.getMinY(), heightAccessor.getHeight());
|
||||
}
|
||||
|
||||
static int clampSpawnHeight(int minY, int height) {
|
||||
int minimum = minY + 1;
|
||||
int maximum = minY + height - 2;
|
||||
return Math.max(minimum, Math.min(maximum, 96));
|
||||
return ModdedDimensionMetadata.clampSpawnHeight(heightAccessor.getMinY(), heightAccessor.getHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1320,7 +831,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private GenerationSessionLease requireGenerationLease(Engine current, String operation) {
|
||||
GenerationSessionLease requireGenerationLease(Engine current, String operation) {
|
||||
try {
|
||||
return current.acquireGenerationLease(operation);
|
||||
} catch (GenerationSessionException exception) {
|
||||
@@ -1333,80 +844,4 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
info.add("Iris dimension: " + dimensionKey);
|
||||
}
|
||||
|
||||
private record SpawnTableKey(Biome biome, MobCategory category) {
|
||||
}
|
||||
|
||||
private record StructureStepCache(Registry<Structure> registry, List<List<Structure>> structures) {
|
||||
}
|
||||
|
||||
private record NativeStructureStartKey(String structureId, long chunkPosition) {
|
||||
}
|
||||
|
||||
private record NativePlacement(StructureStart start, IrisNativeStructureDecision decision) {
|
||||
}
|
||||
|
||||
private record NativePlacementGroup(String structureId, int featureIndex, int step,
|
||||
List<NativePlacement> placements) {
|
||||
}
|
||||
|
||||
record DimensionMetadata(int minY, int maxY, int seaLevel) {
|
||||
int depth() {
|
||||
return maxY - minY;
|
||||
}
|
||||
}
|
||||
|
||||
private record ConfiguredPack(IrisData data, IrisDimension dimension, DimensionMetadata metadata) {
|
||||
}
|
||||
|
||||
static final class EngineBinding<T> {
|
||||
private final long timeout;
|
||||
private final TimeUnit timeoutUnit;
|
||||
private volatile CompletableFuture<T> future = new CompletableFuture<>();
|
||||
|
||||
EngineBinding(long timeout, TimeUnit timeoutUnit) {
|
||||
this.timeout = timeout;
|
||||
this.timeoutUnit = timeoutUnit;
|
||||
}
|
||||
|
||||
T await(String dimensionKey) {
|
||||
try {
|
||||
return future.get(timeout, timeoutUnit);
|
||||
} catch (InterruptedException error) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("Interrupted while waiting for Iris generator '"
|
||||
+ dimensionKey + "' to bind", error);
|
||||
} catch (ExecutionException error) {
|
||||
throw new IllegalStateException("Iris generator '" + dimensionKey + "' failed to bind",
|
||||
error.getCause());
|
||||
} catch (TimeoutException error) {
|
||||
throw new IllegalStateException("Timed out waiting for Iris generator '"
|
||||
+ dimensionKey + "' to bind", error);
|
||||
}
|
||||
}
|
||||
|
||||
void complete(T value) {
|
||||
future.complete(value);
|
||||
}
|
||||
|
||||
void fail(Throwable error) {
|
||||
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<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-1
@@ -45,6 +45,7 @@ import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public final class ModdedBlockResolution {
|
||||
@@ -86,6 +87,8 @@ public final class ModdedBlockResolution {
|
||||
"acacia_leaves", "birch_leaves", "dark_oak_leaves", "jungle_leaves", "oak_leaves", "spruce_leaves");
|
||||
private static final BlockState AIR = Blocks.AIR.defaultBlockState();
|
||||
private static final UnresolvedKeyLog UNRESOLVED = new UnresolvedKeyLog("Iris modded block resolution", 30_000L);
|
||||
private static final int REPORTED_FAILURE_KEYS_MAX = 256;
|
||||
private static final Set<String> REPORTED_FAILURE_KEYS = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private ModdedBlockResolution() {
|
||||
}
|
||||
@@ -152,6 +155,17 @@ public final class ModdedBlockResolution {
|
||||
return map;
|
||||
}
|
||||
|
||||
private static void reportResolveFailure(String key, Throwable error) {
|
||||
String failureKey = key == null ? "<null>" : key;
|
||||
if (!REPORTED_FAILURE_KEYS.add(failureKey)) {
|
||||
return;
|
||||
}
|
||||
if (REPORTED_FAILURE_KEYS.size() > REPORTED_FAILURE_KEYS_MAX) {
|
||||
REPORTED_FAILURE_KEYS.clear();
|
||||
}
|
||||
IrisLogging.reportError("Iris block data '" + failureKey + "' failed to resolve", error);
|
||||
}
|
||||
|
||||
private static void warnUnresolved(String key, String message) {
|
||||
if (UNRESOLVED.firstOccurrence(key)) {
|
||||
IrisLogging.warn(message);
|
||||
@@ -239,7 +253,7 @@ public final class ModdedBlockResolution {
|
||||
|
||||
return bdx;
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
reportResolveFailure(bdxf, e);
|
||||
if (warn) {
|
||||
warnUnresolved(bdxf, "Unknown Block Data '" + bdxf + "'");
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ public final class ModdedBlockState implements PlatformBlockState {
|
||||
private final String key;
|
||||
private final String namespace;
|
||||
private final String deferredPlacementKey;
|
||||
private volatile String materialKey;
|
||||
private volatile Boolean air;
|
||||
private volatile Boolean solid;
|
||||
private volatile Boolean occluding;
|
||||
@@ -148,6 +149,17 @@ public final class ModdedBlockState implements PlatformBlockState {
|
||||
return namespace;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String materialKey() {
|
||||
String cached = materialKey;
|
||||
if (cached == null) {
|
||||
int bracket = key.indexOf('[');
|
||||
cached = bracket < 0 ? key : key.substring(0, bracket);
|
||||
materialKey = cached;
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAir() {
|
||||
Boolean cached = air;
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
final class ModdedDimensionMetadata {
|
||||
private ModdedDimensionMetadata() {
|
||||
}
|
||||
|
||||
static DimensionMetadata dimensionMetadata(IrisDimension dimension) {
|
||||
int minY = dimension.getMinHeight();
|
||||
int maxY = dimension.getMaxHeight();
|
||||
if (maxY <= minY) {
|
||||
throw new IllegalStateException("Iris dimension '" + dimension.getLoadKey()
|
||||
+ "' has invalid height range " + minY + ".." + maxY);
|
||||
}
|
||||
return new DimensionMetadata(minY, maxY, minY + dimension.getFluidHeight());
|
||||
}
|
||||
|
||||
static Set<String> collectConfiguredBiomeKeys(IrisDimension dimension, IrisData data) {
|
||||
LinkedHashSet<String> keys = new LinkedHashSet<>(
|
||||
collectConfiguredBiomeKeys(dimension.getReachableBiomes(() -> data), dimension.getLoadKey()));
|
||||
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
|
||||
if (region == null) {
|
||||
continue;
|
||||
}
|
||||
if (!region.getSeaBiomes().isEmpty()) {
|
||||
keys.add("minecraft:the_void");
|
||||
}
|
||||
if (!region.getShoreBiomes().isEmpty()) {
|
||||
keys.add("minecraft:beach");
|
||||
}
|
||||
}
|
||||
return Set.copyOf(keys);
|
||||
}
|
||||
|
||||
static Set<String> collectConfiguredBiomeKeys(Iterable<IrisBiome> biomes, String dimensionLoadKey) {
|
||||
LinkedHashSet<String> keys = new LinkedHashSet<>();
|
||||
String namespace = dimensionLoadKey.toLowerCase(Locale.ROOT);
|
||||
for (IrisBiome irisBiome : biomes) {
|
||||
if (irisBiome == null) {
|
||||
continue;
|
||||
}
|
||||
Identifier derivative = Identifier.tryParse(irisBiome.getStructureDerivativeKey());
|
||||
if (derivative != null) {
|
||||
keys.add(derivative.toString().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
if (!irisBiome.isCustom()) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
keys.add(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
return Set.copyOf(keys);
|
||||
}
|
||||
|
||||
static int clampSpawnHeight(int minY, int height) {
|
||||
int minimum = minY + 1;
|
||||
int maximum = minY + height - 2;
|
||||
return Math.max(minimum, Math.min(maximum, 96));
|
||||
}
|
||||
|
||||
record DimensionMetadata(int minY, int maxY, int seaLevel) {
|
||||
int depth() {
|
||||
return maxY - minY;
|
||||
}
|
||||
}
|
||||
|
||||
record ConfiguredPack(IrisData data, IrisDimension dimension, DimensionMetadata metadata) {
|
||||
}
|
||||
}
|
||||
+32
-12
@@ -50,8 +50,12 @@ public final class ModdedDimensionRegistryStore {
|
||||
}
|
||||
|
||||
static List<PersistentDimension> load(Path file) {
|
||||
return contents(file).dimensions();
|
||||
}
|
||||
|
||||
private static Contents contents(Path file) {
|
||||
if (!Files.isRegularFile(file)) {
|
||||
return new ArrayList<>();
|
||||
return new Contents(new ArrayList<>(), new ArrayList<>());
|
||||
}
|
||||
try {
|
||||
JSONObject root = new JSONObject(Files.readString(file, StandardCharsets.UTF_8));
|
||||
@@ -60,7 +64,9 @@ public final class ModdedDimensionRegistryStore {
|
||||
throw new IllegalArgumentException("registry root has no dimensions array");
|
||||
}
|
||||
Map<String, PersistentDimension> deduplicated = new LinkedHashMap<>();
|
||||
List<Object> unparsed = new ArrayList<>();
|
||||
for (int index = 0; index < entries.length(); index++) {
|
||||
Object raw = entries.opt(index);
|
||||
try {
|
||||
JSONObject entry = entries.getJSONObject(index);
|
||||
String id = required(entry, "id", index, file);
|
||||
@@ -72,14 +78,18 @@ public final class ModdedDimensionRegistryStore {
|
||||
PersistentDimension previous = deduplicated.putIfAbsent(
|
||||
id, new PersistentDimension(id, pack, dimension, entry.getLong("seed")));
|
||||
if (previous != null) {
|
||||
throw new IllegalArgumentException("duplicate id '" + id + "'");
|
||||
LOGGER.warn("Iris persistent dimension registry entry {} in {} duplicates id '{}'; keeping the first",
|
||||
index, file, id);
|
||||
}
|
||||
} catch (RuntimeException invalidEntry) {
|
||||
LOGGER.error("Iris persistent dimension registry entry {} in {} is invalid; skipping only that entry",
|
||||
index, file, invalidEntry);
|
||||
if (raw != null) {
|
||||
unparsed.add(raw);
|
||||
}
|
||||
LOGGER.warn("Iris persistent dimension registry entry {} in {} is invalid ({}); kept verbatim: {}",
|
||||
index, file, invalidEntry.getMessage(), raw);
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(deduplicated.values());
|
||||
return new Contents(new ArrayList<>(deduplicated.values()), unparsed);
|
||||
} catch (RuntimeException | IOException e) {
|
||||
throw new IllegalStateException("Iris persistent dimension registry at " + file
|
||||
+ " could not be read; refusing to discard persistent worlds", e);
|
||||
@@ -91,15 +101,19 @@ public final class ModdedDimensionRegistryStore {
|
||||
}
|
||||
|
||||
public static synchronized void put(MinecraftServer server, PersistentDimension dimension) {
|
||||
Map<String, PersistentDimension> current = index(load(server));
|
||||
Path file = storeFile(server);
|
||||
Contents contents = contents(file);
|
||||
Map<String, PersistentDimension> current = index(contents.dimensions());
|
||||
current.put(dimension.id(), dimension);
|
||||
write(server, new ArrayList<>(current.values()));
|
||||
write(file, new ArrayList<>(current.values()), contents.unparsed());
|
||||
}
|
||||
|
||||
public static synchronized void remove(MinecraftServer server, String id) {
|
||||
Map<String, PersistentDimension> current = index(load(server));
|
||||
Path file = storeFile(server);
|
||||
Contents contents = contents(file);
|
||||
Map<String, PersistentDimension> current = index(contents.dimensions());
|
||||
if (current.remove(id) != null) {
|
||||
write(server, new ArrayList<>(current.values()));
|
||||
write(file, new ArrayList<>(current.values()), contents.unparsed());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,11 +133,11 @@ public final class ModdedDimensionRegistryStore {
|
||||
return value;
|
||||
}
|
||||
|
||||
private static void write(MinecraftServer server, List<PersistentDimension> dimensions) {
|
||||
write(storeFile(server), dimensions);
|
||||
static void write(Path file, List<PersistentDimension> dimensions) {
|
||||
write(file, dimensions, List.of());
|
||||
}
|
||||
|
||||
static void write(Path file, List<PersistentDimension> dimensions) {
|
||||
private static void write(Path file, List<PersistentDimension> dimensions, List<Object> unparsed) {
|
||||
JSONArray entries = new JSONArray();
|
||||
for (PersistentDimension dimension : dimensions) {
|
||||
JSONObject entry = new JSONObject();
|
||||
@@ -133,6 +147,9 @@ public final class ModdedDimensionRegistryStore {
|
||||
entry.put("seed", dimension.seed());
|
||||
entries.put(entry);
|
||||
}
|
||||
for (Object entry : unparsed) {
|
||||
entries.put(entry);
|
||||
}
|
||||
JSONObject root = new JSONObject();
|
||||
root.put("dimensions", entries);
|
||||
Path temp = file.resolveSibling(FILE_NAME + ".tmp");
|
||||
@@ -167,4 +184,7 @@ public final class ModdedDimensionRegistryStore {
|
||||
|
||||
public record PersistentDimension(String id, String pack, String dimension, long seed) {
|
||||
}
|
||||
|
||||
private record Contents(List<PersistentDimension> dimensions, List<Object> unparsed) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
final class ModdedEngineBinding<T> {
|
||||
private final long timeout;
|
||||
private final TimeUnit timeoutUnit;
|
||||
private volatile CompletableFuture<T> future = new CompletableFuture<>();
|
||||
|
||||
ModdedEngineBinding(long timeout, TimeUnit timeoutUnit) {
|
||||
this.timeout = timeout;
|
||||
this.timeoutUnit = timeoutUnit;
|
||||
}
|
||||
|
||||
T await(String dimensionKey) {
|
||||
try {
|
||||
return future.get(timeout, timeoutUnit);
|
||||
} catch (InterruptedException error) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("Interrupted while waiting for Iris generator '"
|
||||
+ dimensionKey + "' to bind", error);
|
||||
} catch (ExecutionException error) {
|
||||
throw new IllegalStateException("Iris generator '" + dimensionKey + "' failed to bind",
|
||||
error.getCause());
|
||||
} catch (TimeoutException error) {
|
||||
throw new IllegalStateException("Timed out waiting for Iris generator '"
|
||||
+ dimensionKey + "' to bind", error);
|
||||
}
|
||||
}
|
||||
|
||||
void complete(T value) {
|
||||
future.complete(value);
|
||||
}
|
||||
|
||||
void fail(Throwable error) {
|
||||
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<>();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -342,7 +342,7 @@ public final class ModdedEntitySpawner {
|
||||
double addition = RNG.r.d();
|
||||
double subtraction = RNG.r.d();
|
||||
double particleX = entity.getX() + addition - subtraction + RNG.r.d();
|
||||
double particleY = entity.getY() + 0.25 + addition - subtraction + level.getMinY() + RNG.r.i(effect.getParticleOffset());
|
||||
double particleY = entity.getY() + 0.25 + addition - subtraction + RNG.r.i(effect.getParticleOffset());
|
||||
double particleZ = entity.getZ() + addition - subtraction + RNG.r.d();
|
||||
double altX = effect.isRandomAltX() ? RNG.r.d(-effect.getParticleAltX(), effect.getParticleAltX()) : effect.getParticleAltX();
|
||||
double altY = effect.isRandomAltY() ? RNG.r.d(-effect.getParticleAltY(), effect.getParticleAltY()) : effect.getParticleAltY();
|
||||
|
||||
+22
-1
@@ -55,6 +55,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -63,6 +64,7 @@ public final class ModdedForcedDatapack {
|
||||
private static final String PACK_ID = "iris_worldgen";
|
||||
private static final String PACK_FOLDER = "iris";
|
||||
private static final Object LOCK = new Object();
|
||||
private static final AtomicBoolean LOADED = new AtomicBoolean(false);
|
||||
|
||||
private ModdedForcedDatapack() {
|
||||
}
|
||||
@@ -71,9 +73,26 @@ public final class ModdedForcedDatapack {
|
||||
return (Consumer<Pack> consumer) -> {
|
||||
Pack pack = buildPack();
|
||||
consumer.accept(pack);
|
||||
LOADED.set(true);
|
||||
};
|
||||
}
|
||||
|
||||
public static void verifyInjected() {
|
||||
if (LOADED.get()) {
|
||||
return;
|
||||
}
|
||||
Path packsRoot = packsRoot();
|
||||
File[] packs = packsRoot.toFile().listFiles(File::isDirectory);
|
||||
if (packs == null || packs.length == 0) {
|
||||
return;
|
||||
}
|
||||
LOGGER.error("===============================================================");
|
||||
LOGGER.error("Iris forced datapack '{}' was never loaded by this server.", PACK_ID);
|
||||
LOGGER.error("{} installed pack(s) at {} contributed no dimension types or custom biomes.", packs.length, packsRoot);
|
||||
LOGGER.error("Datapack source injection failed for this loader (mixin/event not applied), so world creation will fail and restarting will not fix it.");
|
||||
LOGGER.error("===============================================================");
|
||||
}
|
||||
|
||||
public static Path datapackRoot() {
|
||||
return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("generated").resolve("datapack");
|
||||
}
|
||||
@@ -310,7 +329,9 @@ public final class ModdedForcedDatapack {
|
||||
String pack, String packDimensionKey) {
|
||||
return registeredType.orElseThrow(() -> new IllegalStateException(
|
||||
"Iris dimension type '" + typeRef + "' for pack '" + pack + "' dimension '"
|
||||
+ packDimensionKey + "' is not loaded. Restart the server so the forced Iris datapack registers it before creating the world."));
|
||||
+ packDimensionKey + "' is not loaded. Restart the server so the forced Iris datapack registers it before creating the world."
|
||||
+ (LOADED.get() ? "" : " The forced Iris datapack has not been loaded by this server at all"
|
||||
+ " (datapack source injection failed; see the Iris boot ERROR), so a restart alone will not register it.")));
|
||||
}
|
||||
|
||||
private static void writeWorldPreset(KList<File> folders, String packName, String dimensionKey,
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
final class ModdedGenPool {
|
||||
private static final AtomicInteger GEN_THREAD_SEQ = new AtomicInteger();
|
||||
private static final boolean PARALLEL_CHUNK_SYSTEM = detectParallelChunkSystem();
|
||||
private static volatile ExecutorService genPool = createGenPool();
|
||||
|
||||
private ModdedGenPool() {
|
||||
}
|
||||
|
||||
static boolean parallelChunkSystem() {
|
||||
return PARALLEL_CHUNK_SYSTEM;
|
||||
}
|
||||
|
||||
static ExecutorService pool() {
|
||||
return genPool;
|
||||
}
|
||||
|
||||
static void start() {
|
||||
ExecutorService pool = genPool;
|
||||
if (pool == null || pool.isShutdown()) {
|
||||
genPool = createGenPool();
|
||||
}
|
||||
}
|
||||
|
||||
static void shutdown() {
|
||||
ExecutorService pool = genPool;
|
||||
if (pool != null) {
|
||||
pool.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean detectParallelChunkSystem() {
|
||||
String[] markers = {
|
||||
"com.ishland.c2me.base.ModProperties",
|
||||
"com.ishland.c2me.base.common.config.C2MEConfig",
|
||||
"com.ishland.c2me.opts.chunkio.ModProperties",
|
||||
"ca.spottedleaf.moonrise.common.util.MoonriseCommon"
|
||||
};
|
||||
for (String marker : markers) {
|
||||
try {
|
||||
Class.forName(marker, false, IrisModdedChunkGenerator.class.getClassLoader());
|
||||
return true;
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ExecutorService createGenPool() {
|
||||
int threads = Math.max(2, Runtime.getRuntime().availableProcessors());
|
||||
ThreadPoolExecutor pool = new ThreadPoolExecutor(
|
||||
threads, threads, 30L, TimeUnit.SECONDS,
|
||||
new LinkedBlockingQueue<>(),
|
||||
runnable -> {
|
||||
Thread thread = new Thread(runnable, "Iris ModGen-" + GEN_THREAD_SEQ.incrementAndGet());
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
pool.allowCoreThreadTimeOut(true);
|
||||
return pool;
|
||||
}
|
||||
}
|
||||
@@ -43,10 +43,11 @@ public final class ModdedIrisLog {
|
||||
|
||||
public static void debug(String message) {
|
||||
if (!debugEnabled()) {
|
||||
LOGGER.debug(clean(message));
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.debug(clean(message));
|
||||
LOGGER.info("[Iris/DEBUG] " + clean(message));
|
||||
}
|
||||
|
||||
public static void info(String message) {
|
||||
|
||||
@@ -267,15 +267,21 @@ public final class ModdedLootApplier {
|
||||
for (int i = 0; i < container.getContainerSize() && !stack.isEmpty(); i++) {
|
||||
ItemStack existing = container.getItem(i);
|
||||
if (existing.isEmpty()) {
|
||||
container.setItem(i, stack);
|
||||
return;
|
||||
container.setItem(i, stack.split(container.getMaxStackSize(stack)));
|
||||
continue;
|
||||
}
|
||||
if (ItemStack.isSameItemSameComponents(existing, stack) && existing.getCount() < existing.getMaxStackSize()) {
|
||||
int move = Math.min(stack.getCount(), existing.getMaxStackSize() - existing.getCount());
|
||||
existing.grow(move);
|
||||
stack.shrink(move);
|
||||
if (ItemStack.isSameItemSameComponents(existing, stack)) {
|
||||
int limit = container.getMaxStackSize(existing);
|
||||
if (existing.getCount() < limit) {
|
||||
int move = Math.min(stack.getCount(), limit - existing.getCount());
|
||||
existing.grow(move);
|
||||
stack.shrink(move);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!stack.isEmpty()) {
|
||||
IrisLogging.debug("Iris loot: container full, dropped " + stack.getCount() + "x " + stack.getItem());
|
||||
}
|
||||
}
|
||||
|
||||
private static void scramble(Container container, RNG rng) {
|
||||
|
||||
+439
@@ -0,0 +1,439 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.engine.framework.NativeStructurePlacementPlanner;
|
||||
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
|
||||
import art.arcane.iris.engine.object.IrisMaterialPalette;
|
||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.nativegen.NativeStructureGenerationException;
|
||||
import art.arcane.iris.nativegen.NativeStructurePostProcessor;
|
||||
import art.arcane.iris.nativegen.NativeStructureReferenceEnvelope;
|
||||
import art.arcane.iris.nativegen.NativeStructureSurfaceFitter;
|
||||
import art.arcane.iris.nativegen.NativeStructureTerrainIntegrator;
|
||||
import art.arcane.iris.nativegen.NativeStructureVegetationClearer;
|
||||
import art.arcane.iris.nativegen.NativeStructureVerticalPlacer;
|
||||
import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
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.RegistryAccess;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||
import net.minecraft.world.level.levelgen.RandomSupport;
|
||||
import net.minecraft.world.level.levelgen.WorldgenRandom;
|
||||
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
|
||||
/**
|
||||
* Native (vanilla registry) structure stage for {@link IrisModdedChunkGenerator}. The generator keeps the
|
||||
* {@link net.minecraft.world.level.chunk.ChunkGenerator} overrides because they issue {@code super} calls;
|
||||
* everything they do beyond that lives here.
|
||||
*/
|
||||
final class ModdedNativeStructureStage {
|
||||
private static final int WORLD_CHECK_SHIFT_RECORD_LIMIT = 4096;
|
||||
private static final boolean WORLD_CHECK_ENABLED = Boolean.getBoolean("iris.worldcheck");
|
||||
|
||||
private final IrisModdedChunkGenerator generator;
|
||||
private final ConcurrentHashMap<NativeStructureStartKey, Integer> worldCheckStructureShifts = new ConcurrentHashMap<>();
|
||||
private volatile StructureStepCache structureStepCache;
|
||||
|
||||
ModdedNativeStructureStage(IrisModdedChunkGenerator generator) {
|
||||
this.generator = generator;
|
||||
}
|
||||
|
||||
Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(ServerLevel level,
|
||||
HolderSet<Structure> holders,
|
||||
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;
|
||||
long bestDistance = Long.MAX_VALUE;
|
||||
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();
|
||||
if (!IrisStructureLocator.isPlaced(current, structureId)) {
|
||||
continue;
|
||||
}
|
||||
IrisStructureLocator.LocateResult result = IrisStructureLocator.locate(
|
||||
current, structureId, pos.getX(), pos.getZ(), radius);
|
||||
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
|
||||
throw new IllegalStateException("Iris structure locate reached its safety limit for "
|
||||
+ structureId + " within " + radius + " chunks");
|
||||
}
|
||||
if (!result.found()) {
|
||||
continue;
|
||||
}
|
||||
long dx = (long) result.originX() - pos.getX();
|
||||
long dz = (long) result.originZ() - pos.getZ();
|
||||
long distance = dx * dx + dz * dz;
|
||||
if (distance < bestDistance) {
|
||||
bestDistance = distance;
|
||||
best = new BlockPos(result.originX(), result.baseY(), result.originZ());
|
||||
bestHolder = holder;
|
||||
}
|
||||
}
|
||||
return best == null ? null : Pair.of(best, bestHolder);
|
||||
}
|
||||
|
||||
HolderSet<Structure> filterReachableNativeStructures(ServerLevel level, HolderSet<Structure> holders,
|
||||
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");
|
||||
}
|
||||
String key = id.toString();
|
||||
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(current,
|
||||
key, NativeStructureVegetationClearer.isUndergroundStep(holder.value().step()));
|
||||
if (!decision.generate() || !generator.structureBiomeSource.isStructureReachable(holder)) {
|
||||
continue;
|
||||
}
|
||||
kept.add(holder);
|
||||
}
|
||||
return kept.size() == holders.size() ? holders : HolderSet.direct(kept);
|
||||
}
|
||||
|
||||
void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess chunk,
|
||||
Map<Structure, StructureStart> previousStarts,
|
||||
Map<Structure, NativeStructureStartPlan> configuredStarts,
|
||||
Engine current,
|
||||
StructureTemplateManager templateManager) {
|
||||
Registry<Structure> registry = registryAccess.lookupOrThrow(Registries.STRUCTURE);
|
||||
ChunkPos chunkPos = chunk.getPos();
|
||||
for (Map.Entry<Structure, StructureStart> entry : chunk.getAllStarts().entrySet()) {
|
||||
Structure structure = entry.getKey();
|
||||
StructureStart start = entry.getValue();
|
||||
if (!start.isValid() || previousStarts.get(structure) == start) {
|
||||
continue;
|
||||
}
|
||||
if (configuredStarts.containsKey(structure)) {
|
||||
recordWorldCheckStructureShift(
|
||||
configuredStarts.get(structure).source().getStructure(), start.getChunkPos(), 0);
|
||||
continue;
|
||||
}
|
||||
Identifier id = registry.getKey(structure);
|
||||
String structureId = id == null ? null : id.toString();
|
||||
if (structureId == null) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"resolution", null, chunkPos.x(), chunkPos.z());
|
||||
}
|
||||
boolean undergroundStep = NativeStructureVegetationClearer.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 = NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||
start,
|
||||
structureId,
|
||||
decision.yShift(),
|
||||
generator.getSeaLevel(),
|
||||
chunk.getMinY(),
|
||||
chunk.getMinY() + chunk.getHeight(),
|
||||
undergroundStep,
|
||||
decision.preserveSourceY(),
|
||||
decision.yBand(),
|
||||
(x, z) -> current.getHeight(x, z, true) + current.getMinHeight());
|
||||
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
|
||||
start, structure, start.getReferences(), templateManager,
|
||||
NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain()));
|
||||
chunk.setStartForStructure(structure, wrapped);
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"vertical adjustment", structureId, chunkPos.x(), chunkPos.z(), error);
|
||||
}
|
||||
recordWorldCheckStructureShift(structureId, start.getChunkPos(), offsetY);
|
||||
}
|
||||
}
|
||||
|
||||
void placeVanillaStructures(WorldGenLevel world, ChunkAccess chunk, StructureManager structureManager) {
|
||||
if (!structureManager.shouldGenerateStructures()) {
|
||||
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());
|
||||
BlockPos origin = sectionPos.origin();
|
||||
Registry<Structure> registry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
List<List<Structure>> byStep = structuresByStep(registry);
|
||||
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
|
||||
long decorationSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ());
|
||||
BoundingBox area = writableArea(chunk);
|
||||
int steps = GenerationStep.Decoration.values().length;
|
||||
Engine current = generator.engine();
|
||||
List<NativePlacementGroup> placementGroups = new ArrayList<>();
|
||||
List<StructureStart> heightmapStarts = new ArrayList<>();
|
||||
List<StructureStart> nativeStarts = new ArrayList<>();
|
||||
List<NativeStructureVegetationClearer.VegetationTarget> vegetationTargets = new ArrayList<>();
|
||||
List<NativeStructureTerrainIntegrator.TerrainTarget> terrainTargets = 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();
|
||||
if (structureId == null) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"resolution", null, chunkPos.x(), chunkPos.z());
|
||||
}
|
||||
try {
|
||||
IrisNativeStructureDecision sourceDecision = NativeStructureGenerationPolicy.resolve(current,
|
||||
structureId, NativeStructureVegetationClearer.isUndergroundStep(structure.step()));
|
||||
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
|
||||
List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size());
|
||||
for (StructureStart start : starts) {
|
||||
NativeStructureStartPlan plan = NativeStructurePlacementPlanner.matchingPlan(
|
||||
current, structureId, start.getChunkPos().x(), start.getChunkPos().z());
|
||||
IrisNativeStructureDecision decision = plan == null
|
||||
? sourceDecision : NativeStructurePlacementPlanner.decisionFor(plan);
|
||||
if (!decision.generate()) {
|
||||
continue;
|
||||
}
|
||||
resolvedPlacements.add(new NativePlacement(start, decision));
|
||||
heightmapStarts.add(start);
|
||||
terrainTargets.add(new NativeStructureTerrainIntegrator.TerrainTarget(
|
||||
structureId, start,
|
||||
NativeStructureTerrainIntegrator.resolveNativeTerrain(
|
||||
start, decision.terrain())));
|
||||
if (plan == null || !plan.placement().isUnderground()) {
|
||||
nativeStarts.add(start);
|
||||
}
|
||||
boolean clearEntireFootprint = NativeStructureVegetationClearer
|
||||
.shouldClearEntireVegetationFootprint(
|
||||
structure.step(), decision.clearVegetation());
|
||||
vegetationTargets.add(new NativeStructureVegetationClearer.VegetationTarget(
|
||||
start, clearEntireFootprint));
|
||||
}
|
||||
if (!resolvedPlacements.isEmpty()) {
|
||||
placementGroups.add(new NativePlacementGroup(
|
||||
structureId, index, step, List.copyOf(resolvedPlacements)));
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"resolution", structureId, chunkPos.x(), chunkPos.z(), error);
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
try {
|
||||
int runtimeMinY = world.getMinY();
|
||||
WorldgenTerrainHeightmaps.primeStructurePlacement(
|
||||
world, heightmapStarts,
|
||||
worldgenSurfaceHeight(current, runtimeMinY),
|
||||
worldgenFloorHeight(current, runtimeMinY));
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"heightmap priming", nativeStructureBatchContext(placementGroups),
|
||||
chunkPos.x(), chunkPos.z(), error);
|
||||
}
|
||||
try {
|
||||
NativeStructureSurfaceFitter.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 {
|
||||
NativeStructureVegetationClearer.clearIntersectingVegetation(
|
||||
world, chunk, area, vegetationTargets);
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"vegetation cleanup", nativeStructureBatchContext(placementGroups),
|
||||
chunkPos.x(), chunkPos.z(), error);
|
||||
}
|
||||
try {
|
||||
NativeStructurePostProcessor.prepareTerrain(
|
||||
world, area, terrainTargets, this::resolvePaletteBlock);
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"terrain carving", nativeStructureBatchContext(placementGroups),
|
||||
chunkPos.x(), chunkPos.z(), error);
|
||||
}
|
||||
for (NativePlacementGroup group : placementGroups) {
|
||||
random.setFeatureSeed(decorationSeed, group.featureIndex(), group.step());
|
||||
try {
|
||||
for (NativePlacement placement : group.placements()) {
|
||||
placeVanillaStructure(world, structureManager, random, area, chunkPos,
|
||||
group.structureId(), placement.start(), placement.decision());
|
||||
}
|
||||
} 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,
|
||||
IrisNativeStructureDecision decision) {
|
||||
NativeStructurePostProcessor.place(world, structureManager, generator, random, area, chunkPos,
|
||||
structureId, start, decision, this::resolvePaletteBlock,
|
||||
(x, z) -> generator.engine().getHeight(x, z, true) + generator.engine().getMinHeight());
|
||||
}
|
||||
|
||||
private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
|
||||
StructureStepCache cached = structureStepCache;
|
||||
if (cached != null && cached.registry() == registry) {
|
||||
return cached.structures();
|
||||
}
|
||||
synchronized (generator) {
|
||||
cached = structureStepCache;
|
||||
if (cached != null && cached.registry() == registry) {
|
||||
return cached.structures();
|
||||
}
|
||||
int steps = GenerationStep.Decoration.values().length;
|
||||
List<List<Structure>> grouped = new ArrayList<>(steps);
|
||||
for (int step = 0; step < steps; step++) {
|
||||
grouped.add(new ArrayList<>());
|
||||
}
|
||||
for (Structure structure : registry) {
|
||||
grouped.get(structure.step().ordinal()).add(structure);
|
||||
}
|
||||
for (int step = 0; step < steps; step++) {
|
||||
grouped.set(step, List.copyOf(grouped.get(step)));
|
||||
}
|
||||
List<List<Structure>> resolved = List.copyOf(grouped);
|
||||
structureStepCache = new StructureStepCache(registry, resolved);
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
private void recordWorldCheckStructureShift(String structureId, ChunkPos startChunk, int offsetY) {
|
||||
if (!WORLD_CHECK_ENABLED || structureId == null) {
|
||||
return;
|
||||
}
|
||||
if (worldCheckStructureShifts.size() >= WORLD_CHECK_SHIFT_RECORD_LIMIT) {
|
||||
worldCheckStructureShifts.clear();
|
||||
}
|
||||
worldCheckStructureShifts.put(new NativeStructureStartKey(structureId, startChunk.pack()), offsetY);
|
||||
}
|
||||
|
||||
Integer worldCheckStructureShift(String structureId, ChunkPos startChunk) {
|
||||
if (structureId == null || startChunk == null) {
|
||||
return null;
|
||||
}
|
||||
return worldCheckStructureShifts.get(new NativeStructureStartKey(structureId, startChunk.pack()));
|
||||
}
|
||||
|
||||
void clearWorldCheckStructureShifts() {
|
||||
worldCheckStructureShifts.clear();
|
||||
}
|
||||
|
||||
private BlockState resolvePaletteBlock(IrisMaterialPalette palette, RNG rng,
|
||||
int x, int y, int z) {
|
||||
PlatformBlockState platformState = palette.get(rng, x, y, z, generator.engine().getData());
|
||||
if (platformState == null || !(platformState.nativeHandle() instanceof BlockState blockState)) {
|
||||
throw new IllegalStateException("Configured native structure palette did not resolve a Minecraft block at "
|
||||
+ x + "," + y + "," + z);
|
||||
}
|
||||
return blockState;
|
||||
}
|
||||
|
||||
private BoundingBox writableArea(ChunkAccess chunk) {
|
||||
ChunkPos chunkPos = chunk.getPos();
|
||||
int minX = chunkPos.getMinBlockX();
|
||||
int minZ = chunkPos.getMinBlockZ();
|
||||
int minY = chunk.getMinY();
|
||||
int maxY = minY + chunk.getHeight() - 1;
|
||||
return new BoundingBox(minX, minY, minZ, minX + 15, maxY, minZ + 15);
|
||||
}
|
||||
|
||||
private IntBinaryOperator worldgenSurfaceHeight(Engine generationEngine, int runtimeMinY) {
|
||||
return (x, z) -> generationEngine.getHeight(x, z, false) + runtimeMinY + 1;
|
||||
}
|
||||
|
||||
private IntBinaryOperator worldgenFloorHeight(Engine generationEngine, int runtimeMinY) {
|
||||
return (x, z) -> generationEngine.getHeight(x, z, true) + runtimeMinY + 1;
|
||||
}
|
||||
|
||||
private record NativeStructureStartKey(String structureId, long chunkPosition) {
|
||||
}
|
||||
|
||||
private record NativePlacement(StructureStart start, IrisNativeStructureDecision decision) {
|
||||
}
|
||||
|
||||
private record NativePlacementGroup(String structureId, int featureIndex, int step,
|
||||
List<NativePlacement> placements) {
|
||||
}
|
||||
|
||||
private record StructureStepCache(Registry<Structure> registry, List<List<Structure>> structures) {
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import art.arcane.iris.spi.PlatformEntityType;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.spi.PlatformScheduler;
|
||||
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||
import art.arcane.iris.spi.PlatformWorld;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
@@ -141,8 +142,8 @@ public final class ModdedPlatform implements IrisPlatform {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean spawnEntity(Object world, String entityKey, double x, double y, double z) {
|
||||
if (!(world instanceof ServerLevel level) || entityKey == null) {
|
||||
public boolean spawnEntity(PlatformWorld world, String entityKey, double x, double y, double z) {
|
||||
if (world == null || entityKey == null || !(world.nativeHandle() instanceof ServerLevel level)) {
|
||||
return false;
|
||||
}
|
||||
PlatformEntityType resolved = registries.entity(entityKey);
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.util.random.WeightedList;
|
||||
import net.minecraft.world.entity.MobCategory;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.MobSpawnSettings;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Vanilla-derivative spawn table state for {@link IrisModdedChunkGenerator}. Every mutator locks the
|
||||
* generator monitor because the repoint/bind/reset paths already hold it while resetting this state.
|
||||
*/
|
||||
final class ModdedSpawnTableMerger {
|
||||
private final IrisModdedChunkGenerator generator;
|
||||
private final ConcurrentHashMap<Biome, Holder<Biome>> vanillaSpawnBiomes = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
|
||||
private volatile boolean vanillaSpawnBiomesInitialized;
|
||||
|
||||
ModdedSpawnTableMerger(IrisModdedChunkGenerator generator) {
|
||||
this.generator = generator;
|
||||
}
|
||||
|
||||
void initializeVanillaSpawnBiomes(Registry<Biome> registry) {
|
||||
synchronized (generator) {
|
||||
if (vanillaSpawnBiomesInitialized) {
|
||||
return;
|
||||
}
|
||||
Engine current = generator.engineOrNull();
|
||||
if (current == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (GenerationSessionLease lease = generator.requireGenerationLease(current, "modded_spawn_biomes");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
String namespace = current.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
|
||||
for (IrisBiome irisBiome : current.getDimension().getReachableBiomes(current)) {
|
||||
if (irisBiome == null || !irisBiome.isCustom()) {
|
||||
continue;
|
||||
}
|
||||
Holder<Biome> vanillaHolder = resolveBiomeHolder(registry, irisBiome.getVanillaDerivativeKey());
|
||||
if (vanillaHolder == null) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
Holder<Biome> customHolder = resolveBiomeHolder(registry, namespace + ":" + customBiome.getId());
|
||||
if (customHolder != null) {
|
||||
vanillaSpawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
vanillaSpawnBiomesInitialized = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Holder<Biome> vanillaSpawnBiome(Biome biome) {
|
||||
return vanillaSpawnBiomes.get(biome);
|
||||
}
|
||||
|
||||
WeightedList<MobSpawnSettings.SpawnerData> mergedSpawnTable(
|
||||
Biome biome, MobCategory category,
|
||||
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns,
|
||||
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns) {
|
||||
SpawnTableKey key = new SpawnTableKey(biome, category);
|
||||
return mergedSpawnTables.computeIfAbsent(key, ignored -> NativeSpawnTableMerger.merge(vanillaSpawns, explicitSpawns));
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String key) {
|
||||
if (key == null || key.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
Identifier identifier = Identifier.tryParse(key);
|
||||
if (identifier == null) {
|
||||
return null;
|
||||
}
|
||||
Optional<Holder.Reference<Biome>> reference = registry.get(identifier);
|
||||
return reference.<Holder<Biome>>map((Holder.Reference<Biome> value) -> value).orElse(null);
|
||||
}
|
||||
|
||||
void resetVanillaSpawnBiomes() {
|
||||
synchronized (generator) {
|
||||
vanillaSpawnBiomes.clear();
|
||||
mergedSpawnTables.clear();
|
||||
vanillaSpawnBiomesInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
private record SpawnTableKey(Biome biome, MobCategory category) {
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,7 @@ public final class ModdedStartup {
|
||||
if (!STARTED.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
ModdedForcedDatapack.verifyInjected();
|
||||
reinjectPersistentDimensions(server);
|
||||
|
||||
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
|
||||
@@ -88,6 +89,8 @@ public final class ModdedStartup {
|
||||
File[] packDirs = packsRoot.listFiles(File::isDirectory);
|
||||
PackValidationRegistry.clear();
|
||||
if (packDirs == null || packDirs.length == 0) {
|
||||
LOGGER.info("Iris found no packs to validate under {}; install one with /iris download <pack>",
|
||||
packsRoot.getAbsolutePath());
|
||||
return;
|
||||
}
|
||||
for (File packDir : packDirs) {
|
||||
|
||||
+16
-1
@@ -182,13 +182,28 @@ public final class ModdedStateRotator implements IrisObjectRotation.StateRotator
|
||||
|
||||
private static Property<?> findRotation(BlockState state) {
|
||||
for (Property<?> property : state.getProperties()) {
|
||||
if (property.getName().equals("rotation") && property instanceof IntegerProperty) {
|
||||
if (property.getName().equals("rotation")
|
||||
&& property instanceof IntegerProperty integer
|
||||
&& isFullRotationCycle(integer)) {
|
||||
return property;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isFullRotationCycle(IntegerProperty property) {
|
||||
List<Integer> values = property.getPossibleValues();
|
||||
if (values.size() != ROTATION_CYCLE_MODS.length) {
|
||||
return false;
|
||||
}
|
||||
for (int value : values) {
|
||||
if (value < 0 || value >= ROTATION_CYCLE_MODS.length) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Property<?> findAxis(BlockState state) {
|
||||
for (Property<?> property : state.getProperties()) {
|
||||
if (property.getName().equals("axis") && property.getValueClass() == Direction.Axis.class) {
|
||||
|
||||
@@ -41,7 +41,7 @@ import java.util.Locale;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public final class ModdedTileReader implements TileData.TileReader {
|
||||
private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().setStrictness(Strictness.LENIENT).create();
|
||||
private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().setStrictness(Strictness.LENIENT).setObjectToNumberStrategy(com.google.gson.ToNumberPolicy.LONG_OR_DOUBLE).create();
|
||||
private static final int DYE_COLOR_COUNT = 16;
|
||||
private static final Identifier DEFAULT_SPAWNER_ENTITY = Identifier.parse("minecraft:pig");
|
||||
private static final Identifier DEFAULT_BANNER_PATTERN = Identifier.parse("minecraft:base");
|
||||
|
||||
+19
-1041
File diff suppressed because it is too large
Load Diff
@@ -100,8 +100,8 @@ public final class ModdedWorldEngines {
|
||||
|
||||
private static Engine create(ServerLevel level, String pack, String dimensionKey, long seedOverride) {
|
||||
ModdedEngineBootstrap.bind();
|
||||
PackValidationRegistry.requireLoadable(pack);
|
||||
File packDir = resolvePack(pack, dimensionKey);
|
||||
PackValidationRegistry.requireLoadable(pack);
|
||||
IrisData data = IrisData.openRuntime(packDir);
|
||||
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
|
||||
if (dimension == null) {
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
final class WorldCheckDimensionContract {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
|
||||
private WorldCheckDimensionContract() {
|
||||
}
|
||||
|
||||
static boolean checkDimensionType(ServerLevel level, IrisModdedChunkGenerator generator) {
|
||||
try {
|
||||
IrisDimension dimension = generator.commandEngine().getDimension();
|
||||
DimensionContract expected = expectedDimensionContract(dimension);
|
||||
DimensionContract actual = runtimeDimensionContract(level.dimensionType());
|
||||
boolean pass = matchesDimensionContract(level.getMinY(), level.getHeight(), expected, actual);
|
||||
String detail = "expected=" + expected + ",actual=" + actual
|
||||
+ ",levelMinY=" + level.getMinY() + ",levelHeight=" + level.getHeight();
|
||||
WorldCheckPredicates.qaEvent("dimension_type", dimension.getLoadKey(), pass, detail);
|
||||
if (!pass) {
|
||||
LOGGER.error("[worldcheck] dimension type mismatch for {}: {}", dimension.getLoadKey(), detail);
|
||||
} else {
|
||||
LOGGER.info("[worldcheck] dimension type contract: {}", detail);
|
||||
}
|
||||
return pass;
|
||||
} catch (Throwable error) {
|
||||
LOGGER.error("[worldcheck] could not validate the Iris dimension type contract", error);
|
||||
WorldCheckPredicates.qaEvent("dimension_type", generator.activeDimensionKey(), false,
|
||||
"validationError=" + error.getClass().getSimpleName() + ":" + error.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static DimensionContract expectedDimensionContract(IrisDimension dimension) {
|
||||
JSONObject json = new JSONObject(dimension.getDimensionType().toJson(DataVersion.getLatest().get()));
|
||||
return new DimensionContract(
|
||||
json.getInt("min_y"),
|
||||
json.getInt("height"),
|
||||
json.getInt("logical_height"),
|
||||
json.getDouble("coordinate_scale"),
|
||||
(float) json.getDouble("ambient_light"),
|
||||
json.getBoolean("has_skylight"),
|
||||
json.getBoolean("has_ceiling"),
|
||||
json.getBoolean("has_ender_dragon_fight"),
|
||||
json.getInt("monster_spawn_block_light_limit"));
|
||||
}
|
||||
|
||||
static DimensionContract runtimeDimensionContract(DimensionType dimensionType) {
|
||||
return new DimensionContract(
|
||||
dimensionType.minY(),
|
||||
dimensionType.height(),
|
||||
dimensionType.logicalHeight(),
|
||||
dimensionType.coordinateScale(),
|
||||
dimensionType.ambientLight(),
|
||||
dimensionType.hasSkyLight(),
|
||||
dimensionType.hasCeiling(),
|
||||
dimensionType.hasEnderDragonFight(),
|
||||
dimensionType.monsterSpawnBlockLightLimit());
|
||||
}
|
||||
|
||||
static boolean matchesDimensionContract(int levelMinY, int levelHeight,
|
||||
DimensionContract expected, DimensionContract actual) {
|
||||
return levelMinY == expected.minY()
|
||||
&& levelHeight == expected.height()
|
||||
&& actual.equals(expected);
|
||||
}
|
||||
|
||||
static boolean checkEntityMixins(ServerLevel level) {
|
||||
ItemEntity item = new ItemEntity(level, 0D, level.getMinY(), 0D, Items.COBBLESTONE.getDefaultInstance());
|
||||
boolean vanillaSave = item.shouldBeSaved();
|
||||
ModdedEntityPersistence.configure(item, false);
|
||||
boolean suppressed = !item.shouldBeSaved();
|
||||
ModdedEntityPersistence.configure(item, true);
|
||||
boolean restored = item.shouldBeSaved();
|
||||
boolean pass = vanillaSave && suppressed && restored;
|
||||
WorldCheckPredicates.qaEvent("entity_mixin", "persistence", pass,
|
||||
"vanilla=" + vanillaSave + ",suppressed=" + suppressed + ",restored=" + restored);
|
||||
if (!pass) {
|
||||
LOGGER.error("[worldcheck] shared entity mixins are not active on this loader");
|
||||
}
|
||||
return pass;
|
||||
}
|
||||
|
||||
record DimensionContract(int minY, int height, int logicalHeight, double coordinateScale,
|
||||
float ambientLight, boolean hasSkyLight, boolean hasCeiling,
|
||||
boolean hasEnderDragonFight, int monsterSpawnBlockLightLimit) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
final class WorldCheckMaterials {
|
||||
private WorldCheckMaterials() {
|
||||
}
|
||||
|
||||
static boolean isCharacteristicMaterial(String structureLabel, Identifier structureKey, Identifier blockKey) {
|
||||
if (structureKey == null || blockKey == null || !blockKey.getNamespace().equals("minecraft")) {
|
||||
return false;
|
||||
}
|
||||
String block = blockKey.getPath();
|
||||
return switch (structureLabel) {
|
||||
case "stronghold" -> isStrongholdMaterial(block);
|
||||
case "trial_chambers" -> isTrialChamberMaterial(block);
|
||||
case "mansion" -> isWoodConstructionMaterial(block, "dark_oak")
|
||||
|| isWoodConstructionMaterial(block, "birch")
|
||||
|| isCobblestoneConstructionMaterial(block);
|
||||
case "village" -> isVillageMaterial(structureKey.getPath(), block);
|
||||
case "monument" -> isMonumentMaterial(block);
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean isStrongholdMaterial(String block) {
|
||||
return block.equals("stone_bricks")
|
||||
|| block.equals("cracked_stone_bricks")
|
||||
|| block.equals("mossy_stone_bricks")
|
||||
|| block.equals("infested_stone_bricks")
|
||||
|| block.equals("infested_cracked_stone_bricks")
|
||||
|| block.equals("infested_mossy_stone_bricks")
|
||||
|| block.equals("stone_brick_stairs")
|
||||
|| block.equals("stone_brick_slab")
|
||||
|| block.equals("stone_brick_wall");
|
||||
}
|
||||
|
||||
private static boolean isTrialChamberMaterial(String block) {
|
||||
return block.contains("tuff_brick")
|
||||
|| block.equals("polished_tuff")
|
||||
|| block.equals("chiseled_tuff")
|
||||
|| block.endsWith("copper_grate")
|
||||
|| block.equals("trial_spawner")
|
||||
|| block.equals("vault");
|
||||
}
|
||||
|
||||
private static boolean isVillageMaterial(String structure, String block) {
|
||||
if (isCobblestoneConstructionMaterial(block)) {
|
||||
return true;
|
||||
}
|
||||
return switch (structure) {
|
||||
case "village_plains" -> isWoodConstructionMaterial(block, "oak");
|
||||
case "village_desert" -> block.equals("cut_sandstone")
|
||||
|| block.equals("smooth_sandstone")
|
||||
|| block.equals("cut_sandstone_slab")
|
||||
|| block.equals("smooth_sandstone_slab")
|
||||
|| block.equals("smooth_sandstone_stairs")
|
||||
|| block.equals("sandstone_stairs")
|
||||
|| block.equals("sandstone_slab")
|
||||
|| block.equals("sandstone_wall");
|
||||
case "village_savanna" -> isWoodConstructionMaterial(block, "acacia");
|
||||
case "village_snowy", "village_taiga" -> isWoodConstructionMaterial(block, "spruce");
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean isWoodConstructionMaterial(String block, String wood) {
|
||||
if (block.startsWith(wood)) {
|
||||
int suffixOffset = wood.length();
|
||||
if (matchesSuffix(block, suffixOffset, "_planks")
|
||||
|| matchesSuffix(block, suffixOffset, "_stairs")
|
||||
|| matchesSuffix(block, suffixOffset, "_slab")
|
||||
|| matchesSuffix(block, suffixOffset, "_fence")
|
||||
|| matchesSuffix(block, suffixOffset, "_fence_gate")
|
||||
|| matchesSuffix(block, suffixOffset, "_door")
|
||||
|| matchesSuffix(block, suffixOffset, "_trapdoor")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
int strippedOffset = "stripped_".length();
|
||||
if (!block.startsWith("stripped_")
|
||||
|| !block.regionMatches(strippedOffset, wood, 0, wood.length())) {
|
||||
return false;
|
||||
}
|
||||
int suffixOffset = strippedOffset + wood.length();
|
||||
return matchesSuffix(block, suffixOffset, "_log")
|
||||
|| matchesSuffix(block, suffixOffset, "_wood");
|
||||
}
|
||||
|
||||
private static boolean isCobblestoneConstructionMaterial(String block) {
|
||||
return block.equals("cobblestone")
|
||||
|| block.equals("cobblestone_stairs")
|
||||
|| block.equals("cobblestone_slab")
|
||||
|| block.equals("cobblestone_wall")
|
||||
|| block.equals("mossy_cobblestone")
|
||||
|| block.equals("mossy_cobblestone_stairs")
|
||||
|| block.equals("mossy_cobblestone_slab")
|
||||
|| block.equals("mossy_cobblestone_wall");
|
||||
}
|
||||
|
||||
private static boolean matchesSuffix(String value, int offset, String suffix) {
|
||||
return value.length() == offset + suffix.length()
|
||||
&& value.regionMatches(offset, suffix, 0, suffix.length());
|
||||
}
|
||||
|
||||
private static boolean isMonumentMaterial(String block) {
|
||||
return block.equals("prismarine")
|
||||
|| block.equals("prismarine_bricks")
|
||||
|| block.equals("dark_prismarine")
|
||||
|| block.equals("sea_lantern");
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import art.arcane.iris.engine.framework.StructureVerticalBounds;
|
||||
import art.arcane.iris.modded.WorldCheckStructureAudit.StructureCheck;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
final class WorldCheckPredicates {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
|
||||
private WorldCheckPredicates() {
|
||||
}
|
||||
|
||||
static void emitSkipped(StructureCheck check, String reason, String... events) {
|
||||
for (String event : events) {
|
||||
qaEvent(event, check.label(), false, "skipped=" + reason);
|
||||
}
|
||||
}
|
||||
|
||||
static void qaEvent(String event, String structure, boolean pass, String detail) {
|
||||
LOGGER.info(qaEventJson(event, structure, pass, detail));
|
||||
}
|
||||
|
||||
static String qaEventJson(String event, String structure, boolean pass, String detail) {
|
||||
return "QA_EVT {\"event\":\"" + jsonEscape(event)
|
||||
+ "\",\"structure\":\"" + jsonEscape(structure)
|
||||
+ "\",\"pass\":" + pass
|
||||
+ ",\"detail\":\"" + jsonEscape(detail) + "\"}";
|
||||
}
|
||||
|
||||
static String jsonEscape(String value) {
|
||||
StringBuilder escaped = new StringBuilder(value.length() + 16);
|
||||
for (int i = 0; i < value.length(); i++) {
|
||||
char character = value.charAt(i);
|
||||
switch (character) {
|
||||
case '"' -> escaped.append("\\\"");
|
||||
case '\\' -> escaped.append("\\\\");
|
||||
case '\b' -> escaped.append("\\b");
|
||||
case '\f' -> escaped.append("\\f");
|
||||
case '\n' -> escaped.append("\\n");
|
||||
case '\r' -> escaped.append("\\r");
|
||||
case '\t' -> escaped.append("\\t");
|
||||
default -> {
|
||||
if (character < 32) {
|
||||
escaped.append("\\u");
|
||||
String hex = Integer.toHexString(character);
|
||||
escaped.append("0".repeat(4 - hex.length())).append(hex);
|
||||
} else {
|
||||
escaped.append(character);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return escaped.toString();
|
||||
}
|
||||
|
||||
static boolean hasNativeStructureEvidence(boolean validStart, int references) {
|
||||
return validStart || references > 0;
|
||||
}
|
||||
|
||||
static boolean hasCharacteristicMaterialEvidence(int blocks, int chunksWithMaterial, int scannedChunks) {
|
||||
if (blocks <= 0 || chunksWithMaterial <= 0 || scannedChunks <= 0
|
||||
|| chunksWithMaterial > scannedChunks) {
|
||||
return false;
|
||||
}
|
||||
return scannedChunks == 1 || chunksWithMaterial > 1;
|
||||
}
|
||||
|
||||
static boolean verticalShiftMatches(int configuredShift, Integer appliedShift, int shiftedMinY,
|
||||
int shiftedMaxY, int worldMinY, int worldMaxYExclusive) {
|
||||
try {
|
||||
if (appliedShift == null) {
|
||||
return configuredShift == 0 && StructureVerticalBounds.clampOffset(
|
||||
shiftedMinY, shiftedMaxY, 0, worldMinY, worldMaxYExclusive) == 0;
|
||||
}
|
||||
int originalMinY = Math.subtractExact(shiftedMinY, appliedShift);
|
||||
int originalMaxY = Math.subtractExact(shiftedMaxY, appliedShift);
|
||||
int expectedShift = StructureVerticalBounds.clampOffset(
|
||||
originalMinY, originalMaxY, configuredShift, worldMinY, worldMaxYExclusive);
|
||||
return appliedShift == expectedShift;
|
||||
} catch (RuntimeException error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static boolean mansionVegetationPass(int remainingVegetationBlocks) {
|
||||
return remainingVegetationBlocks == 0;
|
||||
}
|
||||
|
||||
static boolean mansionVegetationAbovePiece(boolean vegetation, int blockY, int highestPieceY) {
|
||||
return vegetation && blockY > highestPieceY;
|
||||
}
|
||||
|
||||
static boolean villageFoundationPass(int unsupportedColumns) {
|
||||
return unsupportedColumns == 0;
|
||||
}
|
||||
|
||||
static boolean villagePoiPass(int inBounds, int outOfBounds) {
|
||||
return inBounds > 0 && outOfBounds == 0;
|
||||
}
|
||||
|
||||
static MessageDigest sha256() {
|
||||
try {
|
||||
return MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+783
@@ -0,0 +1,783 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.nativegen.NativeStructureFoundationBuilder;
|
||||
import art.arcane.iris.nativegen.NativeStructureVegetationClearer;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiManager;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiRecord;
|
||||
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.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
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;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.BitSet;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
|
||||
final class WorldCheckStructureAudit {
|
||||
static final List<StructureCheck> STRUCTURE_CHECKS = List.of(
|
||||
new StructureCheck("stronghold", List.of("minecraft:stronghold"), 256),
|
||||
new StructureCheck("trial_chambers", List.of("minecraft:trial_chambers"), 128),
|
||||
new StructureCheck("mansion", List.of("minecraft:mansion"), 256),
|
||||
new StructureCheck("village", List.of(
|
||||
"minecraft:village_plains",
|
||||
"minecraft:village_desert",
|
||||
"minecraft:village_savanna",
|
||||
"minecraft:village_snowy",
|
||||
"minecraft:village_taiga"), 128),
|
||||
new StructureCheck("monument", List.of("minecraft:monument"), 128)
|
||||
);
|
||||
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 Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
|
||||
private WorldCheckStructureAudit() {
|
||||
}
|
||||
|
||||
static NativeStructureGate checkNativeStructures(ServerLevel level,
|
||||
IrisModdedChunkGenerator generator,
|
||||
BlockPos origin) {
|
||||
boolean pass = true;
|
||||
int nonVillagePassed = 0;
|
||||
boolean villagePass = false;
|
||||
PendingVillagePoi pendingPoi = null;
|
||||
for (StructureCheck check : STRUCTURE_CHECKS) {
|
||||
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++;
|
||||
}
|
||||
}
|
||||
return new NativeStructureGate(pass, nonVillagePassed, villagePass, pendingPoi);
|
||||
}
|
||||
|
||||
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<>();
|
||||
for (String key : check.registryKeys()) {
|
||||
Identifier identifier = Identifier.tryParse(key);
|
||||
if (identifier == null) {
|
||||
continue;
|
||||
}
|
||||
Optional<Holder.Reference<Structure>> resolved = registry.get(identifier);
|
||||
if (resolved.isPresent()) {
|
||||
registered.add(resolved.get());
|
||||
registeredKeys.add(identifier.toString());
|
||||
}
|
||||
}
|
||||
boolean registryOk = registered.size() == check.registryKeys().size();
|
||||
LOGGER.info("[worldcheck] {} registry: {}/{} resolved {}", check.label(), registered.size(),
|
||||
check.registryKeys().size(), registeredKeys);
|
||||
WorldCheckPredicates.qaEvent("structure_registry", check.label(), registryOk,
|
||||
"resolved=" + registered.size() + ",expected=" + check.registryKeys().size()
|
||||
+ ",keys=" + String.join("|", registeredKeys));
|
||||
if (!registryOk) {
|
||||
LOGGER.error("[worldcheck] {} registry resolution failed; expected {}", check.label(), check.registryKeys());
|
||||
WorldCheckPredicates.emitSkipped(check, "registry", "structure_reachability", "structure_locate",
|
||||
"structure_start_reference", "structure_footprint", "structure_material",
|
||||
"structure_block_entity");
|
||||
return new StructureCheckResult(false, null);
|
||||
}
|
||||
|
||||
List<Holder<Structure>> reachable = new ArrayList<>(registered.size());
|
||||
LinkedHashSet<String> reachableKeys = new LinkedHashSet<>();
|
||||
for (Holder<Structure> holder : registered) {
|
||||
if (!generator.isNativeStructureReachable(holder)) {
|
||||
continue;
|
||||
}
|
||||
reachable.add(holder);
|
||||
Identifier key = registry.getKey(holder.value());
|
||||
if (key != null) {
|
||||
reachableKeys.add(key.toString());
|
||||
}
|
||||
}
|
||||
boolean reachableOk = !reachable.isEmpty();
|
||||
LOGGER.info("[worldcheck] {} biome-reachable through Iris: {}", check.label(), reachableKeys);
|
||||
WorldCheckPredicates.qaEvent("structure_reachability", check.label(), reachableOk,
|
||||
"reachable=" + reachable.size() + ",registered=" + registered.size()
|
||||
+ ",keys=" + String.join("|", reachableKeys));
|
||||
if (!reachableOk) {
|
||||
LOGGER.error("[worldcheck] {} cannot generate in any biome produced by this Iris pack", check.label());
|
||||
WorldCheckPredicates.emitSkipped(check, "reachability", "structure_locate", "structure_start_reference",
|
||||
"structure_footprint", "structure_material", "structure_block_entity");
|
||||
return new StructureCheckResult(false, null);
|
||||
}
|
||||
|
||||
long locateStart = System.nanoTime();
|
||||
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());
|
||||
WorldCheckPredicates.qaEvent("structure_locate", check.label(), locateOk,
|
||||
"method=placement_candidates,millis=" + locateMillis + ",radius=" + check.locateRadius()
|
||||
+ ",result=" + (foundKey == null ? "none" : foundKey));
|
||||
if (found == null) {
|
||||
LOGGER.error("[worldcheck] {} native placement candidates produced no valid start within {} rings after {}ms",
|
||||
check.label(), check.locateRadius(), locateMillis);
|
||||
WorldCheckPredicates.emitSkipped(check, "locate", "structure_start_reference", "structure_footprint",
|
||||
"structure_material", "structure_block_entity");
|
||||
return new StructureCheckResult(false, null);
|
||||
}
|
||||
|
||||
BlockPos position = found.getFirst();
|
||||
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] {} candidate scan returned unexpected structure {}", check.label(), foundKey);
|
||||
WorldCheckPredicates.emitSkipped(check, "locate", "structure_start_reference", "structure_footprint",
|
||||
"structure_material", "structure_block_entity");
|
||||
return new StructureCheckResult(false, null);
|
||||
}
|
||||
|
||||
int chunkX = position.getX() >> 4;
|
||||
int chunkZ = position.getZ() >> 4;
|
||||
ChunkAccess targetChunk = level.getChunk(chunkX, chunkZ);
|
||||
Structure structure = found.getSecond().value();
|
||||
StructureStart start = resolveStructureStart(level, targetChunk, structure);
|
||||
boolean validStart = start != null && start.isValid();
|
||||
int references = targetChunk.getReferencesForStructure(structure).size();
|
||||
boolean startReferenceOk = WorldCheckPredicates.hasNativeStructureEvidence(validStart, references);
|
||||
LOGGER.info("[worldcheck] {} target chunk {},{}: valid start={}, references={}",
|
||||
check.label(), chunkX, chunkZ, validStart, references);
|
||||
WorldCheckPredicates.qaEvent("structure_start_reference", check.label(), startReferenceOk,
|
||||
"chunk=" + chunkX + "," + chunkZ + ",validStart=" + validStart
|
||||
+ ",references=" + references);
|
||||
if (!startReferenceOk || !validStart) {
|
||||
LOGGER.error("[worldcheck] {} located at chunk {},{} but no resolvable valid start was generated",
|
||||
check.label(), chunkX, chunkZ);
|
||||
WorldCheckPredicates.emitSkipped(check, "start_reference", "structure_footprint", "structure_material",
|
||||
"structure_block_entity");
|
||||
return new StructureCheckResult(false, null);
|
||||
}
|
||||
|
||||
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(
|
||||
generator.commandEngine(), foundKey.toString(),
|
||||
NativeStructureVegetationClearer.isUndergroundStep(structure.step()));
|
||||
Integer appliedShift = generator.worldCheckStructureShift(foundKey.toString(), start.getChunkPos());
|
||||
BoundingBox shiftedBounds = start.getBoundingBox();
|
||||
boolean verticalShiftOk = WorldCheckPredicates.verticalShiftMatches(
|
||||
decision.yShift(), appliedShift, shiftedBounds.minY(), shiftedBounds.maxY(),
|
||||
level.getMinY(), level.getMaxY());
|
||||
WorldCheckPredicates.qaEvent("structure_vertical_shift", check.label(), verticalShiftOk,
|
||||
"configured=" + decision.yShift() + ",applied="
|
||||
+ (appliedShift == null ? "unrecorded" : appliedShift));
|
||||
if (!verticalShiftOk) {
|
||||
LOGGER.error("[worldcheck] {} expected vertical shift {} but generation recorded {}",
|
||||
check.label(), decision.yShift(), appliedShift);
|
||||
}
|
||||
|
||||
FootprintAudit footprint = auditFootprint(level, structure, start, check, foundKey);
|
||||
boolean footprintOk = footprint.inspectedChunks() > 0
|
||||
&& footprint.evidenceChunks() == footprint.inspectedChunks()
|
||||
&& footprint.coveredPieces() == footprint.totalPieces();
|
||||
LOGGER.info("[worldcheck] {} footprint: chunks={}/{} evidence={} pieces={}/{}",
|
||||
check.label(), footprint.inspectedChunks(), footprint.availableChunks(),
|
||||
footprint.evidenceChunks(), footprint.coveredPieces(), footprint.totalPieces());
|
||||
WorldCheckPredicates.qaEvent("structure_footprint", check.label(), footprintOk,
|
||||
"inspected=" + footprint.inspectedChunks() + ",available=" + footprint.availableChunks()
|
||||
+ ",evidence=" + footprint.evidenceChunks() + ",coveredPieces="
|
||||
+ footprint.coveredPieces() + ",totalPieces=" + footprint.totalPieces());
|
||||
|
||||
boolean materialOk = WorldCheckPredicates.hasCharacteristicMaterialEvidence(footprint.characteristicBlocks(),
|
||||
footprint.characteristicChunks(), footprint.materialScannedChunks());
|
||||
LOGGER.info("[worldcheck] {} material: blocks={} chunks={}/{}",
|
||||
check.label(), footprint.characteristicBlocks(), footprint.characteristicChunks(),
|
||||
footprint.materialScannedChunks());
|
||||
WorldCheckPredicates.qaEvent("structure_material", check.label(), materialOk,
|
||||
"blocks=" + footprint.characteristicBlocks() + ",chunks=" + footprint.characteristicChunks()
|
||||
+ ",scanned=" + footprint.materialScannedChunks());
|
||||
|
||||
boolean vegetationOk = true;
|
||||
if (check.label().equals("mansion")) {
|
||||
boolean overlap = footprint.vegetationBlocks() > 0;
|
||||
vegetationOk = WorldCheckPredicates.mansionVegetationPass(footprint.vegetationBlocks());
|
||||
LOGGER.info("[worldcheck] mansion vegetation metric: remaining log/leaf blocks={} columns={} overlap={}",
|
||||
footprint.vegetationBlocks(), footprint.vegetationColumns(), overlap);
|
||||
WorldCheckPredicates.qaEvent("mansion_vegetation_metric", check.label(), vegetationOk,
|
||||
"remainingLogsOrLeaves=" + footprint.vegetationBlocks() + ",columns="
|
||||
+ footprint.vegetationColumns() + ",overlap=" + overlap);
|
||||
}
|
||||
|
||||
boolean foundationOk = true;
|
||||
PendingVillagePoi pendingPoi = null;
|
||||
if (check.label().equals("village")) {
|
||||
foundationOk = WorldCheckPredicates.villageFoundationPass(footprint.foundationGapColumns());
|
||||
LOGGER.info("[worldcheck] village foundation metric: bases={} cobblestone={} columns={} unsupported={}",
|
||||
footprint.foundationBaseColumns(), footprint.foundationBlocks(),
|
||||
footprint.foundationColumns(), footprint.foundationGapColumns());
|
||||
WorldCheckPredicates.qaEvent("village_foundation_metric", check.label(), foundationOk,
|
||||
"bases=" + footprint.foundationBaseColumns() + ",cobblestoneBelowBase="
|
||||
+ footprint.foundationBlocks() + ",columns="
|
||||
+ footprint.foundationColumns() + ",unsupported=" + footprint.foundationGapColumns());
|
||||
pendingPoi = new PendingVillagePoi(level, start);
|
||||
}
|
||||
|
||||
boolean blockEntityOk = footprint.blockEntityStates() == footprint.blockEntitiesPresent();
|
||||
LOGGER.info("[worldcheck] {} block entities: state blocks={}, present={}, missing={}",
|
||||
check.label(), footprint.blockEntityStates(), footprint.blockEntitiesPresent(),
|
||||
footprint.blockEntityStates() - footprint.blockEntitiesPresent());
|
||||
WorldCheckPredicates.qaEvent("structure_block_entity", check.label(), blockEntityOk,
|
||||
"states=" + footprint.blockEntityStates() + ",present=" + footprint.blockEntitiesPresent()
|
||||
+ ",missing=" + (footprint.blockEntityStates() - footprint.blockEntitiesPresent()));
|
||||
if (!footprintOk) {
|
||||
LOGGER.error("[worldcheck] {} structure footprint is incomplete", check.label());
|
||||
}
|
||||
if (!materialOk) {
|
||||
LOGGER.error("[worldcheck] {} has no distributed characteristic structure material", check.label());
|
||||
}
|
||||
if (!blockEntityOk) {
|
||||
LOGGER.error("[worldcheck] {} generated block-entity states without matching block entities", check.label());
|
||||
}
|
||||
if (!vegetationOk) {
|
||||
LOGGER.error("[worldcheck] mansion vegetation still intersects the generated structure footprint");
|
||||
}
|
||||
if (!foundationOk) {
|
||||
LOGGER.error("[worldcheck] village has unsupported foundation columns after stilt placement");
|
||||
}
|
||||
boolean pass = verticalShiftOk && footprintOk && materialOk && blockEntityOk
|
||||
&& vegetationOk && foundationOk;
|
||||
return new StructureCheckResult(pass, pass ? pendingPoi : null);
|
||||
}
|
||||
|
||||
private static StructureStart resolveStructureStart(ServerLevel level, ChunkAccess targetChunk,
|
||||
Structure structure) {
|
||||
StructureStart direct = targetChunk.getStartForStructure(structure);
|
||||
if (direct != null && direct.isValid()) {
|
||||
return direct;
|
||||
}
|
||||
int checked = 0;
|
||||
for (long packed : targetChunk.getReferencesForStructure(structure)) {
|
||||
if (checked++ >= MAX_START_REFERENCE_CHUNKS) {
|
||||
break;
|
||||
}
|
||||
ChunkAccess referencedChunk = level.getChunk(ChunkPos.getX(packed), ChunkPos.getZ(packed));
|
||||
StructureStart referenced = referencedChunk.getStartForStructure(structure);
|
||||
if (referenced != null && referenced.isValid()) {
|
||||
return referenced;
|
||||
}
|
||||
}
|
||||
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();
|
||||
BoundingBox bounds = start.getBoundingBox();
|
||||
int availableChunks = footprintChunkCount(bounds);
|
||||
List<ChunkPos> selected = selectFootprintChunks(start, MAX_FOOTPRINT_CHUNKS);
|
||||
int evidenceChunks = 0;
|
||||
int blockEntityStates = 0;
|
||||
int blockEntitiesPresent = 0;
|
||||
int materialScannedChunks = 0;
|
||||
int characteristicBlocks = 0;
|
||||
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[] maximumPieceY = new int[256];
|
||||
for (ChunkPos chunkPos : selected) {
|
||||
ChunkAccess chunk = level.getChunk(chunkPos.x(), chunkPos.z());
|
||||
StructureStart localStart = chunk.getStartForStructure(structure);
|
||||
boolean validStart = localStart != null && localStart.isValid();
|
||||
int references = chunk.getReferencesForStructure(structure).size();
|
||||
if (WorldCheckPredicates.hasNativeStructureEvidence(validStart, references)) {
|
||||
evidenceChunks++;
|
||||
}
|
||||
BlockEntityAudit blockEntities = auditBlockEntities(level, chunk);
|
||||
blockEntityStates += blockEntities.states();
|
||||
blockEntitiesPresent += blockEntities.present();
|
||||
StructureMaterialAudit material = auditStructureMaterial(level, chunk, start, check,
|
||||
structureKey, visited, maximumPieceY);
|
||||
if (material.scanned()) {
|
||||
materialScannedChunks++;
|
||||
}
|
||||
characteristicBlocks += material.characteristicBlocks();
|
||||
if (material.characteristicBlocks() > 0) {
|
||||
characteristicChunks++;
|
||||
}
|
||||
vegetationBlocks += material.vegetationBlocks();
|
||||
vegetationColumns += material.vegetationColumns();
|
||||
foundationBaseColumns += material.foundationBaseColumns();
|
||||
foundationBlocks += material.foundationBlocks();
|
||||
foundationColumns += material.foundationColumns();
|
||||
foundationGapColumns += material.foundationGapColumns();
|
||||
}
|
||||
int coveredPieces = 0;
|
||||
for (StructurePiece piece : pieces) {
|
||||
boolean covered = false;
|
||||
for (ChunkPos chunkPos : selected) {
|
||||
if (intersectsChunk(piece.getBoundingBox(), chunkPos)) {
|
||||
covered = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (covered) {
|
||||
coveredPieces++;
|
||||
}
|
||||
}
|
||||
return new FootprintAudit(selected.size(), availableChunks, evidenceChunks,
|
||||
coveredPieces, pieces.size(), blockEntityStates, blockEntitiesPresent,
|
||||
materialScannedChunks, characteristicBlocks, characteristicChunks,
|
||||
vegetationBlocks, vegetationColumns, foundationBaseColumns, foundationBlocks,
|
||||
foundationColumns, foundationGapColumns);
|
||||
}
|
||||
|
||||
private static StructureMaterialAudit auditStructureMaterial(ServerLevel level, ChunkAccess chunk,
|
||||
StructureStart start,
|
||||
StructureCheck check,
|
||||
Identifier structureKey,
|
||||
BitSet visited,
|
||||
int[] maximumPieceY) {
|
||||
List<StructurePiece> pieces = start.getPieces();
|
||||
visited.clear();
|
||||
Arrays.fill(maximumPieceY, Integer.MIN_VALUE);
|
||||
int characteristicBlocks = 0;
|
||||
int minimumWorldY = level.getMinY();
|
||||
int maximumWorldY = level.getMaxY() - 1;
|
||||
int minimumChunkX = chunk.getPos().getMinBlockX();
|
||||
int maximumChunkX = chunk.getPos().getMaxBlockX();
|
||||
int minimumChunkZ = chunk.getPos().getMinBlockZ();
|
||||
int maximumChunkZ = chunk.getPos().getMaxBlockZ();
|
||||
boolean scanned = false;
|
||||
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
|
||||
for (StructurePiece piece : pieces) {
|
||||
BoundingBox bounds = piece.getBoundingBox();
|
||||
int minimumX = Math.max(minimumChunkX, bounds.minX());
|
||||
int maximumX = Math.min(maximumChunkX, bounds.maxX());
|
||||
int minimumY = Math.max(minimumWorldY, bounds.minY());
|
||||
int maximumY = Math.min(maximumWorldY, bounds.maxY());
|
||||
int minimumZ = Math.max(minimumChunkZ, bounds.minZ());
|
||||
int maximumZ = Math.min(maximumChunkZ, bounds.maxZ());
|
||||
if (minimumX > maximumX || minimumY > maximumY || minimumZ > maximumZ) {
|
||||
continue;
|
||||
}
|
||||
scanned = true;
|
||||
for (int z = minimumZ; z <= maximumZ; z++) {
|
||||
int localZ = z - minimumChunkZ;
|
||||
for (int x = minimumX; x <= maximumX; x++) {
|
||||
int column = (localZ << 4) | (x - minimumChunkX);
|
||||
maximumPieceY[column] = Math.max(maximumPieceY[column], maximumY);
|
||||
}
|
||||
}
|
||||
for (int y = minimumY; y <= maximumY; y++) {
|
||||
int verticalIndex = (y - minimumWorldY) << 8;
|
||||
for (int z = minimumZ; z <= maximumZ; z++) {
|
||||
int localZ = z - minimumChunkZ;
|
||||
for (int x = minimumX; x <= maximumX; x++) {
|
||||
int column = (localZ << 4) | (x - minimumChunkX);
|
||||
int index = verticalIndex | column;
|
||||
if (visited.get(index)) {
|
||||
continue;
|
||||
}
|
||||
visited.set(index);
|
||||
BlockState state = chunk.getBlockState(position.set(x, y, z));
|
||||
Identifier blockKey = BuiltInRegistries.BLOCK.getKey(state.getBlock());
|
||||
if (WorldCheckMaterials.isCharacteristicMaterial(check.label(), structureKey, blockKey)) {
|
||||
characteristicBlocks++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int vegetationBlocks = 0;
|
||||
int vegetationColumns = 0;
|
||||
if (check.label().equals("mansion")) {
|
||||
for (int column = 0; column < maximumPieceY.length; column++) {
|
||||
int highestPieceY = maximumPieceY[column];
|
||||
if (highestPieceY == Integer.MIN_VALUE || highestPieceY >= maximumWorldY) {
|
||||
continue;
|
||||
}
|
||||
boolean vegetationColumn = false;
|
||||
int x = minimumChunkX + (column & 15);
|
||||
int z = minimumChunkZ + (column >> 4);
|
||||
for (int y = highestPieceY + 1; y <= maximumWorldY; y++) {
|
||||
BlockState state = chunk.getBlockState(position.set(x, y, z));
|
||||
boolean vegetation = state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES);
|
||||
if (!WorldCheckPredicates.mansionVegetationAbovePiece(vegetation, y, highestPieceY)) {
|
||||
continue;
|
||||
}
|
||||
vegetationBlocks++;
|
||||
vegetationColumn = true;
|
||||
}
|
||||
if (vegetationColumn) {
|
||||
vegetationColumns++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int foundationBaseColumns = 0;
|
||||
int foundationBlocks = 0;
|
||||
int foundationColumns = 0;
|
||||
int foundationGapColumns = 0;
|
||||
if (check.label().equals("village")) {
|
||||
BoundingBox area = new BoundingBox(
|
||||
minimumChunkX, minimumWorldY, minimumChunkZ,
|
||||
maximumChunkX, maximumWorldY, maximumChunkZ);
|
||||
ChunkGenerator chunkGenerator = level.getChunkSource().getGenerator();
|
||||
if (!(chunkGenerator instanceof IrisModdedChunkGenerator irisGenerator)) {
|
||||
throw new IllegalStateException("Iris structure audit requires the Iris chunk generator");
|
||||
}
|
||||
Engine engine = irisGenerator.commandEngine();
|
||||
IntBinaryOperator surfaceHeight = (x, z) ->
|
||||
engine.getHeight(x, z, true) + engine.getMinHeight();
|
||||
NativeStructureFoundationBuilder.StiltSupportAudit foundation =
|
||||
NativeStructureFoundationBuilder.auditStiltSupport(
|
||||
level, area, start, Blocks.COBBLESTONE.defaultBlockState(), surfaceHeight);
|
||||
foundationBaseColumns = foundation.baseColumns();
|
||||
foundationBlocks = foundation.stiltBlocks();
|
||||
foundationColumns = foundation.stiltColumns();
|
||||
foundationGapColumns = foundation.unsupportedColumns();
|
||||
}
|
||||
|
||||
return new StructureMaterialAudit(scanned, characteristicBlocks, vegetationBlocks,
|
||||
vegetationColumns, foundationBaseColumns, foundationBlocks, foundationColumns,
|
||||
foundationGapColumns);
|
||||
}
|
||||
|
||||
private static List<ChunkPos> selectFootprintChunks(StructureStart start, int limit) {
|
||||
LinkedHashSet<ChunkPos> selected = new LinkedHashSet<>();
|
||||
addBounded(selected, start.getChunkPos(), limit);
|
||||
List<ChunkPos> pieceAnchors = new ArrayList<>();
|
||||
for (StructurePiece piece : start.getPieces()) {
|
||||
BoundingBox bounds = piece.getBoundingBox();
|
||||
pieceAnchors.add(new ChunkPos((bounds.minX() + bounds.maxX()) >> 5,
|
||||
(bounds.minZ() + bounds.maxZ()) >> 5));
|
||||
pieceAnchors.add(new ChunkPos(bounds.minX() >> 4, bounds.minZ() >> 4));
|
||||
pieceAnchors.add(new ChunkPos(bounds.maxX() >> 4, bounds.maxZ() >> 4));
|
||||
}
|
||||
pieceAnchors.sort(Comparator.comparingInt((ChunkPos chunkPos) ->
|
||||
chunkPos.distanceSquared(start.getChunkPos())));
|
||||
for (ChunkPos chunkPos : pieceAnchors) {
|
||||
addBounded(selected, chunkPos, limit);
|
||||
}
|
||||
for (ChunkPos chunkPos : boundedFootprintChunks(start.getBoundingBox(), start.getChunkPos(), limit)) {
|
||||
if (intersectsAnyPiece(start.getPieces(), chunkPos)) {
|
||||
addBounded(selected, chunkPos, limit);
|
||||
}
|
||||
}
|
||||
return List.copyOf(selected);
|
||||
}
|
||||
|
||||
static List<ChunkPos> boundedFootprintChunks(BoundingBox bounds, ChunkPos origin, int limit) {
|
||||
if (limit <= 0) {
|
||||
return List.of();
|
||||
}
|
||||
int minChunkX = bounds.minX() >> 4;
|
||||
int maxChunkX = bounds.maxX() >> 4;
|
||||
int minChunkZ = bounds.minZ() >> 4;
|
||||
int maxChunkZ = bounds.maxZ() >> 4;
|
||||
long width = (long) maxChunkX - minChunkX + 1L;
|
||||
long depth = (long) maxChunkZ - minChunkZ + 1L;
|
||||
long total = width * depth;
|
||||
LinkedHashSet<ChunkPos> chunks = new LinkedHashSet<>();
|
||||
addBounded(chunks, origin, limit);
|
||||
if (total <= limit) {
|
||||
for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) {
|
||||
for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) {
|
||||
addBounded(chunks, new ChunkPos(chunkX, chunkZ), limit);
|
||||
}
|
||||
}
|
||||
return List.copyOf(chunks);
|
||||
}
|
||||
addBounded(chunks, new ChunkPos(minChunkX, minChunkZ), limit);
|
||||
addBounded(chunks, new ChunkPos(maxChunkX, minChunkZ), limit);
|
||||
addBounded(chunks, new ChunkPos(minChunkX, maxChunkZ), limit);
|
||||
addBounded(chunks, new ChunkPos(maxChunkX, maxChunkZ), limit);
|
||||
int samplesPerAxis = Math.max(2, (int) Math.floor(Math.sqrt(limit)));
|
||||
for (int sampleZ = 0; sampleZ < samplesPerAxis; sampleZ++) {
|
||||
int chunkZ = sampleCoordinate(minChunkZ, maxChunkZ, sampleZ, samplesPerAxis);
|
||||
for (int sampleX = 0; sampleX < samplesPerAxis; sampleX++) {
|
||||
int chunkX = sampleCoordinate(minChunkX, maxChunkX, sampleX, samplesPerAxis);
|
||||
addBounded(chunks, new ChunkPos(chunkX, chunkZ), limit);
|
||||
}
|
||||
}
|
||||
return List.copyOf(chunks);
|
||||
}
|
||||
|
||||
private static int footprintChunkCount(BoundingBox bounds) {
|
||||
long width = (long) (bounds.maxX() >> 4) - (bounds.minX() >> 4) + 1L;
|
||||
long depth = (long) (bounds.maxZ() >> 4) - (bounds.minZ() >> 4) + 1L;
|
||||
long total = width * depth;
|
||||
return total > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) total;
|
||||
}
|
||||
|
||||
private static int sampleCoordinate(int minimum, int maximum, int index, int samples) {
|
||||
if (samples <= 1 || minimum == maximum) {
|
||||
return minimum;
|
||||
}
|
||||
double progress = (double) index / (double) (samples - 1);
|
||||
return minimum + (int) Math.round((maximum - minimum) * progress);
|
||||
}
|
||||
|
||||
private static void addBounded(Set<ChunkPos> chunks, ChunkPos chunkPos, int limit) {
|
||||
if (chunks.size() < limit) {
|
||||
chunks.add(chunkPos);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean intersectsAnyPiece(List<StructurePiece> pieces, ChunkPos chunkPos) {
|
||||
for (StructurePiece piece : pieces) {
|
||||
if (intersectsChunk(piece.getBoundingBox(), chunkPos)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean intersectsChunk(BoundingBox bounds, ChunkPos chunkPos) {
|
||||
return bounds.maxX() >= chunkPos.getMinBlockX()
|
||||
&& bounds.minX() <= chunkPos.getMaxBlockX()
|
||||
&& bounds.maxZ() >= chunkPos.getMinBlockZ()
|
||||
&& bounds.minZ() <= chunkPos.getMaxBlockZ();
|
||||
}
|
||||
|
||||
private static BlockEntityAudit auditBlockEntities(ServerLevel level, ChunkAccess chunk) {
|
||||
int states = 0;
|
||||
int present = 0;
|
||||
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
|
||||
LevelChunkSection[] sections = chunk.getSections();
|
||||
for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
|
||||
LevelChunkSection section = sections[sectionIndex];
|
||||
if (!section.maybeHas(BlockState::hasBlockEntity)) {
|
||||
continue;
|
||||
}
|
||||
int sectionMinY = chunk.getSectionYFromSectionIndex(sectionIndex) << 4;
|
||||
for (int localY = 0; localY < 16; localY++) {
|
||||
for (int localZ = 0; localZ < 16; localZ++) {
|
||||
for (int localX = 0; localX < 16; localX++) {
|
||||
BlockState state = section.getBlockState(localX, localY, localZ);
|
||||
if (!state.hasBlockEntity()) {
|
||||
continue;
|
||||
}
|
||||
states++;
|
||||
position.set(chunk.getPos().getBlockX(localX), sectionMinY + localY,
|
||||
chunk.getPos().getBlockZ(localZ));
|
||||
if (level.getBlockEntity(position) != null) {
|
||||
present++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new BlockEntityAudit(states, present);
|
||||
}
|
||||
|
||||
static PoiAudit auditStructurePois(ServerLevel level, StructureStart start) {
|
||||
int inBounds = 0;
|
||||
int outOfBounds = 0;
|
||||
PoiManager poiManager = level.getPoiManager();
|
||||
for (ChunkPos chunkPos : selectFootprintChunks(start, MAX_FOOTPRINT_CHUNKS)) {
|
||||
List<PoiRecord> records = poiManager.getInChunk(
|
||||
holder -> true, chunkPos, PoiManager.Occupancy.ANY).toList();
|
||||
for (PoiRecord record : records) {
|
||||
BlockPos position = record.getPos();
|
||||
if (position.getY() < level.getMinY() || position.getY() >= level.getMaxY()) {
|
||||
outOfBounds++;
|
||||
continue;
|
||||
}
|
||||
if (insideAnyPiece(start.getPieces(), position)) {
|
||||
inBounds++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new PoiAudit(inBounds, outOfBounds);
|
||||
}
|
||||
|
||||
private static boolean insideAnyPiece(List<StructurePiece> pieces, BlockPos position) {
|
||||
for (StructurePiece piece : pieces) {
|
||||
if (piece.getBoundingBox().isInside(position)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
record StructureCheck(String label, List<String> registryKeys, int locateRadius) {
|
||||
}
|
||||
|
||||
record NativeStructureGate(boolean passBeforePoi, int nonVillagePassed,
|
||||
boolean villagePassBeforePoi, PendingVillagePoi pendingPoi) {
|
||||
}
|
||||
|
||||
private record StructureCheckResult(boolean pass, PendingVillagePoi pendingPoi) {
|
||||
}
|
||||
|
||||
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 foundationBaseColumns, int foundationBlocks, int foundationColumns,
|
||||
int foundationGapColumns) {
|
||||
}
|
||||
|
||||
private record BlockEntityAudit(int states, int present) {
|
||||
}
|
||||
|
||||
private record StructureMaterialAudit(boolean scanned, int characteristicBlocks,
|
||||
int vegetationBlocks, int vegetationColumns,
|
||||
int foundationBaseColumns, int foundationBlocks,
|
||||
int foundationColumns,
|
||||
int foundationGapColumns) {
|
||||
}
|
||||
|
||||
record PoiAudit(int inBounds, int outOfBounds) {
|
||||
}
|
||||
}
|
||||
@@ -25,10 +25,33 @@ import art.arcane.iris.modded.command.ModdedPregenJob;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
|
||||
/**
|
||||
* Entry point for mods integrating with Iris on Fabric, Forge and NeoForge.
|
||||
* <p>
|
||||
* Everything here is static and null-tolerant: a null or non-Iris {@link ServerLevel} produces false, null, or a
|
||||
* no-op rather than an exception, so a caller never has to pre-check whether a level is generated by Iris.
|
||||
* <p>
|
||||
* <b>Threading.</b> {@link #isIrisLevel(ServerLevel)}, {@link #isStudioLevel(ServerLevel)} and
|
||||
* {@link #getEngine(ServerLevel)} read the level's chunk generator reference and are safe from any thread once
|
||||
* the level is loaded. The mantle accessors are safe off the server thread but touch engine storage - see their
|
||||
* own notes. {@link #pregenerate(ServerLevel, int)} and {@link #registerProvider(ModdedDataProvider)} mutate
|
||||
* global state and belong on the server thread, during mod setup or from a command.
|
||||
* <p>
|
||||
* <b>Stability.</b> This class and the {@code Modded*} types beside it are the intended integration surface. The
|
||||
* types they expose from {@code art.arcane.iris.engine.*} and {@code art.arcane.iris.core.*} - notably
|
||||
* {@link Engine} - are internal to Iris and change without a deprecation cycle. Treat {@link Engine} as an opaque
|
||||
* token to hand back to Iris, and prefer the wrappers here over reaching into it.
|
||||
*
|
||||
* @see ModdedDataProvider for supplying custom blocks, items and entities to the generator
|
||||
*/
|
||||
public final class IrisModdedAPI {
|
||||
private IrisModdedAPI() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether {@code level}'s chunk generator is an Iris generator. False for null and for every vanilla or
|
||||
* third-party generated level. The cheapest available Iris check.
|
||||
*/
|
||||
public static boolean isIrisLevel(ServerLevel level) {
|
||||
if (level == null) {
|
||||
return false;
|
||||
@@ -36,11 +59,22 @@ public final class IrisModdedAPI {
|
||||
return level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether {@code level} is an Iris studio level - a throwaway world opened for pack authoring, which is
|
||||
* deleted on shutdown. Persist nothing against one. False for null and for non-Iris levels.
|
||||
*/
|
||||
public static boolean isStudioLevel(ServerLevel level) {
|
||||
Engine engine = getEngine(level);
|
||||
return engine != null && engine.isStudio();
|
||||
}
|
||||
|
||||
/**
|
||||
* The Iris engine driving {@code level}, or null when the level is null, is not Iris-generated, or its engine
|
||||
* is not currently available - during shutdown, or while the generator is still binding.
|
||||
* <p>
|
||||
* Never cached: resolve per use. Reloading a pack or unloading the level replaces the engine, and a stale
|
||||
* reference goes inert. {@link Engine} is internal to Iris; see the stability note on this class.
|
||||
*/
|
||||
public static Engine getEngine(ServerLevel level) {
|
||||
if (level == null) {
|
||||
return null;
|
||||
@@ -56,10 +90,27 @@ public final class IrisModdedAPI {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a cached, asynchronous pregeneration of {@code radiusBlocks} around the world origin.
|
||||
* Equivalent to {@code pregenerate(level, radiusBlocks, 0, 0, false, true)}.
|
||||
*/
|
||||
public static boolean pregenerate(ServerLevel level, int radiusBlocks) {
|
||||
return pregenerate(level, radiusBlocks, 0, 0, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a pregeneration job over a square region.
|
||||
* <p>
|
||||
* Returns as soon as the job is queued; progress is reported through Iris's own logging and boss bar, not to
|
||||
* the caller. Only one job runs server-wide, so this returns false if one is already active. Call on the
|
||||
* server thread.
|
||||
*
|
||||
* @param radiusBlocks half-extent of the square in blocks, measured from the centre
|
||||
* @param sync write chunks synchronously; slower but avoids the async write queue
|
||||
* @param cached reuse and update the on-disk pregeneration cache so an interrupted job resumes instead of
|
||||
* regenerating
|
||||
* @return false when {@code level} is not Iris-generated or another pregeneration job is already running
|
||||
*/
|
||||
public static boolean pregenerate(ServerLevel level, int radiusBlocks, int centerBlockX, int centerBlockZ, boolean sync, boolean cached) {
|
||||
Engine engine = getEngine(level);
|
||||
if (engine == null) {
|
||||
@@ -68,6 +119,19 @@ public final class IrisModdedAPI {
|
||||
return ModdedPregenJob.start(level.getServer(), level, engine, radiusBlocks, centerBlockX, centerBlockZ, false, sync, cached);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a mantle value of {@code type} at world coordinates.
|
||||
* <p>
|
||||
* The mantle is Iris's own per-block sidecar storage, independent of chunk NBT, and it is how Iris carries
|
||||
* data that must survive between generation stages. Coordinates are world-space: {@code y} is translated by
|
||||
* the engine's minimum height internally.
|
||||
* <p>
|
||||
* Returns null when the level is not Iris-generated, no mantle region exists for that column yet - reads
|
||||
* never create or load one - or nothing of {@code type} is stored there. A {@code y} outside the engine's
|
||||
* height range reads as null rather than throwing.
|
||||
*
|
||||
* @throws IllegalStateException if the engine's mantle has already been closed
|
||||
*/
|
||||
public static <T> T getMantleData(ServerLevel level, int x, int y, int z, Class<T> type) {
|
||||
Engine engine = getEngine(level);
|
||||
if (engine == null) {
|
||||
@@ -76,6 +140,19 @@ public final class IrisModdedAPI {
|
||||
return engine.getMantle().getMantle().get(x, y - engine.getMinHeight(), z, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a mantle value at world coordinates, replacing any previous value of the same type there.
|
||||
* <p>
|
||||
* Unlike {@link #getMantleData(ServerLevel, int, int, int, Class)}, a write creates the mantle region if it
|
||||
* does not exist, which can touch disk - do not call it per block in a tick loop from the server thread. A
|
||||
* null {@code data}, a non-Iris level, or a {@code y} outside the engine's height range is a silent no-op;
|
||||
* remove values with {@link #deleteMantleData(ServerLevel, int, int, int, Class)}.
|
||||
* <p>
|
||||
* Values written under a custom type are discarded when Iris trims a mantle region unless the type is
|
||||
* declared with {@link #retainMantleDataForSlice(Class)}.
|
||||
*
|
||||
* @throws IllegalStateException if the engine's mantle has already been closed
|
||||
*/
|
||||
public static <T> void setMantleData(ServerLevel level, int x, int y, int z, T data) {
|
||||
Engine engine = getEngine(level);
|
||||
if (engine == null || data == null) {
|
||||
@@ -84,6 +161,12 @@ public final class IrisModdedAPI {
|
||||
engine.getMantle().getMantle().set(x, y - engine.getMinHeight(), z, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes any mantle value of {@code type} at world coordinates. A non-Iris level or an out-of-range
|
||||
* {@code y} is a silent no-op. Like a write, this creates the mantle region if it is absent.
|
||||
*
|
||||
* @throws IllegalStateException if the engine's mantle has already been closed
|
||||
*/
|
||||
public static <T> void deleteMantleData(ServerLevel level, int x, int y, int z, Class<T> type) {
|
||||
Engine engine = getEngine(level);
|
||||
if (engine == null) {
|
||||
@@ -92,6 +175,14 @@ public final class IrisModdedAPI {
|
||||
engine.getMantle().getMantle().remove(x, y - engine.getMinHeight(), z, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Declares that mantle slices of {@code sliceType} must be kept rather than discarded.
|
||||
* <p>
|
||||
* Iris drops slices it does not need once a region's generation data has served its purpose. Any type a mod
|
||||
* writes with {@link #setMantleData(ServerLevel, int, int, int, Object)} and expects to read back later must be
|
||||
* declared here first. Registration is by canonical class name, process-wide across every Iris world, and
|
||||
* cannot be undone - declare it once during mod setup. A null {@code sliceType} is ignored.
|
||||
*/
|
||||
public static void retainMantleDataForSlice(Class<?> sliceType) {
|
||||
if (sliceType == null) {
|
||||
return;
|
||||
@@ -99,10 +190,31 @@ public final class IrisModdedAPI {
|
||||
WorldMaintenance.retainMantleDataForSlice(sliceType.getCanonicalName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a custom content provider imperatively, for mods that would rather call Iris than ship a
|
||||
* {@link java.util.ServiceLoader} entry.
|
||||
* <p>
|
||||
* Providers are keyed by {@link ModdedDataProvider#modId()}; a second registration under an id already present
|
||||
* is logged and ignored. {@link ModdedDataProvider#init()} runs during this call, and a throwable it raises is
|
||||
* logged rather than propagated. A null {@code provider} is ignored.
|
||||
* <p>
|
||||
* Ordering matters: Iris only consults providers registered before a pack resolves the block in question, so
|
||||
* register during mod setup. Registering after Iris's own {@link java.util.ServiceLoader} discovery is
|
||||
* supported; registering after a world has generated is not - blocks already resolved are not revisited.
|
||||
*/
|
||||
public static void registerProvider(ModdedDataProvider provider) {
|
||||
ModdedCustomContentRegistry.register(provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a custom {@code namespace:key} onto a fixed vanilla block state, for mods that only need a static alias
|
||||
* and no provider class.
|
||||
* <p>
|
||||
* {@code state} is a block state string in the same syntax packs use, for example
|
||||
* {@code minecraft:oak_log[axis=y]}, and is parsed immediately: an unparseable state or an invalid identifier
|
||||
* is logged and the registration is dropped, so a typo shows up at startup rather than as missing blocks.
|
||||
* Aliases take precedence over provider lookups for the same key. Null arguments are ignored.
|
||||
*/
|
||||
public static void registerCustomBlockData(String namespace, String key, String state) {
|
||||
ModdedCustomContentRegistry.registerCustomBlockData(namespace, key, state);
|
||||
}
|
||||
|
||||
@@ -22,15 +22,39 @@ import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A provider's answer to a block lookup: the state to write, and whether the provider wants a second pass once the
|
||||
* chunk is loaded.
|
||||
* <p>
|
||||
* Immutable. Returned from {@link ModdedDataProvider#getBlockData(net.minecraft.resources.Identifier, java.util.Map)};
|
||||
* construct with {@link #direct(BlockState)} or {@link #deferred(BlockState)} rather than the canonical constructor.
|
||||
*
|
||||
* @param state the block state Iris writes. Never null
|
||||
* @param deferredPlacement whether {@link ModdedDataProvider#processBlockPlacement(ModdedBlockPlacementContext)}
|
||||
* should run for this position after the chunk is loaded
|
||||
*/
|
||||
public record ModdedBlockData(BlockState state, boolean deferredPlacement) {
|
||||
/**
|
||||
* @throws NullPointerException if {@code state} is null
|
||||
*/
|
||||
public ModdedBlockData {
|
||||
Objects.requireNonNull(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* The state is final - Iris writes it during generation and does nothing further.
|
||||
*/
|
||||
public static ModdedBlockData direct(BlockState state) {
|
||||
return new ModdedBlockData(state, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code state} is a placeholder written during generation; the provider finishes the job in
|
||||
* {@link ModdedDataProvider#processBlockPlacement(ModdedBlockPlacementContext)} once the chunk is loaded. Use
|
||||
* when the real block needs a level - a block entity, neighbour state, or mod registries not available on a
|
||||
* generation thread. Pick a placeholder with the same shape and occlusion as the final block so terrain around
|
||||
* it generates correctly.
|
||||
*/
|
||||
public static ModdedBlockData deferred(BlockState state) {
|
||||
return new ModdedBlockData(state, true);
|
||||
}
|
||||
|
||||
+19
@@ -27,6 +27,22 @@ import net.minecraft.world.level.block.state.BlockState;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Everything a provider needs to finish a deferred block placement, handed to
|
||||
* {@link ModdedDataProvider#processBlockPlacement(ModdedBlockPlacementContext)} on the server thread.
|
||||
* <p>
|
||||
* Immutable, and constructed by Iris rather than by mods. {@code state} is defensively copied; {@code position} is
|
||||
* already immutable. Because delivery is on the server thread with the chunk loaded, it is safe to write blocks,
|
||||
* attach block entities and read neighbours from here.
|
||||
*
|
||||
* @param engine the Iris engine for this level. Internal Iris type - treat it as an opaque token
|
||||
* @param level the level to write into. Never null
|
||||
* @param position the block the placeholder was written at. Never null
|
||||
* @param blockId the identifier the pack named, without state properties. Never null
|
||||
* @param blockState the state currently at {@code position} - normally the placeholder returned as deferred, though
|
||||
* another provider or a later generation stage may have replaced it. Never null
|
||||
* @param state the {@code [prop=value]} pairs from the pack's key, possibly empty. Never null; unmodifiable
|
||||
*/
|
||||
public record ModdedBlockPlacementContext(
|
||||
Engine engine,
|
||||
ServerLevel level,
|
||||
@@ -34,6 +50,9 @@ public record ModdedBlockPlacementContext(
|
||||
Identifier blockId,
|
||||
Map<String, String> state,
|
||||
BlockState blockState) {
|
||||
/**
|
||||
* @throws NullPointerException if any component is null
|
||||
*/
|
||||
public ModdedBlockPlacementContext {
|
||||
Objects.requireNonNull(engine);
|
||||
Objects.requireNonNull(level);
|
||||
|
||||
+79
-1
@@ -37,6 +37,22 @@ import java.util.ServiceLoader;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* Registry of {@link ModdedDataProvider} instances and static block-data aliases, and the resolution path Iris
|
||||
* itself calls into.
|
||||
* <p>
|
||||
* Mods should go through {@link IrisModdedAPI#registerProvider(ModdedDataProvider)} and
|
||||
* {@link IrisModdedAPI#registerCustomBlockData(String, String, String)} rather than calling this class directly;
|
||||
* the resolution methods here are Iris internals and are public only because the adapter's generation code lives in
|
||||
* another package.
|
||||
* <p>
|
||||
* <b>Threading.</b> Mutation ({@link #register(ModdedDataProvider)},
|
||||
* {@link #registerCustomBlockData(String, String, String)}, {@link #discover()}) is serialized on the class
|
||||
* monitor. Resolution ({@link #resolveBlock(String)}, {@link #spawnMob}, {@link #processBlockPlacement}) is lock
|
||||
* free over a copy-on-write provider list and a concurrent alias map, so it runs on generation threads. Every
|
||||
* resolution method catches provider throwables, logs them against the provider's mod id, and continues with the
|
||||
* next provider.
|
||||
*/
|
||||
public final class ModdedCustomContentRegistry {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final List<ModdedDataProvider> PROVIDERS = new CopyOnWriteArrayList<>();
|
||||
@@ -47,6 +63,11 @@ public final class ModdedCustomContentRegistry {
|
||||
private ModdedCustomContentRegistry() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a static {@code namespace:key} to block-state alias. Invalid identifiers and unparseable states are
|
||||
* logged and dropped; null arguments are ignored. See
|
||||
* {@link IrisModdedAPI#registerCustomBlockData(String, String, String)}.
|
||||
*/
|
||||
public static synchronized void registerCustomBlockData(String namespace, String key, String state) {
|
||||
if (namespace == null || key == null || state == null) {
|
||||
return;
|
||||
@@ -72,6 +93,11 @@ public final class ModdedCustomContentRegistry {
|
||||
LOGGER.info("Iris registered custom block data {}:{} -> {}", namespace, key, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a provider, rejecting a duplicate {@link ModdedDataProvider#modId()} with a warning and ignoring
|
||||
* null. {@link ModdedDataProvider#init()} runs here; a throwable it raises is logged, not propagated. See
|
||||
* {@link IrisModdedAPI#registerProvider(ModdedDataProvider)}.
|
||||
*/
|
||||
public static synchronized void register(ModdedDataProvider provider) {
|
||||
if (provider == null) {
|
||||
return;
|
||||
@@ -96,6 +122,15 @@ public final class ModdedCustomContentRegistry {
|
||||
LOGGER.info("Iris registered custom content provider '{}'", provider.modId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs {@link ServiceLoader} discovery for {@link ModdedDataProvider} against Iris's own class loader, once per
|
||||
* process. Called during Iris mod initialization; a second call is a no-op that returns an inert handle.
|
||||
* <p>
|
||||
* All-or-nothing: a provider whose {@link ModdedDataProvider#init()} throws aborts the pass, restores the
|
||||
* previous provider and alias state, logs the failing provider's identity, and rethrows.
|
||||
*
|
||||
* @return a handle whose {@link Discovery#rollback()} undoes this pass, used by the bootstrap's rollback chain
|
||||
*/
|
||||
public static synchronized Discovery discover() {
|
||||
if (scanned) {
|
||||
return Discovery.unchanged();
|
||||
@@ -110,9 +145,12 @@ public final class ModdedCustomContentRegistry {
|
||||
boolean previousDiscoveryComplete = scanned;
|
||||
DiscoveryBatch batch = new DiscoveryBatch(previousProviders, previousCustomBlocks);
|
||||
discoveryBatch = batch;
|
||||
ModdedDataProvider failingProvider = null;
|
||||
try {
|
||||
for (ModdedDataProvider provider : discoveredProviders) {
|
||||
failingProvider = provider;
|
||||
batch.add(provider);
|
||||
failingProvider = null;
|
||||
}
|
||||
PROVIDERS.addAll(batch.additions);
|
||||
CUSTOM_BLOCKS.putAll(batch.customBlocks);
|
||||
@@ -130,7 +168,8 @@ public final class ModdedCustomContentRegistry {
|
||||
failure.addSuppressed(rollbackFailure);
|
||||
}
|
||||
}
|
||||
LOGGER.error("Iris custom content provider discovery failed", failure);
|
||||
LOGGER.warn("Iris custom content provider discovery failed at {}",
|
||||
providerIdentity(failingProvider), failure);
|
||||
if (failure instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
@@ -143,6 +182,19 @@ public final class ModdedCustomContentRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
private static String providerIdentity(ModdedDataProvider provider) {
|
||||
if (provider == null) {
|
||||
return "the provider service loader";
|
||||
}
|
||||
String className = provider.getClass().getName();
|
||||
try {
|
||||
String modId = provider.modId();
|
||||
return modId == null || modId.isBlank() ? className : "provider '" + modId + "' (" + className + ")";
|
||||
} catch (Throwable identityFailure) {
|
||||
return className;
|
||||
}
|
||||
}
|
||||
|
||||
static synchronized boolean hasProvider(String modId) {
|
||||
for (ModdedDataProvider provider : PROVIDERS) {
|
||||
if (Objects.equals(provider.modId(), modId)) {
|
||||
@@ -156,10 +208,19 @@ public final class ModdedCustomContentRegistry {
|
||||
return scanned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether any provider or alias is registered. Iris checks this to skip custom resolution entirely on a server
|
||||
* with no integrating mods.
|
||||
*/
|
||||
public static boolean hasProviders() {
|
||||
return !PROVIDERS.isEmpty() || !CUSTOM_BLOCKS.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a pack block key against aliases first, then each ready provider that claims it, in registration
|
||||
* order. {@code key} may carry {@code [prop=value]} properties, which are parsed and passed along. Returns null
|
||||
* when nothing claims it, which lets the caller fall back to air. Called from generation threads.
|
||||
*/
|
||||
public static ModdedBlockData resolveBlock(String key) {
|
||||
if (key == null || (PROVIDERS.isEmpty() && CUSTOM_BLOCKS.isEmpty())) {
|
||||
return null;
|
||||
@@ -192,6 +253,11 @@ public final class ModdedCustomContentRegistry {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delivers a deferred placement to the first ready provider claiming {@code key}; later providers are not
|
||||
* consulted for that position. An unparseable key or no matching provider is logged and skipped. Called on the
|
||||
* server thread with the chunk loaded.
|
||||
*/
|
||||
public static void processBlockPlacement(Engine engine, ServerLevel level, BlockPos position, String key) {
|
||||
Identifier base = parseIdentifier(key);
|
||||
if (base == null) {
|
||||
@@ -214,6 +280,10 @@ public final class ModdedCustomContentRegistry {
|
||||
LOGGER.warn("Iris deferred custom block placement has no provider for {}", key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks each ready provider claiming {@code key} to spawn a custom entity, returning the first non-null result.
|
||||
* Null when no provider claims it or every attempt declined. Called on the server thread.
|
||||
*/
|
||||
public static Entity spawnMob(ServerLevel level, double x, double y, double z, String key) {
|
||||
if (PROVIDERS.isEmpty() || level == null || key == null) {
|
||||
return null;
|
||||
@@ -276,6 +346,10 @@ public final class ModdedCustomContentRegistry {
|
||||
scanned = discoveryComplete;
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo handle for one {@link #discover()} pass, so a failure later in Iris's bootstrap can restore the registry
|
||||
* to its pre-discovery state.
|
||||
*/
|
||||
public static final class Discovery {
|
||||
private final List<ModdedDataProvider> providers;
|
||||
private final Map<String, BlockState> customBlocks;
|
||||
@@ -294,6 +368,10 @@ public final class ModdedCustomContentRegistry {
|
||||
return new Discovery(List.of(), Map.of(), true, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the providers and aliases captured before the pass. Idempotent; a no-op on a handle from a
|
||||
* discovery that did not run.
|
||||
*/
|
||||
public synchronized void rollback() {
|
||||
if (!active) {
|
||||
return;
|
||||
|
||||
+63
@@ -25,28 +25,91 @@ import net.minecraft.world.entity.Entity;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Extension point letting a mod resolve its own blocks, items and entities for Iris packs, so a pack can name
|
||||
* {@code yourmod:something} and have it placed.
|
||||
* <p>
|
||||
* Discovered through {@link java.util.ServiceLoader} at Iris mod initialization, or registered imperatively with
|
||||
* {@link IrisModdedAPI#registerProvider(ModdedDataProvider)}. For ServiceLoader discovery, ship
|
||||
* {@code META-INF/services/art.arcane.iris.modded.api.ModdedDataProvider} listing the implementation's binary
|
||||
* name; the class needs a public no-argument constructor.
|
||||
* <p>
|
||||
* <b>Threading.</b> Implementations must be thread-safe.
|
||||
* {@link #getBlockData(Identifier, Map)} is called from generation threads, potentially many at once, for every
|
||||
* unresolved key a pack names - it must be fast and must not touch world state.
|
||||
* {@link #processBlockPlacement(ModdedBlockPlacementContext)} and
|
||||
* {@link #spawnMob(ServerLevel, double, double, double, Identifier)} are called on the server thread, where
|
||||
* touching the level is safe.
|
||||
* <p>
|
||||
* <b>Failure handling.</b> Iris catches throwables from every callback except {@link #init()} during
|
||||
* ServiceLoader discovery, logs them against {@link #modId()}, and carries on with the remaining providers - one
|
||||
* broken provider does not stop world generation. A throwable from {@link #init()} during discovery aborts
|
||||
* discovery and rolls back every provider registered in that pass.
|
||||
*/
|
||||
public interface ModdedDataProvider {
|
||||
/**
|
||||
* The owning mod's id. Used as the provider's identity: duplicates are rejected, and it labels every log line
|
||||
* Iris emits about this provider. Must be non-null and stable; returning null aborts discovery.
|
||||
*/
|
||||
String modId();
|
||||
|
||||
/**
|
||||
* Whether this provider can answer lookups yet. Iris skips a provider that reports false rather than treating
|
||||
* it as absent, so a provider whose registries populate late can gate itself instead of returning wrong
|
||||
* answers. Defaults to true.
|
||||
*/
|
||||
default boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every identifier this provider can supply for {@code type}. Used for command suggestion and pack tooling, not
|
||||
* on the resolution path - {@link #isValidProvider(Identifier, ModdedDataType)} decides that. Return an empty
|
||||
* collection rather than null.
|
||||
*/
|
||||
Collection<Identifier> getTypes(ModdedDataType type);
|
||||
|
||||
/**
|
||||
* Whether this provider claims {@code id} for {@code type}. Called before every resolution callback, on
|
||||
* generation threads, so keep it to a set lookup. A cheap namespace check is usually enough.
|
||||
*/
|
||||
boolean isValidProvider(Identifier id, ModdedDataType type);
|
||||
|
||||
/**
|
||||
* Resolves a claimed block identifier into a concrete block state.
|
||||
* <p>
|
||||
* {@code state} holds the {@code [prop=value]} pairs from the pack's key, already parsed and possibly empty;
|
||||
* never null. Return null to decline, in which case Iris tries the next provider and finally falls back to air.
|
||||
* Return {@link ModdedBlockData#deferred(net.minecraft.world.level.block.state.BlockState)} when the real block
|
||||
* needs a loaded level - Iris then writes the placeholder state and calls
|
||||
* {@link #processBlockPlacement(ModdedBlockPlacementContext)} later. Called from generation threads.
|
||||
*/
|
||||
default ModdedBlockData getBlockData(Identifier blockId, Map<String, String> state) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finishes a deferred placement once the chunk is loaded: swap in the real block, attach a block entity, seed
|
||||
* NBT.
|
||||
* <p>
|
||||
* Called on the server thread, once per deferred position, by the first provider that claims the identifier -
|
||||
* later providers are not consulted for that position. Only fires for states returned as deferred.
|
||||
*/
|
||||
default void processBlockPlacement(ModdedBlockPlacementContext context) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawns a claimed custom entity at the given position. Return null to decline and let the next provider try.
|
||||
* Called on the server thread from Iris's entity spawning.
|
||||
*/
|
||||
default Entity spawnMob(ServerLevel level, double x, double y, double z, Identifier entityId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* One-time setup, called by Iris immediately after this provider is accepted. A throwable raised here aborts
|
||||
* ServiceLoader discovery; when registered imperatively it is logged and the provider stays registered.
|
||||
*/
|
||||
default void init() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,16 @@
|
||||
|
||||
package art.arcane.iris.modded.api;
|
||||
|
||||
/**
|
||||
* The kinds of custom content a {@link ModdedDataProvider} can claim.
|
||||
* <p>
|
||||
* Constants may be added. Switch expressions over this enum need a {@code default} arm.
|
||||
*/
|
||||
public enum ModdedDataType {
|
||||
/** Block states, resolved through {@link ModdedDataProvider#getBlockData(net.minecraft.resources.Identifier, java.util.Map)}. */
|
||||
BLOCK,
|
||||
/** Item types, claimed for loot and pack tooling. */
|
||||
ITEM,
|
||||
/** Entity types, spawned through {@link ModdedDataProvider#spawnMob(net.minecraft.server.level.ServerLevel, double, double, double, net.minecraft.resources.Identifier)}. */
|
||||
ENTITY
|
||||
}
|
||||
|
||||
+27
-1092
File diff suppressed because it is too large
Load Diff
+191
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.suggestion.SuggestionProvider;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.SharedSuggestionProvider;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
final class ModdedCommandSuggestions {
|
||||
static final SuggestionProvider<CommandSourceStack> BIOME_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestBiomeKeys(context, builder);
|
||||
static final SuggestionProvider<CommandSourceStack> REGION_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestRegionKeys(context, builder);
|
||||
static final SuggestionProvider<CommandSourceStack> OBJECT_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestObjectKeys(context, builder);
|
||||
static final SuggestionProvider<CommandSourceStack> STRUCTURE_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestStructureKeys(context, builder);
|
||||
static final SuggestionProvider<CommandSourceStack> POI_TYPES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> SharedSuggestionProvider.suggest(List.of("buried_treasure"), builder);
|
||||
static final SuggestionProvider<CommandSourceStack> PACK_NAMES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestPackNames(context, builder);
|
||||
static final SuggestionProvider<CommandSourceStack> DIMENSION_NAMES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestDimensionNames(context, builder);
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final int TAB_FAILURE_KEYS_MAX = 256;
|
||||
private static final Set<String> REPORTED_TAB_FAILURES = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private ModdedCommandSuggestions() {
|
||||
}
|
||||
|
||||
private static CompletableFuture<Suggestions> suggestBiomeKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
|
||||
ModdedCommandFeedback.tab(context.getSource());
|
||||
try {
|
||||
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
|
||||
if (engine != null) {
|
||||
return SharedSuggestionProvider.suggest(engine.getData().getBiomeLoader().getPossibleKeys(), builder);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
warnTabFailure("biome keys", context.getSource(), e);
|
||||
}
|
||||
return builder.buildFuture();
|
||||
}
|
||||
|
||||
private static CompletableFuture<Suggestions> suggestRegionKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
|
||||
ModdedCommandFeedback.tab(context.getSource());
|
||||
try {
|
||||
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
|
||||
if (engine != null) {
|
||||
return SharedSuggestionProvider.suggest(engine.getDimension().getRegions(), builder);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
warnTabFailure("region keys", context.getSource(), e);
|
||||
}
|
||||
return builder.buildFuture();
|
||||
}
|
||||
|
||||
private static CompletableFuture<Suggestions> suggestObjectKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
|
||||
ModdedCommandFeedback.tab(context.getSource());
|
||||
try {
|
||||
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
|
||||
if (engine != null) {
|
||||
return SharedSuggestionProvider.suggest(engine.getData().getObjectLoader().getPossibleKeys(), builder);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
warnTabFailure("object keys", context.getSource(), e);
|
||||
}
|
||||
return builder.buildFuture();
|
||||
}
|
||||
|
||||
static CompletableFuture<Suggestions> suggestStructureKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
|
||||
ModdedCommandFeedback.tab(context.getSource());
|
||||
try {
|
||||
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
|
||||
Collection<String> irisKeys = engine == null ? List.of() : IrisStructureLocator.placedKeys(engine);
|
||||
Registry<Structure> registry = context.getSource().getServer().registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
List<String> nativeKeys = new ArrayList<>(registry.keySet().size());
|
||||
for (Identifier identifier : registry.keySet()) {
|
||||
nativeKeys.add(identifier.toString());
|
||||
}
|
||||
return SharedSuggestionProvider.suggest(combineStructureKeys(irisKeys, nativeKeys), builder);
|
||||
} catch (Throwable e) {
|
||||
warnTabFailure("structure keys", context.getSource(), e);
|
||||
}
|
||||
return builder.buildFuture();
|
||||
}
|
||||
|
||||
static void warnTabFailure(String suggestion, CommandSourceStack source, Throwable error) {
|
||||
String origin = tabOrigin(source);
|
||||
if (!REPORTED_TAB_FAILURES.add(suggestion + '|' + origin + '|' + error.getClass().getName())) {
|
||||
return;
|
||||
}
|
||||
if (REPORTED_TAB_FAILURES.size() > TAB_FAILURE_KEYS_MAX) {
|
||||
REPORTED_TAB_FAILURES.clear();
|
||||
}
|
||||
LOGGER.warn("Iris tab-complete for {} in {} failed; suggestions will be empty", suggestion, origin, error);
|
||||
}
|
||||
|
||||
private static String tabOrigin(CommandSourceStack source) {
|
||||
if (source == null) {
|
||||
return "<no source>";
|
||||
}
|
||||
try {
|
||||
return source.getLevel().dimension().identifier().toString();
|
||||
} catch (Throwable originFailure) {
|
||||
return "<no level>";
|
||||
}
|
||||
}
|
||||
|
||||
static List<String> combineStructureKeys(Collection<String> irisKeys, Collection<String> nativeKeys) {
|
||||
Set<String> combined = new TreeSet<>();
|
||||
combined.addAll(irisKeys);
|
||||
combined.addAll(nativeKeys);
|
||||
return List.copyOf(combined);
|
||||
}
|
||||
|
||||
private static CompletableFuture<Suggestions> suggestPackNames(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
|
||||
ModdedCommandFeedback.tab(context.getSource());
|
||||
Set<String> names = new TreeSet<>();
|
||||
names.add("overworld");
|
||||
try {
|
||||
File packs = ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("packs").toFile();
|
||||
File[] children = packs.listFiles();
|
||||
if (children != null) {
|
||||
for (File child : children) {
|
||||
if (!child.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
String packName = child.getName();
|
||||
names.add(packName);
|
||||
File dimensions = new File(child, "dimensions");
|
||||
File[] dimensionFiles = dimensions.listFiles(
|
||||
(File directory, String name) -> name.endsWith(".json"));
|
||||
if (dimensionFiles == null) {
|
||||
continue;
|
||||
}
|
||||
for (File dimensionFile : dimensionFiles) {
|
||||
String fileName = dimensionFile.getName();
|
||||
names.add(packName + ":" + fileName.substring(0, fileName.length() - 5));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
warnTabFailure("pack names", context.getSource(), e);
|
||||
}
|
||||
return SharedSuggestionProvider.suggest(names, builder);
|
||||
}
|
||||
|
||||
private static CompletableFuture<Suggestions> suggestDimensionNames(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
|
||||
ModdedCommandFeedback.tab(context.getSource());
|
||||
List<String> names = new ArrayList<>();
|
||||
for (ServerLevel level : context.getSource().getServer().getAllLevels()) {
|
||||
if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator) {
|
||||
names.add(level.dimension().identifier().toString());
|
||||
}
|
||||
}
|
||||
return SharedSuggestionProvider.suggest(names, builder);
|
||||
}
|
||||
}
|
||||
+344
@@ -0,0 +1,344 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import com.mojang.brigadier.arguments.BoolArgumentType;
|
||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||
import com.mojang.brigadier.arguments.LongArgumentType;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.ArgumentBuilder;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.commands.arguments.DimensionArgument;
|
||||
import net.minecraft.commands.arguments.EntityArgument;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
final class ModdedCommandTree {
|
||||
private static final Predicate<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
|
||||
|
||||
private ModdedCommandTree() {
|
||||
}
|
||||
|
||||
static LiteralArgumentBuilder<CommandSourceStack> rootTree() {
|
||||
LiteralArgumentBuilder<CommandSourceStack> root = Commands.literal("iris");
|
||||
|
||||
root.executes((CommandContext<CommandSourceStack> context) -> ModdedCommandHelp.send(context.getSource(), ""));
|
||||
root.then(helpTree());
|
||||
|
||||
root.then(Commands.literal("version")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.version(context.getSource())));
|
||||
|
||||
root.then(Commands.literal("info").requires(GATE)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.info(context.getSource(), null))
|
||||
.then(Commands.argument("dimension", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.DIMENSION_NAMES)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.info(context.getSource(), StringArgumentType.getString(context, "dimension")))));
|
||||
|
||||
root.then(ModdedWhatCommands.tree());
|
||||
|
||||
root.then(teleportTree("teleport"));
|
||||
root.then(teleportTree("tp"));
|
||||
|
||||
root.then(Commands.literal("evacuate").requires(GATE)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.evacuate(context.getSource(), null))
|
||||
.then(Commands.argument("dimension", DimensionArgument.dimension()).suggests(ModdedCommandSuggestions.DIMENSION_NAMES)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.evacuate(context.getSource(), DimensionArgument.getDimension(context, "dimension")))));
|
||||
|
||||
root.then(Commands.literal("debug").requires(GATE)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.debug(context.getSource())));
|
||||
|
||||
root.then(Commands.literal("reload").requires(GATE)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.reload(context.getSource())));
|
||||
root.then(Commands.literal("height").requires(GATE)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.height(context.getSource())));
|
||||
root.then(Commands.literal("worlds").requires(GATE)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.info(context.getSource(), null)));
|
||||
root.then(Commands.literal("accesslist").requires(GATE)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.info(context.getSource(), null)));
|
||||
|
||||
root.then(gotoTree("goto"));
|
||||
root.then(gotoTree("find"));
|
||||
|
||||
root.then(Commands.literal("seed").requires(GATE)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.seed(context.getSource())));
|
||||
|
||||
root.then(goldenhashTree("goldenhash"));
|
||||
root.then(goldenhashTree("gold"));
|
||||
|
||||
root.then(downloadTree("download"));
|
||||
root.then(downloadTree("dl"));
|
||||
|
||||
root.then(metricsTree("metrics"));
|
||||
root.then(metricsTree("measure"));
|
||||
|
||||
root.then(regenTree("regen"));
|
||||
root.then(regenTree("rg"));
|
||||
|
||||
root.then(pregenTree("pregen"));
|
||||
root.then(pregenTree("pregenerate"));
|
||||
|
||||
root.then(Commands.literal("wand").requires(GATE)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedObjectCommands.giveWand(context.getSource())));
|
||||
root.then(Commands.literal("dust").requires(GATE)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedObjectCommands.giveDust(context.getSource())));
|
||||
root.then(Commands.literal("d").requires(GATE)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedObjectCommands.giveDust(context.getSource())));
|
||||
root.then(ModdedObjectCommands.tree("object"));
|
||||
root.then(ModdedObjectCommands.tree("o"));
|
||||
root.then(editTree());
|
||||
|
||||
root.then(createTree("create"));
|
||||
root.then(createTree("c"));
|
||||
|
||||
root.then(ModdedStudioCommands.tree("studio"));
|
||||
root.then(ModdedStudioCommands.tree("std"));
|
||||
root.then(ModdedStudioCommands.tree("s"));
|
||||
root.then(ModdedPackCommands.tree("pack"));
|
||||
root.then(ModdedPackCommands.tree("pk"));
|
||||
root.then(ModdedWorldCommands.tree("world"));
|
||||
root.then(ModdedWorldCommands.tree("w"));
|
||||
root.then(ModdedDatapackCommands.tree("datapack"));
|
||||
root.then(ModdedDatapackCommands.tree("datapacks"));
|
||||
root.then(ModdedDatapackCommands.tree("dp"));
|
||||
root.then(ModdedStructureCommands.tree("structure"));
|
||||
root.then(ModdedStructureCommands.tree("struct"));
|
||||
root.then(ModdedStructureCommands.tree("str"));
|
||||
root.then(ModdedDeveloperCommands.tree("developer"));
|
||||
root.then(ModdedDeveloperCommands.tree("dev"));
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> createTree(String name) {
|
||||
return Commands.literal(name).requires(GATE)
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes((CommandContext<CommandSourceStack> context) ->
|
||||
ModdedWorldCommands.createWorld(
|
||||
context.getSource(),
|
||||
StringArgumentType.getString(context, "name"),
|
||||
"overworld",
|
||||
1337L))
|
||||
.then(Commands.argument("pack", StringArgumentType.string()).suggests(ModdedCommandSuggestions.PACK_NAMES)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedWorldCommands.createWorld(context.getSource(),
|
||||
StringArgumentType.getString(context, "name"),
|
||||
StringArgumentType.getString(context, "pack"),
|
||||
1337L))
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedWorldCommands.createWorld(context.getSource(),
|
||||
StringArgumentType.getString(context, "name"),
|
||||
StringArgumentType.getString(context, "pack"),
|
||||
LongArgumentType.getLong(context, "seed"))))));
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> teleportTree(String name) {
|
||||
return Commands.literal(name).requires(GATE)
|
||||
.then(Commands.argument("dimension", DimensionArgument.dimension()).suggests(ModdedCommandSuggestions.DIMENSION_NAMES)
|
||||
.executes((CommandContext<CommandSourceStack> context) ->
|
||||
IrisModdedCommands.tp(context.getSource(), DimensionArgument.getDimension(context, "dimension"), null))
|
||||
.then(Commands.argument("player", EntityArgument.player())
|
||||
.executes((CommandContext<CommandSourceStack> context) ->
|
||||
IrisModdedCommands.tp(context.getSource(), DimensionArgument.getDimension(context, "dimension"),
|
||||
EntityArgument.getPlayer(context, "player")))));
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> helpTree() {
|
||||
return Commands.literal("help")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedCommandHelp.send(context.getSource(), ""))
|
||||
.then(Commands.argument("section", StringArgumentType.greedyString())
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedCommandHelp.send(context.getSource(), StringArgumentType.getString(context, "section"))));
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> downloadTree(String name) {
|
||||
return Commands.literal(name).requires(GATE)
|
||||
.then(Commands.argument("pack", StringArgumentType.word()).suggests(ModdedCommandSuggestions.PACK_NAMES)
|
||||
.executes((CommandContext<CommandSourceStack> context) ->
|
||||
IrisModdedCommands.download(context.getSource(),
|
||||
StringArgumentType.getString(context, "pack"), "stable", false))
|
||||
.then(Commands.literal("force")
|
||||
.executes((CommandContext<CommandSourceStack> context) ->
|
||||
IrisModdedCommands.download(context.getSource(),
|
||||
StringArgumentType.getString(context, "pack"), "stable", true)))
|
||||
.then(Commands.argument("overwrite", BoolArgumentType.bool())
|
||||
.executes((CommandContext<CommandSourceStack> context) ->
|
||||
IrisModdedCommands.download(context.getSource(),
|
||||
StringArgumentType.getString(context, "pack"), "stable",
|
||||
BoolArgumentType.getBool(context, "overwrite"))))
|
||||
.then(Commands.argument("branch", StringArgumentType.word())
|
||||
.executes((CommandContext<CommandSourceStack> context) ->
|
||||
IrisModdedCommands.download(context.getSource(),
|
||||
StringArgumentType.getString(context, "pack"),
|
||||
StringArgumentType.getString(context, "branch"), false))
|
||||
.then(Commands.literal("force")
|
||||
.executes((CommandContext<CommandSourceStack> context) ->
|
||||
IrisModdedCommands.download(context.getSource(),
|
||||
StringArgumentType.getString(context, "pack"),
|
||||
StringArgumentType.getString(context, "branch"), true)))
|
||||
.then(Commands.argument("overwrite", BoolArgumentType.bool())
|
||||
.executes((CommandContext<CommandSourceStack> context) ->
|
||||
IrisModdedCommands.download(context.getSource(),
|
||||
StringArgumentType.getString(context, "pack"),
|
||||
StringArgumentType.getString(context, "branch"),
|
||||
BoolArgumentType.getBool(context, "overwrite"))))));
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> metricsTree(String name) {
|
||||
return Commands.literal(name).requires(GATE)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.metrics(context.getSource()));
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> regenTree(String name) {
|
||||
return Commands.literal(name).requires(GATE)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.regen(context.getSource(), 0))
|
||||
.then(Commands.argument("radius", IntegerArgumentType.integer(0, 64))
|
||||
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.regen(context.getSource(), IntegerArgumentType.getInteger(context, "radius"))));
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> gotoTree(String name) {
|
||||
return Commands.literal(name).requires(GATE)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedCommandHelp.send(context.getSource(), name))
|
||||
.then(Commands.literal("biome")
|
||||
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.BIOME_KEYS)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedLocateCommands.gotoBiome(context.getSource(), StringArgumentType.getString(context, "key")))))
|
||||
.then(Commands.literal("region")
|
||||
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.REGION_KEYS)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedLocateCommands.gotoRegion(context.getSource(), StringArgumentType.getString(context, "key")))))
|
||||
.then(Commands.literal("object")
|
||||
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.OBJECT_KEYS)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedLocateCommands.gotoObject(context.getSource(), StringArgumentType.getString(context, "key")))))
|
||||
.then(Commands.literal("structure")
|
||||
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.STRUCTURE_KEYS)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedLocateCommands.gotoStructure(context.getSource(), StringArgumentType.getString(context, "key")))))
|
||||
.then(Commands.literal("poi")
|
||||
.then(Commands.argument("type", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.POI_TYPES)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedLocateCommands.gotoPoi(context.getSource(), StringArgumentType.getString(context, "type")))));
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> pregenTree(String name) {
|
||||
RequiredArgumentBuilder<CommandSourceStack, Integer> radius = Commands.argument("radius", IntegerArgumentType.integer(1, 100000))
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedPregenCommands.pregenStart(context, false, false, false, false, false));
|
||||
attachPregenCenter(radius, false);
|
||||
attachPregenFlags(radius, false, false, false, false, false);
|
||||
RequiredArgumentBuilder<CommandSourceStack, Identifier> dimension = Commands.argument("dimension", DimensionArgument.dimension()).suggests(ModdedCommandSuggestions.DIMENSION_NAMES)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedPregenCommands.pregenStart(context, true, false, false, false, false));
|
||||
attachPregenCenter(dimension, true);
|
||||
attachPregenFlags(dimension, true, false, false, false, false);
|
||||
radius.then(dimension);
|
||||
|
||||
return Commands.literal(name).requires(GATE)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedCommandHelp.send(context.getSource(), name))
|
||||
.then(Commands.literal("start")
|
||||
.then(radius))
|
||||
.then(Commands.literal("stop")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedPregenCommands.pregenStop(context.getSource())))
|
||||
.then(Commands.literal("x")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedPregenCommands.pregenStop(context.getSource())))
|
||||
.then(Commands.literal("pause")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedPregenCommands.pregenPause(context.getSource())))
|
||||
.then(Commands.literal("resume")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedPregenCommands.pregenPause(context.getSource())))
|
||||
.then(Commands.literal("status")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedPregenCommands.pregenStatus(context.getSource())));
|
||||
}
|
||||
|
||||
private static void attachPregenCenter(ArgumentBuilder<CommandSourceStack, ?> node, boolean withDimension) {
|
||||
RequiredArgumentBuilder<CommandSourceStack, Integer> z = Commands.argument("z", IntegerArgumentType.integer())
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedPregenCommands.pregenStart(context, withDimension, true, false, false, false));
|
||||
attachPregenFlags(z, withDimension, true, false, false, false);
|
||||
node.then(Commands.literal("at")
|
||||
.then(Commands.argument("x", IntegerArgumentType.integer())
|
||||
.then(z)));
|
||||
}
|
||||
|
||||
private static void attachPregenFlags(ArgumentBuilder<CommandSourceStack, ?> node, boolean withDimension, boolean withCenter, boolean gui, boolean sync, boolean nocache) {
|
||||
if (!gui) {
|
||||
node.then(pregenFlagNode("gui", withDimension, withCenter, true, sync, nocache));
|
||||
}
|
||||
if (!sync) {
|
||||
node.then(pregenFlagNode("sync", withDimension, withCenter, gui, true, nocache));
|
||||
}
|
||||
if (!nocache) {
|
||||
node.then(pregenFlagNode("nocache", withDimension, withCenter, gui, sync, true));
|
||||
}
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> pregenFlagNode(String name, boolean withDimension, boolean withCenter, boolean gui, boolean sync, boolean nocache) {
|
||||
LiteralArgumentBuilder<CommandSourceStack> flag = Commands.literal(name)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedPregenCommands.pregenStart(context, withDimension, withCenter, gui, sync, nocache));
|
||||
attachPregenFlags(flag, withDimension, withCenter, gui, sync, nocache);
|
||||
return flag;
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> goldenhashTree(String name) {
|
||||
LiteralArgumentBuilder<CommandSourceStack> radiusAndThreads = Commands.literal(name).requires(GATE)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.goldenhash(context.getSource(), 8, 8, ModdedGoldenHash.Mode.AUTO));
|
||||
attachModes(radiusAndThreads, (CommandContext<CommandSourceStack> context) -> 8, (CommandContext<CommandSourceStack> context) -> 8);
|
||||
|
||||
com.mojang.brigadier.builder.RequiredArgumentBuilder<CommandSourceStack, Integer> radius = Commands.argument("radius", IntegerArgumentType.integer(0, 256))
|
||||
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.goldenhash(context.getSource(), IntegerArgumentType.getInteger(context, "radius"), 8, ModdedGoldenHash.Mode.AUTO));
|
||||
attachModes(radius, (CommandContext<CommandSourceStack> context) -> IntegerArgumentType.getInteger(context, "radius"), (CommandContext<CommandSourceStack> context) -> 8);
|
||||
|
||||
com.mojang.brigadier.builder.RequiredArgumentBuilder<CommandSourceStack, Integer> threads = Commands.argument("threads", IntegerArgumentType.integer(1, 64))
|
||||
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.goldenhash(context.getSource(), IntegerArgumentType.getInteger(context, "radius"), IntegerArgumentType.getInteger(context, "threads"), ModdedGoldenHash.Mode.AUTO));
|
||||
attachModes(threads, (CommandContext<CommandSourceStack> context) -> IntegerArgumentType.getInteger(context, "radius"), (CommandContext<CommandSourceStack> context) -> IntegerArgumentType.getInteger(context, "threads"));
|
||||
|
||||
radius.then(threads);
|
||||
radiusAndThreads.then(radius);
|
||||
return radiusAndThreads;
|
||||
}
|
||||
|
||||
private interface IntExtractor {
|
||||
int extract(CommandContext<CommandSourceStack> context);
|
||||
}
|
||||
|
||||
private static void attachModes(com.mojang.brigadier.builder.ArgumentBuilder<CommandSourceStack, ?> node, IntExtractor radius, IntExtractor threads) {
|
||||
node.then(Commands.literal("capture")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.goldenhash(context.getSource(), radius.extract(context), threads.extract(context), ModdedGoldenHash.Mode.CAPTURE)));
|
||||
node.then(Commands.literal("verify")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.goldenhash(context.getSource(), radius.extract(context), threads.extract(context), ModdedGoldenHash.Mode.VERIFY)));
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> editTree() {
|
||||
return Commands.literal("edit").requires(GATE)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedCommandHelp.send(context.getSource(), "edit"))
|
||||
.then(Commands.literal("biome")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editBiome(context.getSource(), null))
|
||||
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.BIOME_KEYS)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editBiome(context.getSource(), StringArgumentType.getString(context, "key")))))
|
||||
.then(Commands.literal("b")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editBiome(context.getSource(), null))
|
||||
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.BIOME_KEYS)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editBiome(context.getSource(), StringArgumentType.getString(context, "key")))))
|
||||
.then(Commands.literal("region")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editRegion(context.getSource(), null))
|
||||
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.REGION_KEYS)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editRegion(context.getSource(), StringArgumentType.getString(context, "key")))))
|
||||
.then(Commands.literal("r")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editRegion(context.getSource(), null))
|
||||
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.REGION_KEYS)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editRegion(context.getSource(), StringArgumentType.getString(context, "key")))))
|
||||
.then(Commands.literal("dimension")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editDimension(context.getSource())))
|
||||
.then(Commands.literal("d")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editDimension(context.getSource())));
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -108,7 +108,7 @@ public final class ModdedDustRevealer {
|
||||
pos.immutable(),
|
||||
key,
|
||||
level.getMinY(),
|
||||
level.getMaxY(),
|
||||
level.getMaxY() + 1,
|
||||
new AtomicBoolean());
|
||||
RevealRun previous = ACTIVE_RUNS.put(player.getUUID(), run);
|
||||
if (previous != null) {
|
||||
@@ -144,14 +144,14 @@ public final class ModdedDustRevealer {
|
||||
run.key(),
|
||||
run.engine().getMinHeight(),
|
||||
run.minY(),
|
||||
run.maxY(),
|
||||
run.maxYExclusive(),
|
||||
run.cancelled(),
|
||||
(int x, int relativeY, int z) ->
|
||||
run.engine().getObjectPlacementKey(x, relativeY, z));
|
||||
}
|
||||
|
||||
static List<BlockPos> collect(BlockPos origin, String key, int engineMinY,
|
||||
int minY, int maxY, AtomicBoolean cancelled,
|
||||
int minY, int maxYExclusive, AtomicBoolean cancelled,
|
||||
ObjectPlacementLookup lookup) {
|
||||
List<BlockPos> hits = new ArrayList<>();
|
||||
Set<BlockPos> visited = new HashSet<>();
|
||||
@@ -169,7 +169,7 @@ public final class ModdedDustRevealer {
|
||||
}
|
||||
BlockPos next = current.offset(dx, dy, dz);
|
||||
if (next.getY() < minY
|
||||
|| next.getY() >= maxY
|
||||
|| next.getY() >= maxYExclusive
|
||||
|| !visited.add(next)) {
|
||||
continue;
|
||||
}
|
||||
@@ -483,7 +483,7 @@ public final class ModdedDustRevealer {
|
||||
BlockPos origin,
|
||||
String key,
|
||||
int minY,
|
||||
int maxY,
|
||||
int maxYExclusive,
|
||||
AtomicBoolean cancelled
|
||||
) {
|
||||
}
|
||||
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import art.arcane.iris.core.gui.GuiHost;
|
||||
import art.arcane.iris.core.loader.IrisRegistrant;
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.ModdedCommandMessages;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.Desktop;
|
||||
import java.io.File;
|
||||
|
||||
final class ModdedEditCommands {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
|
||||
private ModdedEditCommands() {
|
||||
}
|
||||
|
||||
static int editBiome(CommandSourceStack source, String key) {
|
||||
Engine engine = IrisModdedCommands.engineFor(source.getLevel());
|
||||
if (engine == null) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS));
|
||||
return 0;
|
||||
}
|
||||
IrisBiome biome;
|
||||
if (key == null || key.isBlank()) {
|
||||
ServerPlayer player = source.getPlayer();
|
||||
if (player == null) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CONSOLE_MUST_NAME_BIOME_IRIS_EDIT_BIOME_KEY));
|
||||
return 0;
|
||||
}
|
||||
BlockPos pos = player.blockPosition();
|
||||
try {
|
||||
biome = engine.getBiome(pos.getX(), pos.getY() - engine.getMinHeight(), pos.getZ());
|
||||
} catch (Throwable e) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_BIOME_LOOKUP_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName())));
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
biome = engine.getData().getBiomeLoader().load(key.trim());
|
||||
if (biome == null) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_BIOME, MessageArgument.untrusted("key", key)));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return openJson(source, biome);
|
||||
}
|
||||
|
||||
static int editRegion(CommandSourceStack source, String key) {
|
||||
Engine engine = IrisModdedCommands.engineFor(source.getLevel());
|
||||
if (engine == null) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_2));
|
||||
return 0;
|
||||
}
|
||||
IrisRegion region;
|
||||
if (key == null || key.isBlank()) {
|
||||
ServerPlayer player = source.getPlayer();
|
||||
if (player == null) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CONSOLE_MUST_NAME_REGION_IRIS_EDIT_REGION_KEY));
|
||||
return 0;
|
||||
}
|
||||
BlockPos pos = player.blockPosition();
|
||||
try {
|
||||
region = engine.getRegion(pos.getX(), pos.getZ());
|
||||
} catch (Throwable e) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_REGION_LOOKUP_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName())));
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
region = engine.getData().getRegionLoader().load(key.trim());
|
||||
if (region == null) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_REGION, MessageArgument.untrusted("key", key)));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return openJson(source, region);
|
||||
}
|
||||
|
||||
static int editDimension(CommandSourceStack source) {
|
||||
Engine engine = IrisModdedCommands.engineFor(source.getLevel());
|
||||
if (engine == null) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_3));
|
||||
return 0;
|
||||
}
|
||||
return openJson(source, engine.getDimension());
|
||||
}
|
||||
|
||||
private static int openJson(CommandSourceStack source, IrisRegistrant registrant) {
|
||||
if (!GuiHost.isAvailable() || !Desktop.isDesktopSupported()) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CANNOT_OPEN_FILES_HERE, MessageArgument.untrusted("value", ModdedGuiHost.guiUnavailableReason())));
|
||||
return 0;
|
||||
}
|
||||
if (registrant == null || registrant.getLoadFile() == null || !registrant.getLoadFile().isFile()) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CANNOT_FIND_FILE_PERHAPS_IT_WAS_NOT_LOADED_DIRECTLY_FROM));
|
||||
return 0;
|
||||
}
|
||||
File file = registrant.getLoadFile();
|
||||
try {
|
||||
Desktop.getDesktop().open(file);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris edit failed to open {}", file, e);
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_COULD_NOT_OPEN, MessageArgument.untrusted("value", file.getName()), MessageArgument.untrusted("value2", e.getClass().getSimpleName())));
|
||||
return 0;
|
||||
}
|
||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_OPENING_YOUR_EDITOR, MessageArgument.untrusted("value", registrant.getTypeName()), MessageArgument.untrusted("value2", file.getName())));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
+535
@@ -0,0 +1,535 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.ModdedCommandMessages;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||
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.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import art.arcane.volmlib.util.math.Position2;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
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.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.Relative;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
final class ModdedLocateCommands {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final long LOCATE_TIMEOUT_MS = 120000L;
|
||||
private static final int NATIVE_STRUCTURE_LOCATE_RADIUS = 100;
|
||||
private static final ConcurrentHashMap<UUID, CompletableFuture<Position2>> ACTIVE_LOCATE_REQUESTS = new ConcurrentHashMap<>();
|
||||
|
||||
private ModdedLocateCommands() {
|
||||
}
|
||||
|
||||
static int gotoBiome(CommandSourceStack source, String key) {
|
||||
ServerPlayer player = source.getPlayer();
|
||||
if (player == null) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_3));
|
||||
return 0;
|
||||
}
|
||||
ServerLevel level = source.getLevel();
|
||||
Engine engine = IrisModdedCommands.engineFor(level);
|
||||
if (engine == null) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_8));
|
||||
return 0;
|
||||
}
|
||||
IrisBiome biome = engine.getData().getBiomeLoader().load(key.trim());
|
||||
if (biome == null) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_BIOME_2, MessageArgument.untrusted("key", key)));
|
||||
return 0;
|
||||
}
|
||||
locate(source, level, engine, player, Locator.surfaceBiome(biome.getLoadKey()), "biome " + biome.getLoadKey());
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int gotoRegion(CommandSourceStack source, String key) {
|
||||
ServerPlayer player = source.getPlayer();
|
||||
if (player == null) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_4));
|
||||
return 0;
|
||||
}
|
||||
ServerLevel level = source.getLevel();
|
||||
Engine engine = IrisModdedCommands.engineFor(level);
|
||||
if (engine == null) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_9));
|
||||
return 0;
|
||||
}
|
||||
IrisRegion region = engine.getData().getRegionLoader().load(key.trim());
|
||||
if (region == null) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_REGION_2, MessageArgument.untrusted("key", key)));
|
||||
return 0;
|
||||
}
|
||||
if (!engine.getDimension().getRegions().contains(region.getLoadKey())) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_DEFINED_DIMENSION, MessageArgument.untrusted("value", region.getLoadKey())));
|
||||
return 0;
|
||||
}
|
||||
locate(source, level, engine, player, Locator.region(region.getLoadKey()), "region " + region.getLoadKey());
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int gotoObject(CommandSourceStack source, String keyRaw) {
|
||||
ServerLevel level = source.getLevel();
|
||||
Engine engine = IrisModdedCommands.engineFor(level);
|
||||
if (engine == null) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_10));
|
||||
return 0;
|
||||
}
|
||||
String key = keyRaw.trim();
|
||||
if (!engine.hasObjectPlacement(key)) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_CONFIGURED_ANY_REGION_BIOME_OBJECT_PLACEMENTS_OBJECT_KEYS, MessageArgument.untrusted("key", key), MessageArgument.untrusted("value", engine.getData().getObjectLoader().getPossibleKeys().length)));
|
||||
return 0;
|
||||
}
|
||||
ServerPlayer player = source.getPlayer();
|
||||
if (player == null) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_OBJECT_KEY, MessageArgument.untrusted("key", key), MessageArgument.untrusted("value", engine.getData().getObjectLoader().getPossibleKeys().length)));
|
||||
return 0;
|
||||
}
|
||||
locate(source, level, engine, player, Locator.object(key), "object " + key);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int gotoStructure(CommandSourceStack source, String keyRaw) {
|
||||
ServerLevel level = source.getLevel();
|
||||
Engine engine = IrisModdedCommands.engineFor(level);
|
||||
if (engine == null) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_11));
|
||||
return 0;
|
||||
}
|
||||
String key = keyRaw.trim();
|
||||
if (key.isEmpty()) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NAME_IRIS_NATIVE_STRUCTURE_LOCATE));
|
||||
return 0;
|
||||
}
|
||||
ServerPlayer player = source.getPlayer();
|
||||
if (player == null) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_5));
|
||||
return 0;
|
||||
}
|
||||
Optional<NativeStructureTarget> resolved = resolveNativeStructure(source, level, engine, key);
|
||||
if (resolved.isEmpty()) {
|
||||
if (IrisStructureLocator.isPlaced(engine, key)) {
|
||||
locateIrisStructure(source, level, engine, player, key);
|
||||
return 1;
|
||||
}
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_STRUCTURE_USE_TAB_COMPLETION_CHOOSE_IRIS_PLACEMENT_REGISTERED_NATIVE, MessageArgument.untrusted("key", key)));
|
||||
return 0;
|
||||
}
|
||||
NativeStructureTarget target = resolved.get();
|
||||
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, target.key(), false);
|
||||
if (!decision.generate()
|
||||
&& decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
|
||||
IrisModdedCommands.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) {
|
||||
IrisModdedCommands.fail(source, nativeUnavailableMessage(target.key(), target.availability()));
|
||||
return 0;
|
||||
}
|
||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCHING_NATIVE_STRUCTURE_WITHIN_CHUNKS, MessageArgument.untrusted("value", target.key()), MessageArgument.untrusted("NATIVESTRUCTURELOCATERADIUS", NATIVE_STRUCTURE_LOCATE_RADIUS)));
|
||||
runNativeStructureLocate(source, level, player, target);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static void locateIrisStructure(CommandSourceStack source, ServerLevel level, Engine engine,
|
||||
ServerPlayer player, String key) {
|
||||
MinecraftServer server = source.getServer();
|
||||
int blockX = player.blockPosition().getX();
|
||||
int blockZ = player.blockPosition().getZ();
|
||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCHING_IRIS_PLACED_STRUCTURE, MessageArgument.untrusted("key", key)));
|
||||
Thread thread = new Thread(() -> {
|
||||
try {
|
||||
IrisStructureLocator.LocateResult result =
|
||||
IrisStructureLocator.locate(engine, key, blockX, blockZ, 1024);
|
||||
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
|
||||
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNABLE_LOCATE_IRIS_PLACED_STRUCTURE_DENSITY_SEARCH_SAFETY_LIMIT_WAS, MessageArgument.untrusted("key", key))));
|
||||
return;
|
||||
}
|
||||
if (!result.found()) {
|
||||
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_COULD_NOT_FIND_IRIS_PLACED_STRUCTURE_WITHIN_1024_CHUNKS, MessageArgument.untrusted("key", key))));
|
||||
return;
|
||||
}
|
||||
int targetX = result.originX();
|
||||
int targetY = result.baseY() + 2;
|
||||
int targetZ = result.originZ();
|
||||
server.execute(() -> teleportToStructure(source, level, player, targetX, targetY, targetZ,
|
||||
"Iris-placed structure " + key));
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris structure locate failed for {}", key, e);
|
||||
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCH_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName()))));
|
||||
}
|
||||
}, "Iris Structure Locator");
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
private static void runNativeStructureLocate(CommandSourceStack source, ServerLevel level,
|
||||
ServerPlayer player, NativeStructureTarget target) {
|
||||
MinecraftServer server = source.getServer();
|
||||
Runnable locateTask = () -> locateNativeStructure(source, level, player, target);
|
||||
if (Thread.currentThread() == server.getRunningThread()) {
|
||||
locateTask.run();
|
||||
return;
|
||||
}
|
||||
server.execute(locateTask);
|
||||
}
|
||||
|
||||
private static void locateNativeStructure(CommandSourceStack source, ServerLevel level,
|
||||
ServerPlayer player, NativeStructureTarget target) {
|
||||
try {
|
||||
ChunkGenerator generator = level.getChunkSource().getGenerator();
|
||||
Pair<BlockPos, Holder<Structure>> found = generator.findNearestMapStructure(
|
||||
level,
|
||||
HolderSet.direct(target.holder()),
|
||||
player.blockPosition(),
|
||||
NATIVE_STRUCTURE_LOCATE_RADIUS,
|
||||
false);
|
||||
if (found == null) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_COULD_NOT_FIND_NATIVE_STRUCTURE_WITHIN_CHUNKS, MessageArgument.untrusted("value", target.key()), MessageArgument.untrusted("NATIVESTRUCTURELOCATERADIUS", NATIVE_STRUCTURE_LOCATE_RADIUS)));
|
||||
return;
|
||||
}
|
||||
BlockPos position = found.getFirst();
|
||||
int targetX = position.getX();
|
||||
int targetZ = position.getZ();
|
||||
level.getChunk(targetX >> 4, targetZ >> 4);
|
||||
int surfaceY = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, targetX, targetZ) + 1;
|
||||
int targetY = Math.max(level.getMinY() + 1, Math.min(level.getMaxY() - 1, surfaceY));
|
||||
teleportToStructure(source, level, player, targetX, targetY, targetZ,
|
||||
"native structure " + target.key());
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Native structure locate failed for {}", target.key(), e);
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCH_NATIVE_STRUCTURE_FAILED, MessageArgument.untrusted("value", target.key()), MessageArgument.untrusted("value2", e.getClass().getSimpleName())));
|
||||
}
|
||||
}
|
||||
|
||||
private static void teleportToStructure(CommandSourceStack source, ServerLevel level, ServerPlayer player,
|
||||
int targetX, int targetY, int targetZ, String label) {
|
||||
if (player.hasDisconnected() || player.isRemoved()) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PLAYER_DISCONNECTED_BEFORE_STRUCTURE_SEARCH_COMPLETED));
|
||||
return;
|
||||
}
|
||||
if (player.level() != level) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_YOU_CHANGED_DIMENSIONS_BEFORE_STRUCTURE_SEARCH_COMPLETED_RUN_COMMAND_AGAIN));
|
||||
return;
|
||||
}
|
||||
level.getChunk(targetX >> 4, targetZ >> 4);
|
||||
int clampedY = Math.max(level.getMinY() + 1, Math.min(level.getMaxY() - 1, targetY));
|
||||
boolean teleported = player.teleportTo(level, targetX + 0.5D, clampedY, targetZ + 0.5D,
|
||||
Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
|
||||
if (!teleported) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_FOUND_AT_BUT_TELEPORTATION_FAILED, MessageArgument.untrusted("label", label), MessageArgument.untrusted("targetX", targetX), MessageArgument.untrusted("clampedY", clampedY), MessageArgument.untrusted("targetZ", targetZ)));
|
||||
return;
|
||||
}
|
||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORTED_AT, MessageArgument.untrusted("label", label), MessageArgument.untrusted("targetX", targetX), MessageArgument.untrusted("clampedY", clampedY), MessageArgument.untrusted("targetZ", targetZ)));
|
||||
}
|
||||
|
||||
static int verifyStructures(CommandSourceStack source, String keyRaw) {
|
||||
ServerLevel level = source.getLevel();
|
||||
Engine engine = IrisModdedCommands.engineFor(level);
|
||||
if (engine == null) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_12));
|
||||
return 0;
|
||||
}
|
||||
String key = keyRaw == null ? "" : keyRaw.trim();
|
||||
if (!key.isEmpty()) {
|
||||
return verifyStructure(source, level, engine, key);
|
||||
}
|
||||
Registry<Structure> registry = source.getServer().registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
int available = 0;
|
||||
int disabled = 0;
|
||||
int suppressed = 0;
|
||||
int unreachableBiomes = 0;
|
||||
int unsupported = 0;
|
||||
for (Identifier identifier : registry.keySet()) {
|
||||
Optional<Holder.Reference<Structure>> holder = registry.get(identifier);
|
||||
if (holder.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
NativeStructureAvailability availability = nativeAvailability(source, level, engine,
|
||||
identifier.toString(), holder.get());
|
||||
switch (availability) {
|
||||
case AVAILABLE -> available++;
|
||||
case WORLD_DISABLED, FILTERED -> disabled++;
|
||||
case IRIS_SUPPRESSED -> suppressed++;
|
||||
case BIOME_UNREACHABLE -> unreachableBiomes++;
|
||||
case NO_PLACEMENT -> unsupported++;
|
||||
}
|
||||
}
|
||||
int irisPlaced = IrisStructureLocator.placedKeys(engine).size();
|
||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_STRUCTURE_REACHABILITY_NATIVE_GENERATION_ELIGIBLE_IRIS_PLACED_NATIVE_DISABLED_NATIVE, MessageArgument.untrusted("available", available), MessageArgument.untrusted("irisPlaced", irisPlaced), MessageArgument.untrusted("disabled", disabled), MessageArgument.untrusted("suppressed", suppressed), MessageArgument.untrusted("unreachableBiomes", unreachableBiomes), MessageArgument.untrusted("unsupported", unsupported)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int verifyStructure(CommandSourceStack source, ServerLevel level, Engine engine, String key) {
|
||||
Optional<NativeStructureTarget> target = resolveNativeStructure(source, level, engine, key);
|
||||
if (target.isEmpty()) {
|
||||
if (IrisStructureLocator.isPlaced(engine, key)) {
|
||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_STRUCTURE_IS_IRIS_PLACED_LOCATABLE_WITH_IRIS_GOTO_STRUCTURE, MessageArgument.untrusted("key", key), MessageArgument.untrusted("key2", key)));
|
||||
return 1;
|
||||
}
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_STRUCTURE_IT_IS_NEITHER_IRIS_PLACED_NOR_REGISTERED_BY, MessageArgument.untrusted("key", key)));
|
||||
return 0;
|
||||
}
|
||||
NativeStructureTarget resolved = target.get();
|
||||
if (resolved.availability() == NativeStructureAvailability.IRIS_SUPPRESSED) {
|
||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_STRUCTURE_IS_EXPLICITLY_REPLACED_BY_IRIS_PLACEMENT_LOCATABLE_WITH_IRIS, MessageArgument.untrusted("value", resolved.key()), MessageArgument.untrusted("value2", resolved.key())));
|
||||
return 1;
|
||||
}
|
||||
if (resolved.availability() != NativeStructureAvailability.AVAILABLE) {
|
||||
IrisModdedCommands.fail(source, nativeUnavailableMessage(resolved.key(), resolved.availability()));
|
||||
return 0;
|
||||
}
|
||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NATIVE_STRUCTURE_IS_ENABLED_SUPPORTED_BY_THIS_DIMENSION_S_GENERATOR, MessageArgument.untrusted("value", resolved.key()), MessageArgument.untrusted("value2", resolved.key())));
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static Optional<NativeStructureTarget> resolveNativeStructure(CommandSourceStack source,
|
||||
ServerLevel level,
|
||||
Engine engine,
|
||||
String keyRaw) {
|
||||
Identifier identifier = Identifier.tryParse(keyRaw);
|
||||
if (identifier == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Registry<Structure> registry = source.getServer().registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
Optional<Holder.Reference<Structure>> holder = registry.get(identifier);
|
||||
if (holder.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String key = identifier.toString();
|
||||
NativeStructureAvailability availability = nativeAvailability(source, level, engine, key, holder.get());
|
||||
return Optional.of(new NativeStructureTarget(key, holder.get(), availability));
|
||||
}
|
||||
|
||||
private static NativeStructureAvailability nativeAvailability(CommandSourceStack source, ServerLevel level,
|
||||
Engine engine, String key,
|
||||
Holder.Reference<Structure> holder) {
|
||||
boolean worldEnabled = source.getServer().getWorldGenSettings().options().generateStructures();
|
||||
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);
|
||||
boolean hasPlacement = false;
|
||||
if (worldEnabled && selected && !suppressed && biomeReachable) {
|
||||
hasPlacement = !level.getChunkSource().getGeneratorState().getPlacementsForStructure(holder).isEmpty();
|
||||
}
|
||||
return classifyNativeAvailability(worldEnabled, selected, suppressed, biomeReachable, hasPlacement);
|
||||
}
|
||||
|
||||
static NativeStructureAvailability classifyNativeAvailability(boolean worldEnabled, boolean selected,
|
||||
boolean suppressed, boolean biomeReachable,
|
||||
boolean hasPlacement) {
|
||||
if (!worldEnabled) {
|
||||
return NativeStructureAvailability.WORLD_DISABLED;
|
||||
}
|
||||
if (!selected) {
|
||||
return NativeStructureAvailability.FILTERED;
|
||||
}
|
||||
if (suppressed) {
|
||||
return NativeStructureAvailability.IRIS_SUPPRESSED;
|
||||
}
|
||||
if (!biomeReachable) {
|
||||
return NativeStructureAvailability.BIOME_UNREACHABLE;
|
||||
}
|
||||
if (!hasPlacement) {
|
||||
return NativeStructureAvailability.NO_PLACEMENT;
|
||||
}
|
||||
return NativeStructureAvailability.AVAILABLE;
|
||||
}
|
||||
|
||||
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 -> 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.";
|
||||
};
|
||||
}
|
||||
|
||||
static int gotoPoi(CommandSourceStack source, String typeRaw) {
|
||||
ServerLevel level = source.getLevel();
|
||||
Engine engine = IrisModdedCommands.engineFor(level);
|
||||
if (engine == null) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_13));
|
||||
return 0;
|
||||
}
|
||||
String type = typeRaw.trim();
|
||||
ServerPlayer player = source.getPlayer();
|
||||
if (player == null) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_POI_TYPE, MessageArgument.untrusted("type", type)));
|
||||
return 0;
|
||||
}
|
||||
locate(source, level, engine, player, Locator.poi(type), "POI " + type);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static void locate(CommandSourceStack source, ServerLevel level, Engine engine, ServerPlayer player, Locator<?> locator, String label) {
|
||||
MinecraftServer server = source.getServer();
|
||||
int chunkX = player.blockPosition().getX() >> 4;
|
||||
int chunkZ = player.blockPosition().getZ() >> 4;
|
||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCHING, MessageArgument.untrusted("label", label)));
|
||||
CompletableFuture<Position2> search;
|
||||
try {
|
||||
search = locator.find(engine, new Position2(chunkX, chunkZ), LOCATE_TIMEOUT_MS, (Integer checks) -> {
|
||||
});
|
||||
} catch (WrongEngineBroException e) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_ENGINE_THIS_WORLD_HAS_BEEN_CLOSED_REJOIN_DIMENSION_TRY_AGAIN));
|
||||
return;
|
||||
}
|
||||
UUID playerId = player.getUUID();
|
||||
CompletableFuture<Position2> previous = ACTIVE_LOCATE_REQUESTS.put(playerId, search);
|
||||
if (previous != null && previous != search) {
|
||||
previous.cancel(true);
|
||||
}
|
||||
search.whenComplete((Position2 at, Throwable error) -> completeLocate(
|
||||
source, level, engine, player, label, server, playerId, search, at, error));
|
||||
}
|
||||
|
||||
private static void completeLocate(CommandSourceStack source, ServerLevel level, Engine engine,
|
||||
ServerPlayer player, String label, MinecraftServer server, UUID playerId,
|
||||
CompletableFuture<Position2> search, Position2 at, Throwable error) {
|
||||
if (ACTIVE_LOCATE_REQUESTS.get(playerId) != search) {
|
||||
return;
|
||||
}
|
||||
Throwable failure = unwrapCompletionFailure(error);
|
||||
if (failure instanceof CancellationException) {
|
||||
ACTIVE_LOCATE_REQUESTS.remove(playerId, search);
|
||||
return;
|
||||
}
|
||||
if (failure != null) {
|
||||
LOGGER.error("Iris locate failed for {}", label, failure);
|
||||
server.execute(() -> {
|
||||
if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCH_FAILED_2, MessageArgument.untrusted("failure", failure)));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (at == null) {
|
||||
server.execute(() -> {
|
||||
if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_COULD_NOT_FIND_WITHIN_SEARCH_TIMEOUT, MessageArgument.untrusted("label", label)));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
server.execute(() -> {
|
||||
if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) {
|
||||
teleportToLocateResult(source, level, engine, player, label, at);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void teleportToLocateResult(CommandSourceStack source, ServerLevel level, Engine engine,
|
||||
ServerPlayer player, String label, Position2 at) {
|
||||
int blockX = (at.getX() << 4) + 8;
|
||||
int blockZ = (at.getZ() << 4) + 8;
|
||||
try (GenerationSessionLease lease = engine.acquireGenerationLease("modded_locator_teleport");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
int blockY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2;
|
||||
boolean teleported = player.teleportTo(
|
||||
level,
|
||||
blockX + 0.5D,
|
||||
blockY,
|
||||
blockZ + 0.5D,
|
||||
Set.<Relative>of(),
|
||||
player.getYRot(),
|
||||
player.getXRot(),
|
||||
false);
|
||||
if (!teleported) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(
|
||||
ModdedCommandMessages.IRIS_MODDED_COMMANDS_FOUND_AT_BUT_TELEPORTATION_FAILED,
|
||||
MessageArgument.untrusted("label", label),
|
||||
MessageArgument.trusted("targetX", blockX),
|
||||
MessageArgument.trusted("clampedY", blockY),
|
||||
MessageArgument.trusted("targetZ", blockZ)));
|
||||
return;
|
||||
}
|
||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORTED_AT_2, MessageArgument.untrusted("label", label), MessageArgument.untrusted("blockX", blockX), MessageArgument.untrusted("blockY", blockY), MessageArgument.untrusted("blockZ", blockZ)));
|
||||
} catch (GenerationSessionException e) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_ENGINE_CHANGED_WHILE_LOCATING_TRY_AGAIN, MessageArgument.untrusted("label", label)));
|
||||
}
|
||||
}
|
||||
|
||||
private static Throwable unwrapCompletionFailure(Throwable error) {
|
||||
Throwable failure = error;
|
||||
while ((failure instanceof CompletionException || failure instanceof ExecutionException)
|
||||
&& failure.getCause() != null) {
|
||||
failure = failure.getCause();
|
||||
}
|
||||
return failure;
|
||||
}
|
||||
|
||||
enum NativeStructureAvailability {
|
||||
AVAILABLE,
|
||||
WORLD_DISABLED,
|
||||
FILTERED,
|
||||
IRIS_SUPPRESSED,
|
||||
BIOME_UNREACHABLE,
|
||||
NO_PLACEMENT
|
||||
}
|
||||
|
||||
private record NativeStructureTarget(String key, Holder.Reference<Structure> holder,
|
||||
NativeStructureAvailability availability) {
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -87,7 +87,8 @@ public final class ModdedObjectCommands {
|
||||
if (engine != null) {
|
||||
return SharedSuggestionProvider.suggest(engine.getData().getObjectLoader().getPossibleKeys(), builder);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
} catch (Throwable e) {
|
||||
IrisModdedCommands.warnTabFailure("object keys", context.getSource(), e);
|
||||
}
|
||||
return builder.buildFuture();
|
||||
};
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.ModdedCommandMessages;
|
||||
import art.arcane.iris.core.localization.RuntimeUiMessages;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.arguments.DimensionArgument;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
|
||||
final class ModdedPregenCommands {
|
||||
private ModdedPregenCommands() {
|
||||
}
|
||||
|
||||
static int pregenStart(CommandContext<CommandSourceStack> context, boolean withDimension, boolean withCenter, boolean gui, boolean sync, boolean nocache) throws CommandSyntaxException {
|
||||
CommandSourceStack source = context.getSource();
|
||||
int radius = IntegerArgumentType.getInteger(context, "radius");
|
||||
int centerX = withCenter ? IntegerArgumentType.getInteger(context, "x") : 0;
|
||||
int centerZ = withCenter ? IntegerArgumentType.getInteger(context, "z") : 0;
|
||||
ServerLevel level = withDimension ? DimensionArgument.getDimension(context, "dimension") : source.getLevel();
|
||||
Engine engine = IrisModdedCommands.engineFor(level);
|
||||
if (engine == null) {
|
||||
if (withDimension) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_GENERATED_BY_IRIS_SEE_IRIS_INFO_LOADED_IRIS, MessageArgument.untrusted("value", level.dimension().identifier())));
|
||||
} else {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CURRENT_DIMENSION_IS_NOT_GENERATED_BY_IRIS_NAME_ONE_EXPLICITLY, MessageArgument.untrusted("value", level.dimension().identifier()), MessageArgument.untrusted("radius", radius)));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
boolean showGui = gui && ModdedGuiHost.isGuiLaunchable();
|
||||
if (!ModdedPregenJob.start(source.getServer(), level, engine, radius, centerX, centerZ, showGui, sync, !nocache)) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PREGENERATION_TASK_IS_ALREADY_RUNNING_STOP_IT_FIRST_WITH_IRIS));
|
||||
return 0;
|
||||
}
|
||||
ModdedPregenBossBar.begin(source.getPlayer());
|
||||
String guiNote;
|
||||
if (!gui) {
|
||||
guiNote = "";
|
||||
} else if (showGui) {
|
||||
guiNote = " A progress map window is opening on the server display.";
|
||||
} else {
|
||||
guiNote = " (GUI requested but unavailable: " + ModdedGuiHost.guiUnavailableReason() + ")";
|
||||
}
|
||||
String modeNote = " Mode: " + (sync ? "sync" : "async") + (nocache ? ", cache disabled." : ", resumable (checkpoint cache).");
|
||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PREGEN_STARTED_BY_BLOCKS_FROM_PROGRESS_LOGS_CONSOLE_SEE_IRIS, MessageArgument.untrusted("value", level.dimension().identifier()), MessageArgument.untrusted("value2", (radius * 2)), MessageArgument.untrusted("value3", (radius * 2)), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ), MessageArgument.untrusted("modeNote", modeNote), MessageArgument.untrusted("guiNote", guiNote)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int pregenStop(CommandSourceStack source) {
|
||||
if (ModdedPregenJob.stop()) {
|
||||
ModdedPregenBossBar.clear();
|
||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_STOPPING_PREGENERATION_FINISHING_UP_CURRENT_REGION));
|
||||
return 1;
|
||||
}
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NO_ACTIVE_PREGENERATION_TASK_STOP));
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int pregenPause(CommandSourceStack source) {
|
||||
Boolean paused = ModdedPregenJob.pauseResume();
|
||||
if (paused == null) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NO_ACTIVE_PREGENERATION_TASK_PAUSE_RESUME));
|
||||
return 0;
|
||||
}
|
||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PREGENERATION_IS_NOW, MessageArgument.trusted("value", IrisLanguage.plain(paused.booleanValue() ? RuntimeUiMessages.STATUS_PAUSED_LOWER : RuntimeUiMessages.STATUS_RUNNING_LOWER))));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int pregenStatus(CommandSourceStack source) {
|
||||
Component status = ModdedPregenJob.statusComponent();
|
||||
if (status == null) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NO_ACTIVE_PREGENERATION_TASK));
|
||||
return 0;
|
||||
}
|
||||
IrisModdedCommands.ok(source, status);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -102,7 +102,8 @@ public final class ModdedStudioCommands {
|
||||
if (engine != null) {
|
||||
return SharedSuggestionProvider.suggest(engine.getData().getGeneratorLoader().getPossibleKeys(), builder);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
} catch (Throwable e) {
|
||||
IrisModdedCommands.warnTabFailure("generator keys", context.getSource(), e);
|
||||
}
|
||||
return builder.buildFuture();
|
||||
};
|
||||
|
||||
+9
-1
@@ -20,6 +20,7 @@ package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.pack.BrokenPackException;
|
||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.MainWorldService;
|
||||
@@ -303,7 +304,14 @@ public final class ModdedWorldCommands {
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris main world pack load failed for {} (dim={})", pack, packDimension, e);
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_PACK_IS_NOT_READY_YET_STILL_LOADING_VALIDATING_TRY_COMMAND, MessageArgument.untrusted("pack", pack)));
|
||||
if (PackValidationRegistry.get(pack) == null) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_PACK_IS_NOT_READY_YET_STILL_LOADING_VALIDATING_TRY_COMMAND, MessageArgument.untrusted("pack", pack)));
|
||||
return 0;
|
||||
}
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_VALIDATION_FAILED,
|
||||
MessageArgument.untrusted("value", pack + ":" + packDimension),
|
||||
MessageArgument.trusted("value2", e.getClass().getSimpleName() + IrisLanguage.errorDetail(e))));
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_FIX_PACK_RUN_IRIS_PACK_VALIDATE_REVALIDATE, MessageArgument.untrusted("pack", pack)));
|
||||
return 0;
|
||||
}
|
||||
ModdedModConfig.setMainWorld(packRef, seed);
|
||||
|
||||
-620
@@ -1,620 +0,0 @@
|
||||
/*
|
||||
* 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
@@ -1,426 +0,0 @@
|
||||
/*
|
||||
* 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
@@ -1,339 +0,0 @@
|
||||
/*
|
||||
* 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) {
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -58,8 +58,8 @@ public class IrisModdedChunkGeneratorSpawnTest {
|
||||
int spawnEnd = source.indexOf("@Override", spawnStart + 1);
|
||||
String spawn = source.substring(spawnStart, spawnEnd);
|
||||
|
||||
assertTrue(spawn.contains("initializeVanillaSpawnBiomes(registry)"));
|
||||
assertTrue(spawn.contains("vanillaSpawnBiomes.get(visibleBiome.value())"));
|
||||
assertTrue(spawn.contains("spawnTables.initializeVanillaSpawnBiomes(registry)"));
|
||||
assertTrue(spawn.contains("spawnTables.vanillaSpawnBiome(visibleBiome.value())"));
|
||||
assertTrue(spawn.contains("NaturalSpawner.spawnMobsForChunkGeneration("));
|
||||
assertTrue(spawn.contains("new LegacyRandomSource(RandomSupport.generateUniqueSeed())"));
|
||||
assertTrue(spawn.contains("random.setDecorationSeed(region.getSeed()"));
|
||||
|
||||
+15
-15
@@ -58,10 +58,10 @@ public class IrisModdedStructureParityTest {
|
||||
|
||||
@Test
|
||||
public void spawnHeightMatchesPaperFixedSpawnClamp() {
|
||||
assertEquals(96, IrisModdedChunkGenerator.clampSpawnHeight(-64, 384));
|
||||
assertEquals(96, IrisModdedChunkGenerator.clampSpawnHeight(0, 128));
|
||||
assertEquals(88, IrisModdedChunkGenerator.clampSpawnHeight(80, 10));
|
||||
assertEquals(101, IrisModdedChunkGenerator.clampSpawnHeight(100, 20));
|
||||
assertEquals(96, ModdedDimensionMetadata.clampSpawnHeight(-64, 384));
|
||||
assertEquals(96, ModdedDimensionMetadata.clampSpawnHeight(0, 128));
|
||||
assertEquals(88, ModdedDimensionMetadata.clampSpawnHeight(80, 10));
|
||||
assertEquals(101, ModdedDimensionMetadata.clampSpawnHeight(100, 20));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -109,7 +109,7 @@ public class IrisModdedStructureParityTest {
|
||||
.setVanillaDerivative("minecraft:plains")
|
||||
.setInferredType(InferredType.SEA);
|
||||
|
||||
Set<String> keys = IrisModdedChunkGenerator.collectConfiguredBiomeKeys(
|
||||
Set<String> keys = ModdedDimensionMetadata.collectConfiguredBiomeKeys(
|
||||
List.of(ocean, custom, shore, unsafeSea), "OverWorld");
|
||||
|
||||
assertEquals(Set.of("minecraft:deep_ocean", "minecraft:forest", "minecraft:beach",
|
||||
@@ -150,8 +150,8 @@ public class IrisModdedStructureParityTest {
|
||||
.setFluidHeight(50);
|
||||
dimension.setLoadKey("bootstrap_contract");
|
||||
|
||||
IrisModdedChunkGenerator.DimensionMetadata metadata =
|
||||
IrisModdedChunkGenerator.dimensionMetadata(dimension);
|
||||
ModdedDimensionMetadata.DimensionMetadata metadata =
|
||||
ModdedDimensionMetadata.dimensionMetadata(dimension);
|
||||
|
||||
assertEquals(-256, metadata.minY());
|
||||
assertEquals(512, metadata.maxY());
|
||||
@@ -161,8 +161,8 @@ public class IrisModdedStructureParityTest {
|
||||
|
||||
@Test
|
||||
public void structureRingWorkersWaitWithoutBlockingLifecycleBinding() throws Exception {
|
||||
IrisModdedChunkGenerator.EngineBinding<String> binding =
|
||||
new IrisModdedChunkGenerator.EngineBinding<>(5L, TimeUnit.SECONDS);
|
||||
ModdedEngineBinding<String> binding =
|
||||
new ModdedEngineBinding<>(5L, TimeUnit.SECONDS);
|
||||
String exactEngine = "exact-engine";
|
||||
CountDownLatch workerStarted = new CountDownLatch(1);
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
@@ -184,8 +184,8 @@ public class IrisModdedStructureParityTest {
|
||||
|
||||
@Test
|
||||
public void structureRingBindingPropagatesBootstrapFailure() {
|
||||
IrisModdedChunkGenerator.EngineBinding<String> binding =
|
||||
new IrisModdedChunkGenerator.EngineBinding<>(1L, TimeUnit.SECONDS);
|
||||
ModdedEngineBinding<String> binding =
|
||||
new ModdedEngineBinding<>(1L, TimeUnit.SECONDS);
|
||||
IllegalArgumentException failure = new IllegalArgumentException("broken pack");
|
||||
binding.fail(failure);
|
||||
|
||||
@@ -200,16 +200,16 @@ public class IrisModdedStructureParityTest {
|
||||
|
||||
@Test
|
||||
public void structureBiomeBootstrapAllowsOnlyPendingBindingsToUseMetadata() {
|
||||
IrisModdedChunkGenerator.EngineBinding<String> binding =
|
||||
new IrisModdedChunkGenerator.EngineBinding<>(1L, TimeUnit.SECONDS);
|
||||
ModdedEngineBinding<String> binding =
|
||||
new ModdedEngineBinding<>(1L, TimeUnit.SECONDS);
|
||||
|
||||
binding.throwIfFailed("overworld:overworld");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structureBiomeBootstrapPropagatesBindingFailure() {
|
||||
IrisModdedChunkGenerator.EngineBinding<String> binding =
|
||||
new IrisModdedChunkGenerator.EngineBinding<>(1L, TimeUnit.SECONDS);
|
||||
ModdedEngineBinding<String> binding =
|
||||
new ModdedEngineBinding<>(1L, TimeUnit.SECONDS);
|
||||
IllegalArgumentException failure = new IllegalArgumentException("broken pack");
|
||||
binding.fail(failure);
|
||||
|
||||
|
||||
+5
-5
@@ -152,13 +152,13 @@ public class ModdedDimensionTypeParityTest {
|
||||
.ceiling(TRUE);
|
||||
IrisDimension dimension = dimension("runtime_contract", IrisEnvironment.CUSTOM,
|
||||
-128, 384, 384, options);
|
||||
ModdedWorldCheck.DimensionContract expected = ModdedWorldCheck.expectedDimensionContract(dimension);
|
||||
ModdedWorldCheck.DimensionContract fallback = new ModdedWorldCheck.DimensionContract(
|
||||
WorldCheckDimensionContract.DimensionContract expected = WorldCheckDimensionContract.expectedDimensionContract(dimension);
|
||||
WorldCheckDimensionContract.DimensionContract fallback = new WorldCheckDimensionContract.DimensionContract(
|
||||
-256, 768, 512, 1D, 0F, true, false, false, 0);
|
||||
|
||||
assertTrue(ModdedWorldCheck.matchesDimensionContract(-128, 512, expected, expected));
|
||||
assertFalse(ModdedWorldCheck.matchesDimensionContract(-256, 768, expected, fallback));
|
||||
assertFalse(ModdedWorldCheck.matchesDimensionContract(-128, 512, expected, fallback));
|
||||
assertTrue(WorldCheckDimensionContract.matchesDimensionContract(-128, 512, expected, expected));
|
||||
assertFalse(WorldCheckDimensionContract.matchesDimensionContract(-256, 768, expected, fallback));
|
||||
assertFalse(WorldCheckDimensionContract.matchesDimensionContract(-128, 512, expected, fallback));
|
||||
}
|
||||
|
||||
private static IrisDimension dimension(String key, IrisEnvironment environment, int minY, int maxY,
|
||||
|
||||
+44
-41
@@ -28,40 +28,43 @@ public class ModdedWorldCheckTest {
|
||||
|
||||
@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);
|
||||
Path sourceRoot = Path.of(System.getProperty("iris.moddedCommonSources"));
|
||||
String source = Files.readString(sourceRoot.resolve("art/arcane/iris/modded/ModdedWorldCheck.java"));
|
||||
String auditSource = Files.readString(
|
||||
sourceRoot.resolve("art/arcane/iris/modded/WorldCheckStructureAudit.java"));
|
||||
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",
|
||||
int deferredAudit = source.indexOf(
|
||||
"PoiAudit poi = WorldCheckStructureAudit.auditStructurePois", completionMethod);
|
||||
int structureMethod = auditSource.indexOf("private static StructureCheckResult checkNativeStructure");
|
||||
int structureMethodEnd = auditSource.indexOf("private static StructureStart resolveStructureStart",
|
||||
structureMethod);
|
||||
String structureSource = source.substring(structureMethod, structureMethodEnd);
|
||||
String structureSource = auditSource.substring(structureMethod, structureMethodEnd);
|
||||
|
||||
assertTrue(preparationSubmit >= 0);
|
||||
assertTrue(completionSubmit > preparationSubmit);
|
||||
assertTrue(deferredAudit > completionMethod);
|
||||
assertFalse(structureSource.contains("auditStructurePois"));
|
||||
assertFalse(source.contains("prepareDeferredAudits"));
|
||||
assertFalse(auditSource.contains("prepareDeferredAudits"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validStructureStartIsGenerationEvidence() {
|
||||
assertTrue(ModdedWorldCheck.hasNativeStructureEvidence(true, 0));
|
||||
assertTrue(WorldCheckPredicates.hasNativeStructureEvidence(true, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structureReferenceIsGenerationEvidence() {
|
||||
assertTrue(ModdedWorldCheck.hasNativeStructureEvidence(false, 1));
|
||||
assertTrue(WorldCheckPredicates.hasNativeStructureEvidence(false, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void absentStartAndReferencesFailGenerationEvidence() {
|
||||
assertFalse(ModdedWorldCheck.hasNativeStructureEvidence(false, 0));
|
||||
assertFalse(WorldCheckPredicates.hasNativeStructureEvidence(false, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -91,68 +94,68 @@ public class ModdedWorldCheckTest {
|
||||
|
||||
@Test
|
||||
public void materialEvidenceMustExist() {
|
||||
assertFalse(ModdedWorldCheck.hasCharacteristicMaterialEvidence(0, 0, 1));
|
||||
assertFalse(ModdedWorldCheck.hasCharacteristicMaterialEvidence(8, 0, 1));
|
||||
assertFalse(ModdedWorldCheck.hasCharacteristicMaterialEvidence(8, 1, 0));
|
||||
assertFalse(ModdedWorldCheck.hasCharacteristicMaterialEvidence(8, 2, 1));
|
||||
assertFalse(WorldCheckPredicates.hasCharacteristicMaterialEvidence(0, 0, 1));
|
||||
assertFalse(WorldCheckPredicates.hasCharacteristicMaterialEvidence(8, 0, 1));
|
||||
assertFalse(WorldCheckPredicates.hasCharacteristicMaterialEvidence(8, 1, 0));
|
||||
assertFalse(WorldCheckPredicates.hasCharacteristicMaterialEvidence(8, 2, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleChunkStructureAcceptsMaterialInItsOnlyChunk() {
|
||||
assertTrue(ModdedWorldCheck.hasCharacteristicMaterialEvidence(8, 1, 1));
|
||||
assertTrue(WorldCheckPredicates.hasCharacteristicMaterialEvidence(8, 1, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multiChunkStructureRejectsMaterialConfinedToOneChunk() {
|
||||
assertFalse(ModdedWorldCheck.hasCharacteristicMaterialEvidence(8, 1, 4));
|
||||
assertTrue(ModdedWorldCheck.hasCharacteristicMaterialEvidence(8, 2, 4));
|
||||
assertFalse(WorldCheckPredicates.hasCharacteristicMaterialEvidence(8, 1, 4));
|
||||
assertTrue(WorldCheckPredicates.hasCharacteristicMaterialEvidence(8, 2, 4));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configuredVerticalShiftRequiresSafetyClampedGenerationEvidence() {
|
||||
assertTrue(ModdedWorldCheck.verticalShiftMatches(0, null, -32, 20, -64, 320));
|
||||
assertFalse(ModdedWorldCheck.verticalShiftMatches(0, null, -112, -80, -64, 320));
|
||||
assertTrue(ModdedWorldCheck.verticalShiftMatches(0, 0, -32, 20, -64, 320));
|
||||
assertTrue(ModdedWorldCheck.verticalShiftMatches(0, 48, -64, -32, -64, 320));
|
||||
assertTrue(ModdedWorldCheck.verticalShiftMatches(-64, -64, -48, 4, -64, 320));
|
||||
assertTrue(ModdedWorldCheck.verticalShiftMatches(-64, -16, -64, -12, -64, 320));
|
||||
assertFalse(ModdedWorldCheck.verticalShiftMatches(-64, null, -48, 4, -64, 320));
|
||||
assertFalse(ModdedWorldCheck.verticalShiftMatches(-64, -15, -63, -11, -64, 320));
|
||||
assertFalse(ModdedWorldCheck.verticalShiftMatches(0, -1, -33, 19, -64, 320));
|
||||
assertTrue(WorldCheckPredicates.verticalShiftMatches(0, null, -32, 20, -64, 320));
|
||||
assertFalse(WorldCheckPredicates.verticalShiftMatches(0, null, -112, -80, -64, 320));
|
||||
assertTrue(WorldCheckPredicates.verticalShiftMatches(0, 0, -32, 20, -64, 320));
|
||||
assertTrue(WorldCheckPredicates.verticalShiftMatches(0, 48, -64, -32, -64, 320));
|
||||
assertTrue(WorldCheckPredicates.verticalShiftMatches(-64, -64, -48, 4, -64, 320));
|
||||
assertTrue(WorldCheckPredicates.verticalShiftMatches(-64, -16, -64, -12, -64, 320));
|
||||
assertFalse(WorldCheckPredicates.verticalShiftMatches(-64, null, -48, 4, -64, 320));
|
||||
assertFalse(WorldCheckPredicates.verticalShiftMatches(-64, -15, -63, -11, -64, 320));
|
||||
assertFalse(WorldCheckPredicates.verticalShiftMatches(0, -1, -33, 19, -64, 320));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mansionVegetationGateRejectsRemainingLeaves() {
|
||||
assertTrue(ModdedWorldCheck.mansionVegetationPass(0));
|
||||
assertFalse(ModdedWorldCheck.mansionVegetationPass(1));
|
||||
assertTrue(WorldCheckPredicates.mansionVegetationPass(0));
|
||||
assertFalse(WorldCheckPredicates.mansionVegetationPass(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mansionVegetationAuditIgnoresTemplateBlocksAndRejectsVegetationAbovePieces() {
|
||||
assertFalse(ModdedWorldCheck.mansionVegetationAbovePiece(true, 80, 80));
|
||||
assertFalse(ModdedWorldCheck.mansionVegetationAbovePiece(true, 79, 80));
|
||||
assertTrue(ModdedWorldCheck.mansionVegetationAbovePiece(true, 81, 80));
|
||||
assertFalse(ModdedWorldCheck.mansionVegetationAbovePiece(false, 81, 80));
|
||||
assertFalse(WorldCheckPredicates.mansionVegetationAbovePiece(true, 80, 80));
|
||||
assertFalse(WorldCheckPredicates.mansionVegetationAbovePiece(true, 79, 80));
|
||||
assertTrue(WorldCheckPredicates.mansionVegetationAbovePiece(true, 81, 80));
|
||||
assertFalse(WorldCheckPredicates.mansionVegetationAbovePiece(false, 81, 80));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void villageFoundationGateRejectsUnsupportedColumns() {
|
||||
assertTrue(ModdedWorldCheck.villageFoundationPass(0));
|
||||
assertFalse(ModdedWorldCheck.villageFoundationPass(1));
|
||||
assertTrue(WorldCheckPredicates.villageFoundationPass(0));
|
||||
assertFalse(WorldCheckPredicates.villageFoundationPass(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void villagePoiGateRequiresInBoundsPoiWithoutOutOfBoundsRecords() {
|
||||
assertTrue(ModdedWorldCheck.villagePoiPass(1, 0));
|
||||
assertFalse(ModdedWorldCheck.villagePoiPass(0, 0));
|
||||
assertFalse(ModdedWorldCheck.villagePoiPass(1, 1));
|
||||
assertTrue(WorldCheckPredicates.villagePoiPass(1, 0));
|
||||
assertFalse(WorldCheckPredicates.villagePoiPass(0, 0));
|
||||
assertFalse(WorldCheckPredicates.villagePoiPass(1, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void smallStructureFootprintIncludesEveryChunk() {
|
||||
BoundingBox bounds = new BoundingBox(-16, -20, -16, 31, 120, 31);
|
||||
|
||||
List<ChunkPos> chunks = ModdedWorldCheck.boundedFootprintChunks(bounds, ChunkPos.ZERO, 96);
|
||||
List<ChunkPos> chunks = WorldCheckStructureAudit.boundedFootprintChunks(bounds, ChunkPos.ZERO, 96);
|
||||
|
||||
assertEquals(9, chunks.size());
|
||||
assertTrue(chunks.contains(new ChunkPos(-1, -1)));
|
||||
@@ -163,7 +166,7 @@ public class ModdedWorldCheckTest {
|
||||
public void largeStructureFootprintIsBoundedAndSamplesEdges() {
|
||||
BoundingBox bounds = new BoundingBox(-512, -64, -512, 511, 300, 511);
|
||||
|
||||
List<ChunkPos> chunks = ModdedWorldCheck.boundedFootprintChunks(bounds, ChunkPos.ZERO, 20);
|
||||
List<ChunkPos> chunks = WorldCheckStructureAudit.boundedFootprintChunks(bounds, ChunkPos.ZERO, 20);
|
||||
|
||||
assertTrue(chunks.size() <= 20);
|
||||
assertTrue(chunks.size() >= 16);
|
||||
@@ -174,7 +177,7 @@ public class ModdedWorldCheckTest {
|
||||
|
||||
@Test
|
||||
public void qaEventsEscapeStructuredValues() {
|
||||
String event = ModdedWorldCheck.qaEventJson("locate\"", "village\n", false, "x\\y\t");
|
||||
String event = WorldCheckPredicates.qaEventJson("locate\"", "village\n", false, "x\\y\t");
|
||||
|
||||
assertEquals("QA_EVT {\"event\":\"locate\\\"\",\"structure\":\"village\\n\","
|
||||
+ "\"pass\":false,\"detail\":\"x\\\\y\\t\"}", event);
|
||||
@@ -292,7 +295,7 @@ public class ModdedWorldCheckTest {
|
||||
}
|
||||
|
||||
private static boolean characteristic(String structureLabel, String structureKey, String blockKey) {
|
||||
return ModdedWorldCheck.isCharacteristicMaterial(structureLabel,
|
||||
return WorldCheckMaterials.isCharacteristicMaterial(structureLabel,
|
||||
Identifier.parse(structureKey), Identifier.parse(blockKey));
|
||||
}
|
||||
}
|
||||
|
||||
+30
-10
@@ -24,15 +24,14 @@ public class NativeStructureFailureContractTest {
|
||||
|
||||
@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);
|
||||
String source = moddedSource("IrisModdedChunkGenerator.java");
|
||||
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);
|
||||
String stage = moddedSource("ModdedNativeStructureStage.java");
|
||||
int filterStart = stage.indexOf("HolderSet<Structure> filterReachableNativeStructures");
|
||||
int filterEnd = stage.indexOf("void adjustGeneratedStructures", filterStart);
|
||||
String filter = stage.substring(filterStart, filterEnd);
|
||||
|
||||
assertTrue(locate.contains("Engine current = engine();"));
|
||||
assertFalse(locate.contains("catch (Throwable"));
|
||||
@@ -58,10 +57,8 @@ public class NativeStructureFailureContractTest {
|
||||
|
||||
@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");
|
||||
String source = moddedSource("ModdedNativeStructureStage.java");
|
||||
int placementStart = source.indexOf("void placeVanillaStructures");
|
||||
int placementEnd = source.indexOf("private static String nativeStructureBatchContext", placementStart);
|
||||
String placement = source.substring(placementStart, placementEnd);
|
||||
|
||||
@@ -74,6 +71,23 @@ public class NativeStructureFailureContractTest {
|
||||
< placement.indexOf("for (NativePlacementGroup group"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structurePlacementPrimesNeighbourWorldgenHeightmapsBeforeTerrainPreparation() throws IOException {
|
||||
String source = moddedSource("ModdedNativeStructureStage.java");
|
||||
int placementStart = source.indexOf("void placeVanillaStructures");
|
||||
int placementEnd = source.indexOf("private static String nativeStructureBatchContext", placementStart);
|
||||
String placement = source.substring(placementStart, placementEnd);
|
||||
|
||||
assertTrue(source.contains("import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;"));
|
||||
assertTrue(placement.contains("WorldgenTerrainHeightmaps.primeStructurePlacement("));
|
||||
assertTrue(placement.contains("\"heightmap priming\""));
|
||||
assertTrue(placement.contains("heightmapStarts.add(start);"));
|
||||
assertTrue(placement.indexOf("WorldgenTerrainHeightmaps.primeStructurePlacement(")
|
||||
< placement.indexOf("prepareSurfaceStructures"));
|
||||
assertTrue(source.contains("generationEngine.getHeight(x, z, false) + runtimeMinY + 1"));
|
||||
assertTrue(source.contains("generationEngine.getHeight(x, z, true) + runtimeMinY + 1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structureFailurePreservesPhaseIdentityChunkAndCause() {
|
||||
IllegalArgumentException cause = new IllegalArgumentException("broken placement");
|
||||
@@ -86,4 +100,10 @@ public class NativeStructureFailureContractTest {
|
||||
assertTrue(error.getMessage().contains("12,-8"));
|
||||
assertTrue(error.getMessage().contains("aborted"));
|
||||
}
|
||||
|
||||
private static String moddedSource(String fileName) throws IOException {
|
||||
Path sourcePath = Path.of(System.getProperty("iris.moddedCommonSources"),
|
||||
"art", "arcane", "iris", "modded", fileName);
|
||||
return Files.readString(sourcePath);
|
||||
}
|
||||
}
|
||||
|
||||
+9
-8
@@ -12,7 +12,8 @@ import static org.junit.Assert.assertTrue;
|
||||
public class IrisModdedStructureCommandTest {
|
||||
@Test
|
||||
public void gotoStructureSupportsIrisAndNativeRegistryTargets() throws IOException {
|
||||
String source = source("IrisModdedCommands.java");
|
||||
String source = source("ModdedLocateCommands.java");
|
||||
String suggestions = source("ModdedCommandSuggestions.java");
|
||||
|
||||
assertTrue(source.contains("IrisStructureLocator.isPlaced(engine, key)"));
|
||||
assertTrue(source.contains("registry.get(identifier)"));
|
||||
@@ -22,7 +23,7 @@ public class IrisModdedStructureCommandTest {
|
||||
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(suggestions.contains("combineStructureKeys(irisKeys, nativeKeys)"));
|
||||
assertTrue(source.contains("irisGenerator.isNativeStructureReachable(holder)"));
|
||||
assertTrue(source.contains("LocateStatus.SEARCH_LIMIT_REACHED"));
|
||||
assertTrue(source.contains("IRIS_MODDED_COMMANDS_UNABLE_LOCATE_IRIS_PLACED_STRUCTURE_DENSITY_SEARCH_SAFETY_LIMIT_WAS"));
|
||||
@@ -35,9 +36,9 @@ public class IrisModdedStructureCommandTest {
|
||||
|
||||
@Test
|
||||
public void generatorLocateUsesEveryIrisPlacedNativeStructure() 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 source = moddedSource("ModdedNativeStructureStage.java");
|
||||
int methodStart = source.indexOf("Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(");
|
||||
int methodEnd = source.indexOf("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)");
|
||||
@@ -58,8 +59,8 @@ public class IrisModdedStructureCommandTest {
|
||||
|
||||
@Test
|
||||
public void commandResolvesNativePolicyBeforeAnyVanillaAliasLookup() throws IOException {
|
||||
String source = source("IrisModdedCommands.java");
|
||||
int methodStart = source.indexOf("private static int gotoStructure(");
|
||||
String source = source("ModdedLocateCommands.java");
|
||||
int methodStart = source.indexOf("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)");
|
||||
@@ -79,7 +80,7 @@ public class IrisModdedStructureCommandTest {
|
||||
|
||||
@Test
|
||||
public void verifyResolvesRegisteredNativeBeforeGenericIrisAliases() throws IOException {
|
||||
String source = source("IrisModdedCommands.java");
|
||||
String source = source("ModdedLocateCommands.java");
|
||||
int methodStart = source.indexOf("private static int verifyStructure(");
|
||||
int methodEnd = source.indexOf("private static Optional<NativeStructureTarget> resolveNativeStructure(",
|
||||
methodStart);
|
||||
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* 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
@@ -1,86 +0,0 @@
|
||||
/*
|
||||
* 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
@@ -1,184 +0,0 @@
|
||||
/*
|
||||
* 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