mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-07-17 04:13:46 +00:00
dwa
This commit is contained in:
@@ -18,8 +18,8 @@ Consider supporting development by buying Iris on Spigot.
|
||||
| Folia | plugin jar | 26.2 | Region-safe scheduling throughout |
|
||||
| Spigot / CraftBukkit | plugin jar | 26.2 | Full feature set |
|
||||
| Fabric | mod jar | 26.2 | Server worldgen + client HUD; requires Fabric Loader 0.19.3+ |
|
||||
| Forge | mod jar | 26.2 | Server worldgen + client HUD; requires Forge 65.0.0+ |
|
||||
| NeoForge | mod jar | 26.2 | Server worldgen + client HUD; requires NeoForge 26.2+ |
|
||||
| Forge | mod jar | 26.2 | Server worldgen + client HUD; requires Forge 65.0.4+ |
|
||||
| NeoForge | mod jar | 26.2 | Server worldgen + client HUD; requires NeoForge 26.2.0.12-beta+ |
|
||||
|
||||
Java 25 is required on every platform.
|
||||
|
||||
|
||||
+139
-21
@@ -1,14 +1,17 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.QuartPos;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
@@ -26,12 +29,14 @@ import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class CustomBiomeSource extends BiomeSource {
|
||||
@@ -42,11 +47,13 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
private final Registry<Biome> biomeCustomRegistry;
|
||||
private final Registry<Biome> biomeRegistry;
|
||||
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
|
||||
private final KMap<String, Holder<Biome>> customBiomes;
|
||||
private final Map<Biome, Holder<Biome>> vanillaSpawnBiomes;
|
||||
private final Holder<Biome> fallbackBiome;
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> surfaceStructureBiomeCache = new ConcurrentHashMap<>();
|
||||
private volatile KMap<String, Holder<Biome>> customBiomes;
|
||||
private volatile Map<Biome, Holder<Biome>> vanillaSpawnBiomes;
|
||||
private volatile IrisDimension cacheDimension;
|
||||
|
||||
public CustomBiomeSource(long seed, Engine engine, World world) {
|
||||
this.engine = engine;
|
||||
@@ -56,20 +63,19 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
this.fallbackBiome = resolveFallbackBiome(this.biomeRegistry, this.biomeCustomRegistry);
|
||||
this.customBiomes = fillCustomBiomes(this.biomeCustomRegistry, engine, this.fallbackBiome);
|
||||
this.vanillaSpawnBiomes = fillVanillaSpawnBiomes(this.biomeCustomRegistry, this.biomeRegistry, engine);
|
||||
this.cacheDimension = engine.getDimension();
|
||||
}
|
||||
|
||||
private static List<Holder<Biome>> getAllBiomes(Registry<Biome> customRegistry, Registry<Biome> registry, Engine engine, Holder<Biome> fallback) {
|
||||
LinkedHashSet<Holder<Biome>> biomes = new LinkedHashSet<>();
|
||||
if (fallback != null) {
|
||||
biomes.add(fallback);
|
||||
}
|
||||
boolean resolutionFailed = false;
|
||||
|
||||
for (IrisBiome i : engine.getAllBiomes()) {
|
||||
Holder<Biome> vanillaHolder = NMSBinding.biomeToBiomeBase(registry, i.getVanillaDerivative());
|
||||
if (vanillaHolder != null) {
|
||||
biomes.add(vanillaHolder);
|
||||
} else if (!i.isCustom() && fallback != null) {
|
||||
biomes.add(fallback);
|
||||
} else if (!i.isCustom()) {
|
||||
resolutionFailed = true;
|
||||
}
|
||||
|
||||
if (i.isCustom()) {
|
||||
@@ -77,13 +83,17 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
Holder<Biome> customHolder = resolveCustomBiomeHolder(customRegistry, engine, j.getId());
|
||||
if (customHolder != null) {
|
||||
biomes.add(customHolder);
|
||||
} else if (fallback != null) {
|
||||
biomes.add(fallback);
|
||||
} else {
|
||||
resolutionFailed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((resolutionFailed || biomes.isEmpty()) && fallback != null) {
|
||||
biomes.add(fallback);
|
||||
}
|
||||
|
||||
return new ArrayList<>(biomes);
|
||||
}
|
||||
|
||||
@@ -135,12 +145,21 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
|
||||
@Override
|
||||
protected Stream<Holder<Biome>> collectPossibleBiomes() {
|
||||
return getAllBiomes(
|
||||
((RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()))
|
||||
.lookup(Registries.BIOME).orElse(null),
|
||||
((CraftWorld) engine.getWorld().realWorld()).getHandle().registryAccess().lookup(Registries.BIOME).orElse(null),
|
||||
engine,
|
||||
fallbackBiome).stream();
|
||||
return possibleStructureBiomes().stream();
|
||||
}
|
||||
|
||||
Set<Holder<Biome>> possibleStructureBiomes() {
|
||||
ensureCachesCurrent();
|
||||
World world = BukkitWorldBinding.world(engine.getWorld());
|
||||
if (world == null) {
|
||||
throw new IllegalStateException("Iris biome source has no bound Bukkit world");
|
||||
}
|
||||
Registry<Biome> customRegistry = ((RegistryAccess) getFor(
|
||||
RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()))
|
||||
.lookup(Registries.BIOME).orElse(null);
|
||||
Registry<Biome> worldRegistry = ((CraftWorld) world).getHandle().registryAccess()
|
||||
.lookup(Registries.BIOME).orElse(null);
|
||||
return Set.copyOf(getAllBiomes(customRegistry, worldRegistry, engine, fallbackBiome));
|
||||
}
|
||||
|
||||
private KMap<String, Holder<Biome>> fillCustomBiomes(Registry<Biome> customRegistry, Engine engine, Holder<Biome> fallback) {
|
||||
@@ -194,6 +213,7 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
}
|
||||
|
||||
Holder<Biome> getVanillaSpawnBiome(Holder<Biome> biome) {
|
||||
ensureCachesCurrent();
|
||||
if (biome == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -211,6 +231,11 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
|
||||
@Override
|
||||
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
||||
ensureCachesCurrent();
|
||||
if (isGuaranteedSurfaceBiome(y)) {
|
||||
return getSurfaceStructureBiomeHolder(x, z);
|
||||
}
|
||||
|
||||
long cacheKey = packNoiseKey(x, y, z);
|
||||
Holder<Biome> cachedHolder = structureBiomeCache.get(cacheKey);
|
||||
if (cachedHolder != null) {
|
||||
@@ -230,7 +255,69 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
return resolvedHolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Holder<Biome>> getBiomesWithin(int x, int y, int z, int radius, Climate.Sampler sampler) {
|
||||
ensureCachesCurrent();
|
||||
int minQuartY = QuartPos.fromBlock(y - radius);
|
||||
boolean monumentQuery = radius == 29
|
||||
&& y == engine.getMinHeight() + engine.getDimension().getFluidHeight();
|
||||
if (!monumentQuery && !isGuaranteedSurfaceBiome(minQuartY)) {
|
||||
return super.getBiomesWithin(x, y, z, radius, sampler);
|
||||
}
|
||||
int minQuartX = QuartPos.fromBlock(x - radius);
|
||||
int maxQuartX = QuartPos.fromBlock(x + radius);
|
||||
int minQuartZ = QuartPos.fromBlock(z - radius);
|
||||
int maxQuartZ = QuartPos.fromBlock(z + radius);
|
||||
int columns = (maxQuartX - minQuartX + 1) * (maxQuartZ - minQuartZ + 1);
|
||||
Set<Holder<Biome>> biomes = new HashSet<>(columns);
|
||||
for (int quartZ = minQuartZ; quartZ <= maxQuartZ; quartZ++) {
|
||||
for (int quartX = minQuartX; quartX <= maxQuartX; quartX++) {
|
||||
biomes.add(getSurfaceStructureBiomeHolder(quartX, quartZ));
|
||||
}
|
||||
}
|
||||
return biomes;
|
||||
}
|
||||
|
||||
private Holder<Biome> getSurfaceStructureBiomeHolder(int x, int z) {
|
||||
long columnKey = packColumnKey(x, z);
|
||||
Holder<Biome> surfaceHolder = surfaceStructureBiomeCache.get(columnKey);
|
||||
if (surfaceHolder != null) {
|
||||
return surfaceHolder;
|
||||
}
|
||||
Holder<Biome> resolvedSurfaceHolder = resolveSurfaceStructureBiomeHolder(x, z);
|
||||
Holder<Biome> existingSurfaceHolder = surfaceStructureBiomeCache.putIfAbsent(columnKey, resolvedSurfaceHolder);
|
||||
if (existingSurfaceHolder != null) {
|
||||
return existingSurfaceHolder;
|
||||
}
|
||||
if (surfaceStructureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
||||
surfaceStructureBiomeCache.clear();
|
||||
}
|
||||
return resolvedSurfaceHolder;
|
||||
}
|
||||
|
||||
private boolean isGuaranteedSurfaceBiome(int quartY) {
|
||||
if (engine == null || engine.isClosed() || engine.getComplex() == null) {
|
||||
return false;
|
||||
}
|
||||
int worldMinHeight = engine.getWorld().minHeight();
|
||||
int internalY = (quartY << 2) - worldMinHeight;
|
||||
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
|
||||
return internalY > caveSwitchInternalY;
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveSurfaceStructureBiomeHolder(int x, int z) {
|
||||
int blockX = x << 2;
|
||||
int blockZ = z << 2;
|
||||
IrisBiome irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
if (irisBiome == null) {
|
||||
return getFallbackBiome();
|
||||
}
|
||||
Holder<Biome> holder = NMSBinding.biomeToBiomeBase(biomeRegistry, irisBiome.getVanillaDerivative());
|
||||
return holder == null ? getFallbackBiome() : holder;
|
||||
}
|
||||
|
||||
public Holder<Biome> getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
||||
ensureCachesCurrent();
|
||||
long cacheKey = packNoiseKey(x, y, z);
|
||||
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
|
||||
if (cachedHolder != null) {
|
||||
@@ -250,6 +337,28 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
return resolvedHolder;
|
||||
}
|
||||
|
||||
private void ensureCachesCurrent() {
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
if (cacheDimension == dimension) {
|
||||
return;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (cacheDimension == dimension) {
|
||||
return;
|
||||
}
|
||||
KMap<String, Holder<Biome>> refreshedCustomBiomes = fillCustomBiomes(
|
||||
biomeCustomRegistry, engine, fallbackBiome);
|
||||
Map<Biome, Holder<Biome>> refreshedSpawnBiomes = fillVanillaSpawnBiomes(
|
||||
biomeCustomRegistry, biomeRegistry, engine);
|
||||
noiseBiomeCache.clear();
|
||||
structureBiomeCache.clear();
|
||||
surfaceStructureBiomeCache.clear();
|
||||
customBiomes = refreshedCustomBiomes;
|
||||
vanillaSpawnBiomes = refreshedSpawnBiomes;
|
||||
cacheDimension = dimension;
|
||||
}
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveStructureBiomeHolder(int x, int y, int z) {
|
||||
BiomeResolution resolution = resolveBiomeResolution(x, y, z);
|
||||
if (resolution == null) {
|
||||
@@ -315,14 +424,19 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
int blockY = y << 2;
|
||||
int worldMinHeight = engine.getWorld().minHeight();
|
||||
int internalY = blockY - worldMinHeight;
|
||||
int surfaceInternalY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
|
||||
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
|
||||
boolean deepUnderground = internalY <= caveSwitchInternalY;
|
||||
boolean belowSurface = internalY <= surfaceInternalY - 8;
|
||||
boolean underground = deepUnderground && belowSurface;
|
||||
IrisBiome irisBiome = underground
|
||||
? engine.getCaveBiome(blockX, internalY, blockZ)
|
||||
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
boolean underground = false;
|
||||
IrisBiome irisBiome;
|
||||
if (deepUnderground) {
|
||||
int surfaceInternalY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
|
||||
underground = internalY <= surfaceInternalY - 8;
|
||||
irisBiome = underground
|
||||
? engine.getCaveBiome(blockX, internalY, blockZ)
|
||||
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
} else {
|
||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
}
|
||||
if (irisBiome == null && underground) {
|
||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
}
|
||||
@@ -357,6 +471,10 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
| ((long) y & 4095L);
|
||||
}
|
||||
|
||||
private static long packColumnKey(int x, int z) {
|
||||
return ((long) x << 32) ^ ((long) z & 4294967295L);
|
||||
}
|
||||
|
||||
private static Holder<Biome> resolveCustomBiomeHolder(Registry<Biome> customRegistry, Engine engine, String customBiomeId) {
|
||||
if (customRegistry == null || engine == null || customBiomeId == null || customBiomeId.isBlank()) {
|
||||
return null;
|
||||
|
||||
+227
-86
@@ -1,5 +1,20 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisImportedStructureControl;
|
||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.engine.object.IrisVanillaStructureStiltSettings;
|
||||
import art.arcane.iris.nativegen.NativeStructurePostProcessor;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.iris.util.common.reflect.WrappedField;
|
||||
import art.arcane.iris.util.common.reflect.WrappedReturningMethod;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
@@ -7,6 +22,13 @@ 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.level.ServerLevel;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.util.random.Weighted;
|
||||
import net.minecraft.util.random.WeightedList;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
@@ -23,26 +45,6 @@ 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 java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisImportedStructureControl;
|
||||
import art.arcane.iris.util.common.reflect.WrappedField;
|
||||
import art.arcane.iris.util.common.reflect.WrappedReturningMethod;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.util.random.Weighted;
|
||||
import net.minecraft.util.random.WeightedList;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.MobCategory;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
@@ -52,35 +54,44 @@ import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
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;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import java.util.stream.Collectors;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.CraftWorld;
|
||||
import org.bukkit.craftbukkit.block.data.CraftBlockData;
|
||||
import org.bukkit.craftbukkit.generator.CustomChunkGenerator;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.spigotmc.SpigotWorldConfig;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
private static final String NATIVE_MONUMENT_KEY = "minecraft:monument";
|
||||
private static final WrappedField<ChunkGenerator, BiomeSource> BIOME_SOURCE;
|
||||
private static final WrappedReturningMethod<Heightmap, Object> SET_HEIGHT;
|
||||
private final ChunkGenerator delegate;
|
||||
private final Engine engine;
|
||||
private final CustomBiomeSource customBiomeSource;
|
||||
private final int runtimeMinY;
|
||||
private final int runtimeHeight;
|
||||
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
|
||||
private volatile Set<String> reachableStructureKeysCache;
|
||||
private volatile ReachableStructureCache reachableStructureCache;
|
||||
private volatile StructureStepCache structureStepCache;
|
||||
|
||||
public IrisChunkGenerator(ChunkGenerator delegate, long seed, Engine engine, World world) {
|
||||
this(delegate, engine, world, new CustomBiomeSource(seed, engine, world));
|
||||
@@ -91,6 +102,9 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
this.delegate = delegate;
|
||||
this.engine = engine;
|
||||
this.customBiomeSource = customBiomeSource;
|
||||
ServerLevel level = ((CraftWorld) world).getHandle();
|
||||
this.runtimeMinY = level.getMinY();
|
||||
this.runtimeHeight = level.getHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -105,16 +119,20 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
if (id == null) {
|
||||
continue;
|
||||
}
|
||||
int[] at = IrisStructureLocator.locate(engine, id.toString(), pos.getX(), pos.getZ(), Math.max(1, radius));
|
||||
if (at == null) {
|
||||
IrisStructureLocator.LocateResult result = IrisStructureLocator.locate(
|
||||
engine, id.toString(), pos.getX(), pos.getZ(), Math.max(1, radius));
|
||||
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
|
||||
continue;
|
||||
}
|
||||
long dx = (long) at[0] - pos.getX();
|
||||
long dz = (long) at[2] - pos.getZ();
|
||||
if (!result.found()) {
|
||||
continue;
|
||||
}
|
||||
long dx = (long) result.originX() - pos.getX();
|
||||
long dz = (long) result.originZ() - pos.getZ();
|
||||
long d = dx * dx + dz * dz;
|
||||
if (d < bestDist) {
|
||||
bestDist = d;
|
||||
best = new BlockPos(at[0], at[1], at[2]);
|
||||
best = new BlockPos(result.originX(), result.baseY(), result.originZ());
|
||||
bestHolder = holder;
|
||||
}
|
||||
}
|
||||
@@ -122,7 +140,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
return Pair.of(best, bestHolder);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.reportError("Iris-placed structure lookup failed near "
|
||||
+ pos.getX() + ", " + pos.getZ() + ".", e);
|
||||
}
|
||||
if (!importedControl().active()) {
|
||||
return null;
|
||||
@@ -134,28 +153,30 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
try {
|
||||
return delegate.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.error("Vanilla structure locate failed near " + pos.getX() + ", " + pos.getZ() + ": " + e);
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.reportError("Vanilla structure locate failed near "
|
||||
+ pos.getX() + ", " + pos.getZ() + ".", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private HolderSet<Structure> filterReachableStructures(ServerLevel level, HolderSet<Structure> holders) {
|
||||
Set<String> reachable = reachableStructureKeysCache;
|
||||
if (reachable == null) {
|
||||
reachable = VanillaStructureBiomes.reachableStructureKeys(level, delegate.getBiomeSource());
|
||||
reachableStructureKeysCache = reachable;
|
||||
}
|
||||
if (reachable.isEmpty()) {
|
||||
return holders;
|
||||
}
|
||||
Set<String> reachable = reachableStructureKeys(level);
|
||||
IrisImportedStructureControl control = importedControl();
|
||||
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
List<Holder<Structure>> kept = new ArrayList<>();
|
||||
List<Holder<Structure>> kept = new ArrayList<>(holders.size());
|
||||
for (Holder<Structure> holder : holders) {
|
||||
Object id = registry.getKey(holder.value());
|
||||
if (id != null && reachable.contains(id.toString())) {
|
||||
kept.add(holder);
|
||||
if (id == null) {
|
||||
continue;
|
||||
}
|
||||
String key = id.toString();
|
||||
IrisNativeStructureDecision decision = control.resolve(
|
||||
key, NativeStructurePostProcessor.isUndergroundStep(holder.value().step()));
|
||||
if (NATIVE_MONUMENT_KEY.equals(key) || !decision.generate()
|
||||
|| IrisStructureLocator.suppressesVanilla(engine, key) || !reachable.contains(key)) {
|
||||
continue;
|
||||
}
|
||||
kept.add(holder);
|
||||
}
|
||||
if (kept.size() == holders.size()) {
|
||||
return holders;
|
||||
@@ -163,6 +184,30 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
return HolderSet.direct(kept);
|
||||
}
|
||||
|
||||
private Set<String> reachableStructureKeys(ServerLevel level) {
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
ReachableStructureCache cached = reachableStructureCache;
|
||||
if (cached != null && cached.dimension() == dimension) {
|
||||
return cached.keys();
|
||||
}
|
||||
synchronized (this) {
|
||||
cached = reachableStructureCache;
|
||||
if (cached != null && cached.dimension() == dimension) {
|
||||
return cached.keys();
|
||||
}
|
||||
Set<String> reachable;
|
||||
try {
|
||||
reachable = Set.copyOf(VanillaStructureBiomes.reachableStructureKeys(level, customBiomeSource));
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError("Iris could not resolve native structure biome reachability; "
|
||||
+ "native locate is disabled until the next hotload.", error);
|
||||
reachable = Set.of();
|
||||
}
|
||||
reachableStructureCache = new ReachableStructureCache(dimension, reachable);
|
||||
return reachable;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<? extends ChunkGenerator> codec() {
|
||||
return MapCodec.unit(null);
|
||||
@@ -177,12 +222,12 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|
||||
@Override
|
||||
public int getMinY() {
|
||||
return delegate.getMinY();
|
||||
return runtimeMinY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSeaLevel() {
|
||||
return delegate.getSeaLevel();
|
||||
return runtimeMinY + engine.getDimension().getFluidHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -190,7 +235,42 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
if (!importedControl().active()) {
|
||||
return;
|
||||
}
|
||||
Map<Structure, StructureStart> previousStarts = new HashMap<>(access.getAllStarts());
|
||||
super.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey);
|
||||
adjustGeneratedStructures(registryAccess, access, previousStarts);
|
||||
}
|
||||
|
||||
private void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess access, Map<Structure, StructureStart> previousStarts) {
|
||||
Registry<Structure> registry = registryAccess.lookupOrThrow(Registries.STRUCTURE);
|
||||
IrisImportedStructureControl control = importedControl();
|
||||
for (Map.Entry<Structure, StructureStart> entry : access.getAllStarts().entrySet()) {
|
||||
Structure structure = entry.getKey();
|
||||
StructureStart start = entry.getValue();
|
||||
if (!start.isValid() || previousStarts.get(structure) == start) {
|
||||
continue;
|
||||
}
|
||||
Identifier id = registry.getKey(structure);
|
||||
String structureId = id == null ? null : id.toString();
|
||||
IrisNativeStructureDecision decision = control.resolve(
|
||||
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
|
||||
if (!decision.generate() || IrisStructureLocator.suppressesVanilla(engine, structureId)) {
|
||||
access.setStartForStructure(structure, StructureStart.INVALID_START);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
NativeStructurePostProcessor.applyVerticalShift(
|
||||
start,
|
||||
decision.yShift(),
|
||||
access.getMinY(),
|
||||
access.getMinY() + access.getHeight());
|
||||
} catch (RuntimeException error) {
|
||||
access.setStartForStructure(structure, StructureStart.INVALID_START);
|
||||
IrisLogging.reportError("Iris rejected native structure " + structureId + " in chunk "
|
||||
+ access.getPos().x() + "," + access.getPos().z()
|
||||
+ " because its vertical bounds are invalid.", error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IrisImportedStructureControl importedControl() {
|
||||
@@ -281,68 +361,119 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
SectionPos sectionPos = SectionPos.of(chunkPos, world.getMinSectionY());
|
||||
BlockPos origin = sectionPos.origin();
|
||||
Registry<Structure> registry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
Map<Integer, List<Structure>> byStep = registry.stream().collect(Collectors.groupingBy(s -> s.step().ordinal()));
|
||||
List<List<Structure>> byStep = structuresByStep(registry);
|
||||
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
|
||||
long decoSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ());
|
||||
BoundingBox area = writableArea(chunk);
|
||||
int steps = GenerationStep.Decoration.values().length;
|
||||
IrisImportedStructureControl control = importedControl();
|
||||
List<NativePlacementGroup> placementGroups = new ArrayList<>();
|
||||
List<NativeStructurePostProcessor.VegetationTarget> vegetationTargets = new ArrayList<>();
|
||||
for (int step = 0; step < steps; step++) {
|
||||
int index = 0;
|
||||
for (Structure structure : byStep.getOrDefault(step, List.of())) {
|
||||
for (Structure structure : byStep.get(step)) {
|
||||
Object id = registry.getKey(structure);
|
||||
String structureId = id == null ? null : id.toString();
|
||||
if (control.shouldGenerate(structureId) && !IrisStructureLocator.suppressesVanilla(engine, structureId)) {
|
||||
random.setFeatureSeed(decoSeed, index, step);
|
||||
int[] offset = control.resolveOffset(structureId, isUndergroundStep(structure.step()));
|
||||
boolean shifted = offset[0] != 0 || offset[1] != 0 || offset[2] != 0;
|
||||
WorldGenLevel target = shifted ? shiftedLevel(world, offset[0], offset[1], offset[2]) : world;
|
||||
BoundingBox placeArea = shifted
|
||||
? new BoundingBox(area.minX() - offset[0], area.minY(), area.minZ() - offset[2], area.maxX() - offset[0], area.maxY(), area.maxZ() - offset[2])
|
||||
: area;
|
||||
IrisNativeStructureDecision decision = control.resolve(
|
||||
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
|
||||
if (decision.generate() && !IrisStructureLocator.suppressesVanilla(engine, structureId)) {
|
||||
try {
|
||||
structureManager.startsForStructure(sectionPos, structure)
|
||||
.forEach(start -> start.placeInChunk(target, structureManager, this, random, placeArea, chunkPos));
|
||||
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
|
||||
if (!starts.isEmpty()) {
|
||||
List<StructureStart> resolvedStarts = List.copyOf(starts);
|
||||
placementGroups.add(new NativePlacementGroup(
|
||||
structureId, decision, index, step, resolvedStarts));
|
||||
for (StructureStart start : resolvedStarts) {
|
||||
vegetationTargets.add(new NativeStructurePostProcessor.VegetationTarget(
|
||||
start, decision.clearVegetation()));
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.reportError("Iris failed to resolve native structure " + structureId
|
||||
+ " in chunk " + chunkPos.x() + "," + chunkPos.z() + ".", e);
|
||||
}
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
try {
|
||||
NativeStructurePostProcessor.clearIntersectingVegetation(
|
||||
world, chunk, area, vegetationTargets);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError("Iris failed to clear vegetation from native structures in chunk "
|
||||
+ chunkPos.x() + "," + chunkPos.z() + ".", e);
|
||||
}
|
||||
for (NativePlacementGroup group : placementGroups) {
|
||||
random.setFeatureSeed(decoSeed, group.featureIndex(), group.step());
|
||||
try {
|
||||
for (StructureStart start : group.starts()) {
|
||||
placeVanillaStructure(world, structureManager, random, area, chunkPos,
|
||||
group.structureId(), start, group.decision());
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError("Iris failed to place native structure " + group.structureId()
|
||||
+ " in chunk " + chunkPos.x() + "," + chunkPos.z() + ".", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isUndergroundStep(GenerationStep.Decoration step) {
|
||||
return step == GenerationStep.Decoration.UNDERGROUND_STRUCTURES
|
||||
|| step == GenerationStep.Decoration.STRONGHOLDS;
|
||||
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::resolveStiltBlock);
|
||||
}
|
||||
|
||||
private WorldGenLevel shiftedLevel(WorldGenLevel world, int dx, int dy, int dz) {
|
||||
return (WorldGenLevel) Proxy.newProxyInstance(
|
||||
WorldGenLevel.class.getClassLoader(),
|
||||
new Class<?>[]{WorldGenLevel.class},
|
||||
(proxy, method, args) -> {
|
||||
if (args != null) {
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if (args[i] instanceof BlockPos bp) {
|
||||
args[i] = new BlockPos(bp.getX() + dx, bp.getY() + dy, bp.getZ() + dz);
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
return method.invoke(world, args);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw e.getCause();
|
||||
}
|
||||
});
|
||||
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 BlockState resolveStiltBlock(IrisVanillaStructureStiltSettings settings, RNG rng, int x, int y, int z) {
|
||||
if (settings.getPalette() == null) {
|
||||
return Blocks.COBBLESTONE.defaultBlockState();
|
||||
}
|
||||
PlatformBlockState platformState = settings.getPalette().get(rng, x, y, z, engine.getData());
|
||||
if (platformState == null || !(platformState.nativeHandle() instanceof BlockData blockData)) {
|
||||
return Blocks.COBBLESTONE.defaultBlockState();
|
||||
}
|
||||
if (blockData instanceof IrisCustomData customData) {
|
||||
blockData = customData.getBase();
|
||||
}
|
||||
if (blockData instanceof CraftBlockData craftBlockData) {
|
||||
return craftBlockData.getState();
|
||||
}
|
||||
return Blocks.COBBLESTONE.defaultBlockState();
|
||||
}
|
||||
|
||||
private BoundingBox writableArea(ChunkAccess chunk) {
|
||||
ChunkPos cp = chunk.getPos();
|
||||
int i = cp.getMinBlockX();
|
||||
int j = cp.getMinBlockZ();
|
||||
int minY = getMinY() + 1;
|
||||
int maxY = getMinY() + engine.getHeight() - 1;
|
||||
int minY = chunk.getMinY();
|
||||
int maxY = minY + chunk.getHeight() - 1;
|
||||
return new BoundingBox(i, minY, j, i + 15, maxY, j + 15);
|
||||
}
|
||||
|
||||
@@ -403,7 +534,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|
||||
@Override
|
||||
public int getGenDepth() {
|
||||
return delegate.getGenDepth();
|
||||
return runtimeHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -447,7 +578,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|
||||
Method setHeight = null;
|
||||
for (Method method : Heightmap.class.getDeclaredMethods()) {
|
||||
var types = method.getParameterTypes();
|
||||
Class<?>[] types = method.getParameterTypes();
|
||||
if (types.length != 3 || !Arrays.equals(types, new Class<?>[]{int.class, int.class, int.class})
|
||||
|| !method.getReturnType().equals(void.class))
|
||||
continue;
|
||||
@@ -475,4 +606,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|
||||
private record SpawnTableKey(Biome biome, MobCategory category) {
|
||||
}
|
||||
|
||||
private record ReachableStructureCache(IrisDimension dimension, Set<String> keys) {
|
||||
}
|
||||
|
||||
private record StructureStepCache(Registry<Structure> registry, List<List<Structure>> structures) {
|
||||
}
|
||||
|
||||
private record NativePlacementGroup(String structureId, IrisNativeStructureDecision decision,
|
||||
int featureIndex, int step, List<StructureStart> starts) {
|
||||
}
|
||||
}
|
||||
|
||||
+69
-50
@@ -13,6 +13,7 @@ import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.engine.data.chunk.TerrainChunk;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.util.project.agent.Agent;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
@@ -71,6 +72,7 @@ import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.commands.data.BlockDataAccessor;
|
||||
import net.minecraft.server.level.ChunkMap;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ThreadedLevelLightEngine;
|
||||
import net.minecraft.tags.TagKey;
|
||||
@@ -93,6 +95,7 @@ import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.chunk.status.WorldGenContext;
|
||||
import net.minecraft.world.level.dimension.LevelStem;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import net.minecraft.world.level.levelgen.FlatLevelSource;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.WorldgenRandom;
|
||||
@@ -1019,30 +1022,39 @@ public class NMSBinding implements INMSBinding {
|
||||
}
|
||||
|
||||
public void inject(long seed, Engine engine, World world) throws NoSuchFieldException, IllegalAccessException {
|
||||
var chunkMap = ((CraftWorld)world).getHandle().getChunkSource().chunkMap;
|
||||
var worldGenContextField = getField(chunkMap.getClass(), WorldGenContext.class);
|
||||
ServerLevel level = ((CraftWorld) world).getHandle();
|
||||
validateDimensionContract(engine, world, level);
|
||||
|
||||
ChunkMap chunkMap = level.getChunkSource().chunkMap;
|
||||
Field worldGenContextField = getField(chunkMap.getClass(), WorldGenContext.class);
|
||||
worldGenContextField.setAccessible(true);
|
||||
var worldGenContext = (WorldGenContext) worldGenContextField.get(chunkMap);
|
||||
var dimensionType = chunkMap.level.dimensionTypeRegistration().unwrapKey().orElse(null);
|
||||
String expectedDimensionType = "iris:" + engine.getDimension().getDimensionTypeKey();
|
||||
if (dimensionType != null) {
|
||||
String actualDimensionType = dimensionType.identifier().toString();
|
||||
if (!dimensionType.identifier().getNamespace().equals("iris")) {
|
||||
IrisLogging.error("Loaded world %s with invalid dimension type! expected=%s actual=%s", world.getName(), expectedDimensionType, actualDimensionType);
|
||||
} else {
|
||||
IrisLogging.debug("Loaded world " + world.getName() + " with Iris dimension type " + actualDimensionType);
|
||||
}
|
||||
} else {
|
||||
IrisLogging.error("Loaded world %s with unknown dimension type! expected=%s", world.getName(), expectedDimensionType);
|
||||
}
|
||||
WorldGenContext worldGenContext = (WorldGenContext) worldGenContextField.get(chunkMap);
|
||||
|
||||
IrisChunkGenerator irisGenerator = new IrisChunkGenerator(worldGenContext.generator(), seed, engine, world);
|
||||
var newContext = new WorldGenContext(
|
||||
WorldGenContext newContext = new WorldGenContext(
|
||||
worldGenContext.level(), irisGenerator,
|
||||
worldGenContext.structureManager(), worldGenContext.lightEngine(), worldGenContext.mainThreadExecutor(), worldGenContext.unsavedListener());
|
||||
|
||||
worldGenContextField.set(chunkMap, newContext);
|
||||
retargetStructureCheck(((CraftWorld) world).getHandle(), irisGenerator);
|
||||
retargetStructureCheck(level, irisGenerator);
|
||||
}
|
||||
|
||||
private void validateDimensionContract(Engine engine, World world, ServerLevel level) {
|
||||
DimensionType actualType = level.dimensionType();
|
||||
String actualTypeKey = level.dimensionTypeRegistration().unwrapKey()
|
||||
.map(key -> key.identifier().toString())
|
||||
.orElse("<unregistered>");
|
||||
IrisDimensionRuntimeContract expected = IrisDimensionRuntimeContract.expected(engine.getDimension(), "iris");
|
||||
IrisDimensionRuntimeContract actual = new IrisDimensionRuntimeContract(
|
||||
actualTypeKey,
|
||||
actualType.minY(),
|
||||
actualType.height(),
|
||||
actualType.logicalHeight());
|
||||
String runtimeName = "Bukkit world '" + world.getName() + "'";
|
||||
expected.requireExact(runtimeName, actual);
|
||||
expected.requireHeight(runtimeName, level.getMinY(), level.getHeight());
|
||||
expected.requireHeight(runtimeName, world.getMinHeight(), world.getMaxHeight() - world.getMinHeight());
|
||||
IrisLogging.debug("Loaded world " + world.getName() + " with exact Iris dimension type " + actualTypeKey);
|
||||
}
|
||||
|
||||
private static void retargetStructureCheck(ServerLevel level, IrisChunkGenerator generator) throws NoSuchFieldException, IllegalAccessException {
|
||||
@@ -1208,33 +1220,38 @@ public class NMSBinding implements INMSBinding {
|
||||
|
||||
@Override
|
||||
public boolean injectBukkit() {
|
||||
if (injected.getAndSet(true))
|
||||
return true;
|
||||
try {
|
||||
IrisLogging.info("Injecting Bukkit");
|
||||
new AgentBuilder.Default()
|
||||
.disableClassFormatChanges()
|
||||
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
||||
.type(ElementMatchers.is(ServerLevel.class))
|
||||
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
|
||||
builder.visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor()
|
||||
.and(ElementMatchers.takesArgument(0, MinecraftServer.class))
|
||||
.and(ElementMatchers.takesArgument(5, LevelStem.class)))))
|
||||
.installOn(Agent.getInstrumentation());
|
||||
ByteBuddy buddy = new ByteBuddy();
|
||||
for (Class<?> clazz : List.of(ChunkAccess.class, ProtoChunk.class)) {
|
||||
buddy.redefine(clazz)
|
||||
.visit(Advice.to(ChunkAccessAdvice.class).on(ElementMatchers.isMethod().and(ElementMatchers.takesArguments(ShortList.class, int.class))))
|
||||
.make()
|
||||
.load(clazz.getClassLoader(), Agent.installed());
|
||||
synchronized (injected) {
|
||||
if (injected.get()) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
IrisLogging.info("Injecting Bukkit");
|
||||
new AgentBuilder.Default()
|
||||
.disableClassFormatChanges()
|
||||
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
||||
.type(ElementMatchers.is(ServerLevel.class))
|
||||
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
|
||||
builder.visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor()
|
||||
.and(ElementMatchers.takesArgument(0, MinecraftServer.class))
|
||||
.and(ElementMatchers.takesArgument(5, LevelStem.class))
|
||||
.and(ElementMatchers.takesArgument(12, ChunkGenerator.class)))))
|
||||
.installOn(Agent.getInstrumentation());
|
||||
ByteBuddy buddy = new ByteBuddy();
|
||||
for (Class<?> clazz : List.of(ChunkAccess.class, ProtoChunk.class)) {
|
||||
buddy.redefine(clazz)
|
||||
.visit(Advice.to(ChunkAccessAdvice.class).on(ElementMatchers.isMethod().and(ElementMatchers.takesArguments(ShortList.class, int.class))))
|
||||
.make()
|
||||
.load(clazz.getClassLoader(), Agent.installed());
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.error(C.RED + "Failed to inject Bukkit");
|
||||
e.printStackTrace();
|
||||
injected.set(true);
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.error(C.RED + "Failed to inject Bukkit");
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1304,7 +1321,8 @@ public class NMSBinding implements INMSBinding {
|
||||
static void enter(
|
||||
@Advice.Argument(0) MinecraftServer server,
|
||||
@Advice.Argument(4) ResourceKey<Level> dimensionKey,
|
||||
@Advice.Argument(value = 5, readOnly = false) LevelStem levelStem
|
||||
@Advice.Argument(value = 5, readOnly = false) LevelStem levelStem,
|
||||
@Advice.Argument(12) ChunkGenerator constructorGenerator
|
||||
) {
|
||||
if (dimensionKey == null)
|
||||
return;
|
||||
@@ -1315,18 +1333,19 @@ public class NMSBinding implements INMSBinding {
|
||||
return;
|
||||
}
|
||||
|
||||
Object generator = Class.forName("art.arcane.iris.core.lifecycle.WorldLifecycleStaging", true, Bukkit.getPluginManager().getPlugin("Iris")
|
||||
.getClass()
|
||||
.getClassLoader())
|
||||
.getDeclaredMethod("consumeStemGenerator", String.class)
|
||||
.invoke(null, levelId);
|
||||
ClassLoader pluginClassLoader = Bukkit.getPluginManager().getPlugin("Iris").getClass().getClassLoader();
|
||||
Class<?> generatorType = Class.forName("art.arcane.iris.engine.platform.PlatformChunkGenerator", true, pluginClassLoader);
|
||||
Object generator = generatorType.isInstance(constructorGenerator) ? constructorGenerator : null;
|
||||
if (generator == null) {
|
||||
generator = Class.forName("art.arcane.iris.core.lifecycle.WorldLifecycleStaging", true, pluginClassLoader)
|
||||
.getDeclaredMethod("consumeStemGenerator", String.class)
|
||||
.invoke(null, levelId);
|
||||
}
|
||||
if (!(generator instanceof ChunkGenerator gen) || !gen.getClass().getPackageName().startsWith("art.arcane.iris")) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object bindings = Class.forName("art.arcane.iris.core.nms.INMS", true, Bukkit.getPluginManager().getPlugin("Iris")
|
||||
.getClass()
|
||||
.getClassLoader())
|
||||
Object bindings = Class.forName("art.arcane.iris.core.nms.INMS", true, pluginClassLoader)
|
||||
.getDeclaredMethod("get")
|
||||
.invoke(null);
|
||||
if (bindings == null) {
|
||||
|
||||
+4
-1
@@ -25,7 +25,10 @@ final class VanillaStructureBiomes {
|
||||
if (source == null) {
|
||||
return keys;
|
||||
}
|
||||
for (Holder<Biome> holder : source.possibleBiomes()) {
|
||||
Set<Holder<Biome>> possibleBiomes = source instanceof CustomBiomeSource customBiomeSource
|
||||
? customBiomeSource.possibleStructureBiomes()
|
||||
: source.possibleBiomes();
|
||||
for (Holder<Biome> holder : possibleBiomes) {
|
||||
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
|
||||
if (key.isPresent()) {
|
||||
keys.add(key.get().identifier().toString());
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
public class IrisChunkGeneratorMonumentLocateContractTest {
|
||||
@Test
|
||||
public void irisPlacementRunsBeforeExactNativeMonumentIsRemovedFromDelegateLookup() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")));
|
||||
int findStart = source.indexOf("findNearestMapStructure(ServerLevel level");
|
||||
assertTrue(findStart >= 0);
|
||||
int filterStart = source.indexOf("private HolderSet<Structure> filterReachableStructures", findStart);
|
||||
assertTrue(filterStart > findStart);
|
||||
String findMethod = source.substring(findStart, filterStart);
|
||||
int irisLocate = findMethod.indexOf("IrisStructureLocator.locate(");
|
||||
int searchLimit = findMethod.indexOf("LocateStatus.SEARCH_LIMIT_REACHED", irisLocate);
|
||||
int limitSkip = findMethod.indexOf("continue;", searchLimit);
|
||||
int nativeFilter = findMethod.indexOf("filterReachableStructures(level, holders)");
|
||||
int delegateLocate = findMethod.indexOf("delegate.findNearestMapStructure(level, reachable");
|
||||
int reachabilityStart = source.indexOf("private Set<String> reachableStructureKeys", filterStart);
|
||||
assertTrue(reachabilityStart > filterStart);
|
||||
String filterMethod = source.substring(filterStart, reachabilityStart);
|
||||
int monumentReject = filterMethod.indexOf("if (NATIVE_MONUMENT_KEY.equals(key)");
|
||||
int rejectContinue = filterMethod.indexOf("continue;", monumentReject);
|
||||
|
||||
assertTrue(irisLocate >= 0);
|
||||
assertTrue(searchLimit > irisLocate);
|
||||
assertTrue(limitSkip > searchLimit);
|
||||
assertTrue(nativeFilter > limitSkip);
|
||||
assertTrue(delegateLocate > nativeFilter);
|
||||
assertTrue(monumentReject >= 0);
|
||||
assertTrue(rejectContinue > monumentReject);
|
||||
assertTrue(findMethod.contains("new BlockPos(result.originX(), result.baseY(), result.originZ())"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stiltSupportUsesPlacedSolidOccupancyWithoutSnapshotDifferenceRequirement() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")));
|
||||
int placement = source.indexOf("start.placeInChunk(world, structureManager, generator");
|
||||
int stiltPlacement = source.indexOf("placeStilts(world, area, structureId, start", placement);
|
||||
int occupancyCheck = source.indexOf("if (state.isSolid())", stiltPlacement);
|
||||
|
||||
assertTrue(placement >= 0);
|
||||
assertTrue(stiltPlacement > placement);
|
||||
assertTrue(occupancyCheck > stiltPlacement);
|
||||
assertFalse(source.contains("state.equals("));
|
||||
assertFalse(source.contains("snapshot.states"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verticalShiftMovesPiecesJigsawJunctionsAndCachedBoundsTogether() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")));
|
||||
int method = source.indexOf("public static int applyVerticalShift");
|
||||
int clamp = source.indexOf("StructureVerticalBounds.clampOffset", method);
|
||||
int pieceMove = source.indexOf("piece.move(0, offsetY, 0)", clamp);
|
||||
int junctionMove = source.indexOf("junction.getSourceGroundY() + offsetY", pieceMove);
|
||||
int boundsMove = source.indexOf("bounds.move(0, offsetY, 0)", junctionMove);
|
||||
|
||||
assertTrue(method >= 0);
|
||||
assertTrue(clamp > method);
|
||||
assertTrue(pieceMove > clamp);
|
||||
assertTrue(junctionMove > pieceMove);
|
||||
assertTrue(boundsMove > junctionMove);
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NativeStructurePostProcessorVegetationTest {
|
||||
@Test
|
||||
public void surfaceStructuresClearTreeColumnsAutomatically() {
|
||||
assertTrue(NativeStructurePostProcessor.shouldClearVegetationColumn(100, 100, false));
|
||||
assertTrue(NativeStructurePostProcessor.shouldClearVegetationColumn(116, 100, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buriedStructuresPreserveUnrelatedSurfaceForest() {
|
||||
assertFalse(NativeStructurePostProcessor.shouldClearVegetationColumn(99, 100, false));
|
||||
assertFalse(NativeStructurePostProcessor.shouldClearVegetationColumn(20, 100, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void explicitVegetationOptionForcesUnusualPlacementCleanup() {
|
||||
assertTrue(NativeStructurePostProcessor.shouldClearVegetationColumn(20, 100, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allUndergroundGenerationStepsShareOneClassification() {
|
||||
assertTrue(NativeStructurePostProcessor.isUndergroundStep(
|
||||
GenerationStep.Decoration.UNDERGROUND_STRUCTURES));
|
||||
assertTrue(NativeStructurePostProcessor.isUndergroundStep(
|
||||
GenerationStep.Decoration.UNDERGROUND_DECORATION));
|
||||
assertTrue(NativeStructurePostProcessor.isUndergroundStep(
|
||||
GenerationStep.Decoration.STRONGHOLDS));
|
||||
assertFalse(NativeStructurePostProcessor.isUndergroundStep(
|
||||
GenerationStep.Decoration.SURFACE_STRUCTURES));
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,13 @@ def mainClass = 'art.arcane.iris.Iris'
|
||||
def bootstrapperClass = 'art.arcane.iris.IrisBootstrap'
|
||||
|
||||
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
|
||||
.orElse('com.github.VolmitSoftware:VolmLib:cdf7e8bb880199734dacd386a6616bda799fcde1')
|
||||
.orElse('com.github.VolmitSoftware:VolmLib:4c38988b344792a79f925f57f8a675bc85fc1bed')
|
||||
.get()
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(':core'))
|
||||
compileOnly(libs.lombok)
|
||||
annotationProcessor(libs.lombok)
|
||||
compileOnly(volmLibCoordinate) {
|
||||
transitive = false
|
||||
}
|
||||
@@ -21,7 +23,6 @@ dependencies {
|
||||
transitive = false
|
||||
}
|
||||
compileOnly(libs.placeholderApi)
|
||||
compileOnly(libs.multiverseCore)
|
||||
}
|
||||
|
||||
tasks.named('processResources').configure {
|
||||
@@ -41,3 +42,8 @@ tasks.named('processResources').configure {
|
||||
tasks.named('jar', Jar).configure {
|
||||
archiveBaseName.set('iris-bukkit-plugin')
|
||||
}
|
||||
|
||||
tasks.named('test').configure {
|
||||
systemProperty('iris.commandFindSource', file('src/main/java/art/arcane/iris/core/commands/CommandFind.java').absolutePath)
|
||||
systemProperty('iris.commandStructureSource', file('src/main/java/art/arcane/iris/core/commands/CommandStructure.java').absolutePath)
|
||||
}
|
||||
|
||||
@@ -18,8 +18,12 @@
|
||||
|
||||
package art.arcane.iris;
|
||||
|
||||
import art.arcane.iris.engine.IrisEngineEffects;
|
||||
import art.arcane.iris.engine.IrisWorldManager;
|
||||
|
||||
import art.arcane.iris.engine.framework.EngineComponentCleanup;
|
||||
import art.arcane.iris.engine.framework.EngineEffectsProvider;
|
||||
import art.arcane.iris.engine.framework.EnginePlatformHooks;
|
||||
import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
|
||||
import art.arcane.iris.core.splash.IrisSplashComposer;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
@@ -29,6 +33,7 @@ import art.arcane.iris.core.ServerConfigurator;
|
||||
import art.arcane.iris.core.datapack.DatapackIngestService;
|
||||
import art.arcane.iris.core.lifecycle.PaperLibBootstrap;
|
||||
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
|
||||
import art.arcane.iris.core.runtime.BukkitEnginePlatformHooks;
|
||||
import art.arcane.iris.core.runtime.TransientWorldCleanupSupport;
|
||||
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
|
||||
import art.arcane.iris.core.lifecycle.WorldLifecycleStaging;
|
||||
@@ -57,6 +62,7 @@ import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
||||
import art.arcane.iris.core.safeguard.IrisSafeguard;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.platform.bukkit.BukkitEnvironment;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
@@ -116,6 +122,7 @@ import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
@@ -201,27 +208,35 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
}
|
||||
|
||||
public static KList<Object> initialize(String s, Class<? extends Annotation> slicedClass) {
|
||||
private static <T> KList<T> initialize(String s, Class<T> requiredType) {
|
||||
JarScanner js = new JarScanner(instance.getJarFile(), s);
|
||||
KList<Object> v = new KList<>();
|
||||
KList<T> v = new KList<>();
|
||||
J.attempt(js::scan);
|
||||
for (Class<?> i : js.getClasses()) {
|
||||
if (slicedClass == null || i.isAnnotationPresent(slicedClass)) {
|
||||
try {
|
||||
v.add(i.getDeclaredConstructor().newInstance());
|
||||
} catch (Throwable ex) {
|
||||
Iris.warn("Skipped class initialization for %s: %s%s",
|
||||
i.getName(),
|
||||
ex.getClass().getSimpleName(),
|
||||
ex.getMessage() == null ? "" : " - " + ex.getMessage());
|
||||
Iris.reportError(ex);
|
||||
}
|
||||
if (!isConcreteImplementation(i, requiredType)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
v.add(requiredType.cast(i.getDeclaredConstructor().newInstance()));
|
||||
} catch (Throwable ex) {
|
||||
Iris.warn("Skipped class initialization for %s: %s%s",
|
||||
i.getName(),
|
||||
ex.getClass().getSimpleName(),
|
||||
ex.getMessage() == null ? "" : " - " + ex.getMessage());
|
||||
Iris.reportError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
static boolean isConcreteImplementation(Class<?> candidate, Class<?> requiredType) {
|
||||
int modifiers = candidate.getModifiers();
|
||||
return requiredType.isAssignableFrom(candidate)
|
||||
&& !candidate.isInterface()
|
||||
&& !Modifier.isAbstract(modifiers);
|
||||
}
|
||||
|
||||
public static KList<Class<?>> getClasses(String s, Class<? extends Annotation> slicedClass) {
|
||||
JarScanner js = new JarScanner(instance.getJarFile(), s);
|
||||
KList<Class<?>> v = new KList<>();
|
||||
@@ -243,10 +258,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
return v;
|
||||
}
|
||||
|
||||
public static KList<Object> initialize(String s) {
|
||||
return initialize(s, null);
|
||||
}
|
||||
|
||||
public static void sq(Runnable r) {
|
||||
synchronized (syncJobs) {
|
||||
syncJobs.queue(r);
|
||||
@@ -599,9 +610,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
services = new KMap<>();
|
||||
setupAudience();
|
||||
Bindings.setupSentry();
|
||||
initialize("art.arcane.iris.core.service").forEach((i) -> {
|
||||
services.put((Class<? extends IrisService>) i.getClass(), (IrisService) i);
|
||||
IrisServices.register(i.getClass(), i);
|
||||
initialize("art.arcane.iris.core.service", IrisService.class).forEach((i) -> {
|
||||
Class<? extends IrisService> serviceType = i.getClass().asSubclass(IrisService.class);
|
||||
services.put(serviceType, i);
|
||||
IrisServices.register(serviceType, i);
|
||||
});
|
||||
IrisServices.register(BlockEditAccess.class, services.get(EditSVC.class));
|
||||
IrisServices.register(PreservationRegistry.class, services.get(PreservationSVC.class));
|
||||
@@ -619,6 +631,9 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
tickets = new ChunkTickets();
|
||||
linkMultiverseCore = new MultiverseCoreLink();
|
||||
IrisServices.register(MultiverseCoreLink.class, linkMultiverseCore);
|
||||
IrisServices.register(EngineComponentCleanup.class, (EngineComponentCleanup) BukkitPlatform::unregisterListener);
|
||||
IrisServices.register(EngineEffectsProvider.class, (EngineEffectsProvider) IrisEngineEffects::new);
|
||||
IrisServices.register(EnginePlatformHooks.class, new BukkitEnginePlatformHooks());
|
||||
IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> {
|
||||
IrisWorldManager manager = new IrisWorldManager(engine);
|
||||
manager.startManager();
|
||||
@@ -711,7 +726,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
Iris.info(C.LIGHT_PURPLE + "Preparing Spawn for " + s + "' using Iris:" + generator + "...");
|
||||
WorldCreator c = WorldCreator.ofKey(worldKey)
|
||||
.generator(gen)
|
||||
.environment(dim.getEnvironment());
|
||||
.environment(BukkitEnvironment.from(dim.getEnvironment()));
|
||||
Long stagedSeed = IrisWorlds.readBukkitWorldSeed(s);
|
||||
if (stagedSeed != null) {
|
||||
c.seed(stagedSeed);
|
||||
@@ -1067,7 +1082,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
|
||||
private void setupPapi() {
|
||||
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
|
||||
if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) {
|
||||
new IrisPapiExpansion().register();
|
||||
}
|
||||
}
|
||||
@@ -1215,10 +1230,9 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
NamespacedKey worldKey = IrisWorldStorage.keyFromLegacyName(worldName);
|
||||
|
||||
IrisWorld w = IrisWorld.builder()
|
||||
.key(worldKey)
|
||||
.platformIdentity(worldKey.toString())
|
||||
.name(worldName)
|
||||
.seed(1337)
|
||||
.environment(dim.getEnvironment())
|
||||
.worldFolder(IrisWorldStorage.dimensionRoot(worldKey))
|
||||
.minHeight(dim.getMinHeight())
|
||||
.maxHeight(dim.getMaxHeight())
|
||||
|
||||
@@ -2,46 +2,19 @@ package art.arcane.iris;
|
||||
|
||||
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner;
|
||||
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner.ProvisionResult;
|
||||
import io.papermc.paper.datapack.Datapack;
|
||||
import io.papermc.paper.datapack.DatapackRegistrar;
|
||||
import io.papermc.paper.datapack.DiscoveredDatapack;
|
||||
import io.papermc.paper.plugin.bootstrap.BootstrapContext;
|
||||
import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
|
||||
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public final class IrisBootstrap implements PluginBootstrap {
|
||||
static final String PACK_ID = "generated";
|
||||
|
||||
@Override
|
||||
public void bootstrap(BootstrapContext context) {
|
||||
ProvisionResult provisioned = provision(context);
|
||||
Path datapackRoot = provisioned.datapackRoot();
|
||||
context.getLogger().info("Iris startup datapack is {} at {}", provisioned.status(), datapackRoot);
|
||||
context.getLifecycleManager().registerEventHandler(LifecycleEvents.DATAPACK_DISCOVERY, event -> {
|
||||
try {
|
||||
discoverPack(event.registrar(), datapackRoot);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Unable to discover the Iris startup datapack at " + datapackRoot, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static DiscoveredDatapack discoverPack(DatapackRegistrar registrar, Path datapackRoot) throws IOException {
|
||||
DiscoveredDatapack datapack = registrar.discoverPack(
|
||||
datapackRoot,
|
||||
PACK_ID,
|
||||
configurer -> configurer
|
||||
.autoEnableOnServerStart(true)
|
||||
.position(true, Datapack.Position.TOP)
|
||||
);
|
||||
if (datapack == null) {
|
||||
throw new IllegalStateException("Paper did not accept the Iris startup datapack at " + datapackRoot);
|
||||
}
|
||||
return datapack;
|
||||
}
|
||||
|
||||
private static ProvisionResult provision(BootstrapContext context) {
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
final class BukkitNativeStructureLocatePolicy {
|
||||
private static final String MONUMENT_KEY = "minecraft:monument";
|
||||
private static final String UNAVAILABLE_MESSAGE = "Native monument locating is unavailable on Paper, Purpur, and Folia because a cold search can stall the server thread. The monument lookup was skipped; monument generation is unaffected.";
|
||||
|
||||
private BukkitNativeStructureLocatePolicy() {
|
||||
}
|
||||
|
||||
static boolean isUnavailable(String structureKey) {
|
||||
return structureKey != null && MONUMENT_KEY.equalsIgnoreCase(structureKey.trim());
|
||||
}
|
||||
|
||||
static String unavailableMessage() {
|
||||
return UNAVAILABLE_MESSAGE;
|
||||
}
|
||||
}
|
||||
+95
-13
@@ -42,6 +42,7 @@ import io.papermc.paper.registry.RegistryKey;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Registry;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.generator.structure.Structure;
|
||||
import org.bukkit.util.StructureSearchResult;
|
||||
@@ -98,7 +99,7 @@ public class CommandFind implements DirectorExecutor {
|
||||
EngineBukkitOps.gotoPOI(e, type, player(), teleport);
|
||||
}
|
||||
|
||||
@Director(description = "Find a structure (a vanilla key like minecraft:village_plains or minecraft:stronghold, or an imported iris structure key)")
|
||||
@Director(description = "Find a structure (a vanilla key like minecraft:village_plains or minecraft:stronghold, or an imported iris structure key)", sync = true)
|
||||
public void structure(
|
||||
@Param(description = "The structure to look for (e.g. minecraft:village_plains, minecraft:stronghold, minecraft_ancient_city)", customHandler = StructureHandler.class)
|
||||
String structure
|
||||
@@ -117,7 +118,12 @@ public class CommandFind implements DirectorExecutor {
|
||||
}
|
||||
|
||||
if (IrisStructureLocator.isPlaced(e, structure)) {
|
||||
EngineBukkitOps.gotoStructure(e, structure, player(), true);
|
||||
locateIrisStructure(e, structure, commandSender);
|
||||
return;
|
||||
}
|
||||
|
||||
if (BukkitNativeStructureLocatePolicy.isUnavailable(structure)) {
|
||||
commandSender.sendMessage(C.RED + BukkitNativeStructureLocatePolicy.unavailableMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -127,6 +133,8 @@ public class CommandFind implements DirectorExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
World targetWorld = target.getWorld();
|
||||
Location origin = target.getLocation();
|
||||
commandSender.sendMessage(C.GRAY + "Locating " + structure + "...");
|
||||
J.s(() -> {
|
||||
try {
|
||||
@@ -140,32 +148,66 @@ public class CommandFind implements DirectorExecutor {
|
||||
}
|
||||
}
|
||||
if (match == null) {
|
||||
commandSender.sendMessage(C.RED + "Unknown structure: " + structure);
|
||||
sendStructureMessage(target, commandSender, C.RED + "Unknown structure: " + structure);
|
||||
return;
|
||||
}
|
||||
if (!StructureReachability.isReachable(e, structure)) {
|
||||
KList<String> miss = StructureReachability.missingBiomeKeys(e, structure);
|
||||
commandSender.sendMessage(C.YELLOW + structure + " cannot generate in this world (its required biomes are not produced by this pack"
|
||||
+ (miss.isEmpty() ? "" : ": needs " + String.join("/", miss)) + ").");
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.YELLOW + structure + " cannot generate in this world (its required biomes are not produced by this pack"
|
||||
+ (miss.isEmpty() ? "" : ": needs " + String.join("/", miss)) + ").");
|
||||
return;
|
||||
}
|
||||
StructureSearchResult result = target.getWorld().locateNearestStructure(target.getLocation(), match, 100, true);
|
||||
StructureSearchResult result = targetWorld.locateNearestStructure(origin, match, 100, false);
|
||||
if (result == null || result.getLocation() == null) {
|
||||
commandSender.sendMessage(C.YELLOW + "No " + structure + " found within range of you.");
|
||||
sendStructureMessage(target, commandSender, C.YELLOW + "No " + structure + " found within range of you.");
|
||||
return;
|
||||
}
|
||||
Location at = result.getLocation();
|
||||
int y = target.getWorld().getHighestBlockYAt(at.getBlockX(), at.getBlockZ()) + 2;
|
||||
Location dest = new Location(target.getWorld(), at.getBlockX() + 0.5, y, at.getBlockZ() + 0.5);
|
||||
BukkitPlatform.teleportAsync(target, dest);
|
||||
commandSender.sendMessage(C.GREEN + "Teleported to " + structure + " @ " + at.getBlockX() + ", " + at.getBlockZ());
|
||||
prepareStructureTeleport(target, targetWorld, commandSender, structure, result.getLocation(), false);
|
||||
} catch (Throwable t) {
|
||||
commandSender.sendMessage(C.RED + "Could not locate " + structure + ": " + t.getClass().getSimpleName());
|
||||
sendStructureMessage(target, commandSender, C.RED + "Could not locate " + structure + ": " + t.getClass().getSimpleName());
|
||||
Iris.reportError("Could not locate structure '" + structure + "'.", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void locateIrisStructure(Engine engine, String structure, VolmitSender commandSender) {
|
||||
Player target = player();
|
||||
if (target == null) {
|
||||
commandSender.sendMessage(C.GOLD + "Run this in-game to teleport to a structure.");
|
||||
return;
|
||||
}
|
||||
World targetWorld = target.getWorld();
|
||||
Location origin = target.getLocation();
|
||||
int blockX = origin.getBlockX();
|
||||
int blockZ = origin.getBlockZ();
|
||||
commandSender.sendMessage(C.GRAY + "Locating " + structure + "...");
|
||||
J.a(() -> {
|
||||
try {
|
||||
IrisStructureLocator.LocateResult result =
|
||||
IrisStructureLocator.locate(engine, structure, blockX, blockZ, 1024);
|
||||
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.YELLOW + "Unable to locate " + structure
|
||||
+ ": the density search safety limit was reached before the full 1024-chunk radius was searched.");
|
||||
return;
|
||||
}
|
||||
if (!result.found()) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.YELLOW + "No " + structure + " found within 1024 chunks of you.");
|
||||
return;
|
||||
}
|
||||
Location destination = new Location(
|
||||
targetWorld, result.originX(), result.baseY(), result.originZ());
|
||||
prepareStructureTeleport(target, targetWorld, commandSender, structure, destination, true);
|
||||
} catch (Throwable t) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.RED + "Could not locate " + structure + ": " + t.getClass().getSimpleName());
|
||||
Iris.reportError("Could not locate Iris-placed structure '" + structure + "'.", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Find an object")
|
||||
public void object(
|
||||
@Param(description = "The object to look for", customHandler = ObjectHandler.class)
|
||||
@@ -199,4 +241,44 @@ public class CommandFind implements DirectorExecutor {
|
||||
|
||||
sender().sendMessage(C.RED + object + " is not configured in any region/biome object placements.");
|
||||
}
|
||||
|
||||
private void prepareStructureTeleport(Player target, World world, VolmitSender commandSender, String structure,
|
||||
Location at, boolean useLocatedY) {
|
||||
int chunkX = at.getBlockX() >> 4;
|
||||
int chunkZ = at.getBlockZ() >> 4;
|
||||
BukkitPlatform.chunkAtAsync(world, chunkX, chunkZ, true).whenComplete((chunk, error) -> {
|
||||
if (error != null) {
|
||||
sendStructureMessage(target, commandSender, C.RED + "Could not load the destination for " + structure + ".");
|
||||
Iris.reportError("Could not load structure destination '" + structure + "'.", error);
|
||||
return;
|
||||
}
|
||||
boolean scheduled = J.runRegion(world, chunkX, chunkZ,
|
||||
() -> teleportToStructure(target, world, commandSender, structure, at, useLocatedY));
|
||||
if (!scheduled) {
|
||||
sendStructureMessage(target, commandSender, C.RED + "Could not schedule the destination lookup for " + structure + ".");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void teleportToStructure(Player target, World world, VolmitSender commandSender, String structure,
|
||||
Location at, boolean useLocatedY) {
|
||||
try {
|
||||
int y = useLocatedY
|
||||
? Math.max(world.getMinHeight() + 1, Math.min(world.getMaxHeight() - 1, at.getBlockY() + 2))
|
||||
: world.getHighestBlockYAt(at.getBlockX(), at.getBlockZ()) + 2;
|
||||
Location destination = new Location(world, at.getBlockX() + 0.5, y, at.getBlockZ() + 0.5);
|
||||
J.runEntity(target, () -> {
|
||||
BukkitPlatform.teleportAsync(target, destination);
|
||||
commandSender.sendMessage(C.GREEN + "Teleported to " + structure + " @ "
|
||||
+ at.getBlockX() + ", " + y + ", " + at.getBlockZ());
|
||||
});
|
||||
} catch (Throwable t) {
|
||||
sendStructureMessage(target, commandSender, C.RED + "Could not prepare the destination for " + structure + ".");
|
||||
Iris.reportError("Could not prepare structure destination '" + structure + "'.", t);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendStructureMessage(Player target, VolmitSender commandSender, String message) {
|
||||
J.runEntity(target, () -> commandSender.sendMessage(message));
|
||||
}
|
||||
}
|
||||
|
||||
+124
-56
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.structure.BulkStructureImporter;
|
||||
import art.arcane.iris.core.structure.StructureCaptureImporter;
|
||||
@@ -41,7 +42,9 @@ import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import io.papermc.paper.registry.RegistryAccess;
|
||||
import io.papermc.paper.registry.RegistryKey;
|
||||
import org.bukkit.Bukkit;
|
||||
@@ -51,12 +54,12 @@ import org.bukkit.Registry;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.generator.structure.Structure;
|
||||
import org.bukkit.util.StructureSearchResult;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -111,7 +114,7 @@ public class CommandStructure implements DirectorExecutor {
|
||||
sender().sendMessage(C.GRAY + "Captured " + report.imported() + " structures. Place them from a 'structures' list and regenerate chunks. Delete a structures/*.json to re-capture it.");
|
||||
}
|
||||
|
||||
@Director(description = "Locate every vanilla/datapack/iris structure to verify which are locatable in this world. Heavy synchronous search per structure - keep the radius modest. 'Not found' can mean rarer than the radius, a different dimension (nether/end structures never appear in the overworld), or a structure that generates but cannot be located; generation itself happens during chunk decoration, independent of locate.", aliases = {"locateall"}, origin = DirectorOrigin.BOTH, sync = true)
|
||||
@Director(description = "Verify native structure eligibility and locate Iris-placed structures without running blocking native searches.", aliases = {"locateall"}, origin = DirectorOrigin.BOTH, sync = true)
|
||||
public void verify(
|
||||
@Param(description = "The dimension to verify", aliases = "dim")
|
||||
IrisDimension dimension,
|
||||
@@ -124,85 +127,150 @@ public class CommandStructure implements DirectorExecutor {
|
||||
return;
|
||||
}
|
||||
boolean senderIsPlayer = sender() != null && sender().isPlayer();
|
||||
Location center = (senderIsPlayer && player().getWorld() == world) ? player().getLocation() : world.getSpawnLocation();
|
||||
Location center = senderIsPlayer && player().getWorld() == world
|
||||
? player().getLocation()
|
||||
: world.getSpawnLocation();
|
||||
int searchRadius = Math.max(1, Math.min(radius, 1000));
|
||||
|
||||
sender().sendMessage(C.GREEN + "Verifying structures in " + C.WHITE + world.getName() + C.GREEN + " from " + center.getBlockX() + "," + center.getBlockZ() + " within " + searchRadius + " chunks...");
|
||||
|
||||
Engine engine = null;
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(world);
|
||||
if (access != null) {
|
||||
engine = access.getEngine();
|
||||
Engine engine = access == null ? null : access.getEngine();
|
||||
if (engine == null) {
|
||||
sender().sendMessage(C.RED + "The selected Iris world has no active generator engine.");
|
||||
return;
|
||||
}
|
||||
Set<String> reachable = engine == null ? Collections.emptySet() : StructureReachability.reachableKeys(engine);
|
||||
|
||||
int found = 0;
|
||||
int missing = 0;
|
||||
int unreachable = 0;
|
||||
int irisPlaced = 0;
|
||||
KList<String> notFound = new KList<>();
|
||||
KList<String> cannotGenerate = new KList<>();
|
||||
KList<String> structureKeys = new KList<>();
|
||||
Registry<Structure> structureRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.STRUCTURE);
|
||||
for (Structure structure : structureRegistry) {
|
||||
NamespacedKey key = structureRegistry.getKey(structure);
|
||||
String keyName = key == null ? structure.toString() : key.toString();
|
||||
boolean isIrisPlaced = engine != null && IrisStructureLocator.suppressesVanilla(engine, keyName);
|
||||
boolean isReachable = engine != null && reachable.contains(keyName.toLowerCase());
|
||||
if (engine != null && !isIrisPlaced && !isReachable) {
|
||||
unreachable++;
|
||||
KList<String> miss = StructureReachability.missingBiomeKeys(engine, keyName);
|
||||
cannotGenerate.add(keyName + (miss.isEmpty() ? "" : " (needs " + String.join("/", miss) + ")"));
|
||||
if (key != null) {
|
||||
structureKeys.add(key.toString());
|
||||
}
|
||||
}
|
||||
VolmitSender commandSender = sender();
|
||||
Player target = senderIsPlayer ? player() : null;
|
||||
commandSender.sendMessage(C.GREEN + "Verifying structures in " + C.WHITE + world.getName()
|
||||
+ C.GREEN + " from " + center.getBlockX() + "," + center.getBlockZ()
|
||||
+ " within " + searchRadius + " chunks...");
|
||||
int centerX = center.getBlockX();
|
||||
int centerZ = center.getBlockZ();
|
||||
J.a(() -> runVerification(engine, structureKeys, centerX, centerZ, searchRadius, commandSender, target));
|
||||
}
|
||||
|
||||
private void runVerification(Engine engine, KList<String> structureKeys, int centerX, int centerZ,
|
||||
int searchRadius, VolmitSender commandSender, Player target) {
|
||||
KList<String> messages = new KList<>();
|
||||
Set<String> reachable;
|
||||
try {
|
||||
reachable = StructureReachability.reachableKeys(engine);
|
||||
} catch (Throwable error) {
|
||||
messages.add(C.RED + "Structure verification could not resolve native biome reachability.");
|
||||
sendVerificationMessages(commandSender, target, messages);
|
||||
Iris.reportError("Could not resolve native structure biome reachability for verification.", error);
|
||||
return;
|
||||
}
|
||||
int located = 0;
|
||||
int nativeEligible = 0;
|
||||
int disabled = 0;
|
||||
int unreachable = 0;
|
||||
int unavailable = 0;
|
||||
int searchLimited = 0;
|
||||
int errors = 0;
|
||||
for (String keyName : structureKeys) {
|
||||
boolean irisPlaced = IrisStructureLocator.isPlaced(engine, keyName);
|
||||
if (irisPlaced) {
|
||||
try {
|
||||
IrisStructureLocator.LocateResult result =
|
||||
IrisStructureLocator.locate(engine, keyName, centerX, centerZ, searchRadius);
|
||||
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
|
||||
searchLimited++;
|
||||
messages.add(C.YELLOW + "[iris-search-limit] " + C.WHITE + keyName + C.YELLOW
|
||||
+ ": unable to complete the density search before its safety limit was reached");
|
||||
continue;
|
||||
}
|
||||
if (!result.found()) {
|
||||
messages.add(C.YELLOW + "[iris-not-found] " + C.WHITE + keyName);
|
||||
continue;
|
||||
}
|
||||
located++;
|
||||
messages.add(C.AQUA + "[iris] " + C.WHITE + keyName + C.GREEN + " @ "
|
||||
+ result.originX() + "," + result.baseY() + "," + result.originZ());
|
||||
} catch (Throwable error) {
|
||||
errors++;
|
||||
messages.add(C.RED + "[error] " + C.WHITE + keyName + C.RED + ": "
|
||||
+ error.getClass().getSimpleName());
|
||||
Iris.reportError("Could not verify Iris-placed structure '" + keyName + "'.", error);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (isIrisPlaced) {
|
||||
irisPlaced++;
|
||||
if (!engine.getDimension().getImportedStructures().shouldGenerate(keyName)) {
|
||||
disabled++;
|
||||
messages.add(C.GRAY + "[disabled] " + C.WHITE + keyName);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
StructureSearchResult result = world.locateNearestStructure(center, structure, searchRadius, true);
|
||||
if (result != null && result.getLocation() != null) {
|
||||
found++;
|
||||
Location l = result.getLocation();
|
||||
sender().sendMessage((isIrisPlaced ? C.AQUA + "[iris] " : C.GREEN + "[ok] ") + C.WHITE + keyName + C.GREEN + " @ " + l.getBlockX() + "," + l.getBlockZ());
|
||||
} else {
|
||||
missing++;
|
||||
notFound.add(keyName);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
missing++;
|
||||
notFound.add(keyName + " (error: " + e.getClass().getSimpleName() + ")");
|
||||
if (!reachable.contains(keyName.toLowerCase(Locale.ROOT))) {
|
||||
unreachable++;
|
||||
KList<String> missing = StructureReachability.missingBiomeKeys(engine, keyName);
|
||||
messages.add(C.YELLOW + "[unreachable] " + C.WHITE + keyName
|
||||
+ (missing.isEmpty() ? "" : C.YELLOW + " needs " + String.join("/", missing)));
|
||||
continue;
|
||||
}
|
||||
if (BukkitNativeStructureLocatePolicy.isUnavailable(keyName)) {
|
||||
unavailable++;
|
||||
messages.add(C.RED + "[unavailable] " + C.WHITE + keyName + C.RED + ": "
|
||||
+ BukkitNativeStructureLocatePolicy.unavailableMessage());
|
||||
continue;
|
||||
}
|
||||
nativeEligible++;
|
||||
messages.add(C.GREEN + "[native-eligible] " + C.WHITE + keyName);
|
||||
}
|
||||
messages.add(C.GREEN + "Structure verify: " + C.WHITE + located + C.GREEN + " Iris placements located, "
|
||||
+ C.WHITE + nativeEligible + C.GREEN + " native structures eligible, "
|
||||
+ C.WHITE + disabled + C.GREEN + " disabled by policy, "
|
||||
+ C.WHITE + unreachable + C.GREEN + " biome-unreachable, "
|
||||
+ C.WHITE + unavailable + C.GREEN + " unavailable on this platform, "
|
||||
+ C.WHITE + searchLimited + C.GREEN + " density searches safety-limited, "
|
||||
+ C.WHITE + errors + C.GREEN + " errors. Native eligibility is checked without running blocking live locates.");
|
||||
sendVerificationMessages(commandSender, target, messages);
|
||||
}
|
||||
|
||||
sender().sendMessage(C.GREEN + "Structure verify: " + C.WHITE + found + C.GREEN + " located (" + irisPlaced + " iris-placed), "
|
||||
+ C.WHITE + unreachable + C.GREEN + " cannot generate here, "
|
||||
+ C.WHITE + missing + C.GREEN + " reachable-but-not-found within " + searchRadius + " chunks.");
|
||||
if (!cannotGenerate.isEmpty()) {
|
||||
sender().sendMessage(C.RED + "Cannot generate (required biomes absent from this pack): " + C.GRAY + String.join(", ", cannotGenerate));
|
||||
}
|
||||
if (!notFound.isEmpty()) {
|
||||
sender().sendMessage(C.YELLOW + "Reachable but not found (rarer than radius): " + C.GRAY + String.join(", ", notFound));
|
||||
private void sendVerificationMessages(VolmitSender commandSender, Player target,
|
||||
KList<String> messages) {
|
||||
Runnable send = () -> {
|
||||
for (String message : messages) {
|
||||
commandSender.sendMessage(message);
|
||||
}
|
||||
};
|
||||
if (target != null) {
|
||||
J.runEntity(target, send);
|
||||
} else {
|
||||
J.s(send);
|
||||
}
|
||||
}
|
||||
|
||||
private World resolveIrisWorld(IrisDimension dimension) {
|
||||
if (sender() != null && sender().isPlayer() && IrisToolbelt.isIrisWorld(player().getWorld())) {
|
||||
return player().getWorld();
|
||||
PlatformChunkGenerator playerGenerator = IrisToolbelt.access(player().getWorld());
|
||||
if (matchesDimension(playerGenerator, dimension.getLoadKey())) {
|
||||
return player().getWorld();
|
||||
}
|
||||
}
|
||||
World fallback = null;
|
||||
for (World w : Bukkit.getWorlds()) {
|
||||
if (!IrisToolbelt.isIrisWorld(w)) {
|
||||
continue;
|
||||
}
|
||||
if (fallback == null) {
|
||||
fallback = w;
|
||||
}
|
||||
PlatformChunkGenerator gen = IrisToolbelt.access(w);
|
||||
if (gen != null && gen.getEngine() != null && gen.getEngine().getDimension() != null
|
||||
&& dimension.getLoadKey().equals(gen.getEngine().getDimension().getLoadKey())) {
|
||||
if (matchesDimension(gen, dimension.getLoadKey())) {
|
||||
return w;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
return null;
|
||||
}
|
||||
|
||||
static boolean matchesDimension(PlatformChunkGenerator generator, String dimensionKey) {
|
||||
return dimensionKey != null
|
||||
&& generator != null
|
||||
&& generator.getEngine() != null
|
||||
&& generator.getEngine().getDimension() != null
|
||||
&& dimensionKey.equals(generator.getEngine().getDimension().getLoadKey());
|
||||
}
|
||||
|
||||
@Director(description = "Resolve an iris structure's jigsaw graph and report piece count & bounds", origin = DirectorOrigin.BOTH)
|
||||
|
||||
+7
-5
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
@@ -43,6 +44,7 @@ import art.arcane.iris.engine.object.IrisNoiseGenerator;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.engine.object.NoiseStyle;
|
||||
import art.arcane.iris.engine.platform.EngineBukkitOps;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
@@ -621,14 +623,14 @@ public class CommandStudio implements DirectorExecutor {
|
||||
}
|
||||
|
||||
sender().sendMessage(C.GREEN + "Sending you to the studio world!");
|
||||
var player = player();
|
||||
BukkitPlatform.teleportAsync(player(), Iris.service(StudioSVC.class)
|
||||
Player player = player();
|
||||
IrisWorld studioWorld = Iris.service(StudioSVC.class)
|
||||
.getActiveProject()
|
||||
.getActiveProvider()
|
||||
.getTarget()
|
||||
.getWorld()
|
||||
.spawnLocation()
|
||||
).thenRun(() -> player.setGameMode(GameMode.SPECTATOR));
|
||||
.getWorld();
|
||||
BukkitPlatform.teleportAsync(player, BukkitWorldBinding.spawnLocation(studioWorld))
|
||||
.thenRun(() -> player.setGameMode(GameMode.SPECTATOR));
|
||||
}
|
||||
|
||||
@Director(description = "Update your dimension projects VSCode workspace")
|
||||
|
||||
+5
-4
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.render.RenderType;
|
||||
@@ -47,7 +48,7 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
||||
public List<GuiMarker> players() {
|
||||
IrisWorld world = engine.getWorld();
|
||||
List<GuiMarker> markers = new ArrayList<>();
|
||||
for (Player player : world.getPlayers()) {
|
||||
for (Player player : BukkitWorldBinding.players(world)) {
|
||||
markers.add(GuiMarker.player(player.getName(), player.getLocation().getX(), player.getLocation().getZ()));
|
||||
}
|
||||
return markers;
|
||||
@@ -58,7 +59,7 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
||||
J.s(() -> {
|
||||
IrisWorld world = engine.getWorld();
|
||||
List<GuiMarker> markers = new ArrayList<>();
|
||||
for (LivingEntity entity : world.getEntitiesByClass(LivingEntity.class)) {
|
||||
for (LivingEntity entity : BukkitWorldBinding.entities(world, LivingEntity.class)) {
|
||||
if (entity instanceof Player) {
|
||||
continue;
|
||||
}
|
||||
@@ -78,11 +79,11 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
||||
@Override
|
||||
public void teleport(double worldX, double worldZ) {
|
||||
IrisWorld world = engine.getWorld();
|
||||
if (!world.hasRealWorld()) {
|
||||
if (!world.hasPlatformWorld()) {
|
||||
return;
|
||||
}
|
||||
J.s(() -> {
|
||||
List<Player> players = world.getPlayers();
|
||||
List<Player> players = BukkitWorldBinding.players(world);
|
||||
if (players.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.runtime;
|
||||
|
||||
import art.arcane.iris.core.ServerConfigurator;
|
||||
import art.arcane.iris.core.datapack.DatapackIngestService;
|
||||
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.project.IrisProject;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.core.tools.WorldMaintenance;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineMode;
|
||||
import art.arcane.iris.engine.framework.EnginePlatformHooks;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import org.bukkit.World;
|
||||
|
||||
public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
|
||||
@Override
|
||||
public void refreshWorkspace(Engine engine) {
|
||||
new IrisProject(engine.getData().getDataFolder()).updateWorkspace();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshDatapackWorkspace(Engine engine) {
|
||||
DatapackIngestService.refreshWorkspace(engine.getData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reloadDatapacks(Engine engine) {
|
||||
synchronized (ServerConfigurator.class) {
|
||||
ServerConfigurator.installDataPacks(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fireHotloadEvent(Engine engine) {
|
||||
IrisPlatforms.get().callEvent(new IrisEngineHotloadEvent(engine));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateDimensionHotload(Engine engine, IrisDimension replacement) {
|
||||
IrisDimensionRuntimeContract.requireHotloadCompatible(
|
||||
"Bukkit Studio world '" + engine.getWorld().name() + "'",
|
||||
engine.getDimension(),
|
||||
replacement,
|
||||
"iris");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPregeneratorActive(Engine engine) {
|
||||
IrisWorld world = engine.getWorld();
|
||||
if (world == null) {
|
||||
return false;
|
||||
}
|
||||
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
|
||||
return pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(world.identity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdownPregenerator(Engine engine) {
|
||||
PregeneratorJob.shutdownInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldDisableChunkContextCache(Engine engine) {
|
||||
IrisWorld world = engine.getWorld();
|
||||
if (!J.isFolia() || world == null || !world.hasPlatformWorld()) {
|
||||
return false;
|
||||
}
|
||||
boolean maintenanceActive = WorldMaintenance.isWorldMaintenanceActive(world.identity());
|
||||
return EngineMode.shouldDisableContextCacheForMaintenance(maintenanceActive, isPregeneratorActive(engine));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldSkipMantleCleanup(Engine engine) {
|
||||
IrisWorld world = engine.getWorld();
|
||||
return world != null
|
||||
&& WorldMaintenance.isWorldMaintenanceActive(world.identity())
|
||||
&& !isPregeneratorActive(engine);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldSkipMantleMarkerRead(Engine engine, int chunkX, int chunkZ) {
|
||||
IrisWorld irisWorld = engine.getWorld();
|
||||
if (!J.isFolia() || irisWorld == null || !irisWorld.hasPlatformWorld()) {
|
||||
return false;
|
||||
}
|
||||
World world = BukkitWorldBinding.world(irisWorld);
|
||||
return world != null && J.isOwnedByCurrentRegion(world, chunkX, chunkZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldBypassMantleStages(Engine engine) {
|
||||
if (!J.isFolia() || !engine.getWorld().hasPlatformWorld()) {
|
||||
return false;
|
||||
}
|
||||
World world = BukkitWorldBinding.world(engine.getWorld());
|
||||
return world != null && IrisToolbelt.isWorldMaintenanceBypassingMantleStages(world);
|
||||
}
|
||||
}
|
||||
+5
-3
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import com.google.common.util.concurrent.AtomicDouble;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
@@ -9,6 +10,7 @@ import art.arcane.iris.core.runtime.GoldenHashScanner;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
@@ -312,7 +314,7 @@ public class IrisEngineSVC implements IrisService {
|
||||
|| engine.getMantle().getMantle().isClosed()
|
||||
|| !shouldReduce(engine))
|
||||
return;
|
||||
World engineWorld = engine.getWorld().realWorld();
|
||||
World engineWorld = BukkitWorldBinding.world(engine.getWorld());
|
||||
if (shouldSkipForMaintenance(engineWorld)) {
|
||||
return;
|
||||
}
|
||||
@@ -346,7 +348,7 @@ public class IrisEngineSVC implements IrisService {
|
||||
|| engine.getMantle().getMantle().isClosed()
|
||||
|| !shouldReduce(engine))
|
||||
return;
|
||||
World engineWorld = engine.getWorld().realWorld();
|
||||
World engineWorld = BukkitWorldBinding.world(engine.getWorld());
|
||||
if (shouldSkipForMaintenance(engineWorld)) {
|
||||
return;
|
||||
}
|
||||
@@ -446,7 +448,7 @@ public class IrisEngineSVC implements IrisService {
|
||||
}
|
||||
|
||||
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
|
||||
boolean pregeneratorTargetsWorld = pregeneratorJob != null && pregeneratorJob.targetsWorld(world);
|
||||
boolean pregeneratorTargetsWorld = pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(WorldIdentity.serialize(world));
|
||||
return shouldSkipMantleReductionForMaintenance(maintenanceActive, pregeneratorTargetsWorld);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,3 +8,65 @@ load: STARTUP
|
||||
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
|
||||
website: volmit.com
|
||||
description: More than a Dimension!
|
||||
dependencies:
|
||||
server:
|
||||
PlaceholderAPI:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
CraftEngine:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
Nexo:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
ItemsAdder:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
SCore:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
ExecutableItems:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: false
|
||||
MythicLib:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
MMOItems:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
eco:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
EcoItems:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
MythicMobs:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
MythicCrucible:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
KGenerators:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
Multiverse-Core:
|
||||
load: AFTER
|
||||
required: false
|
||||
join-classpath: true
|
||||
WorldEdit:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
|
||||
@@ -3,6 +3,22 @@ version: ${version}
|
||||
main: ${main}
|
||||
folia-supported: true
|
||||
load: STARTUP
|
||||
softdepend:
|
||||
- PlaceholderAPI
|
||||
- CraftEngine
|
||||
- Nexo
|
||||
- ItemsAdder
|
||||
- SCore
|
||||
- ExecutableItems
|
||||
- MythicLib
|
||||
- MMOItems
|
||||
- eco
|
||||
- EcoItems
|
||||
- MythicMobs
|
||||
- MythicCrucible
|
||||
- KGenerators
|
||||
- WorldEdit
|
||||
loadbefore: [ Multiverse-Core ]
|
||||
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
|
||||
website: volmit.com
|
||||
description: More than a Dimension!
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
package art.arcane.iris;
|
||||
|
||||
import io.papermc.paper.datapack.Datapack;
|
||||
import io.papermc.paper.datapack.DatapackRegistrar;
|
||||
import io.papermc.paper.datapack.DatapackSource;
|
||||
import io.papermc.paper.datapack.DiscoveredDatapack;
|
||||
import io.papermc.paper.plugin.configuration.PluginMeta;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.bukkit.FeatureFlag;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisBootstrapTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void discoverPackAutoEnablesAtFixedTop() throws Exception {
|
||||
File datapackDirectory = temporaryFolder.newFolder("datapack");
|
||||
RecordingRegistrar registrar = new RecordingRegistrar(DiscoveryBehavior.ACCEPT);
|
||||
|
||||
DiscoveredDatapack discovered = IrisBootstrap.discoverPack(registrar, datapackDirectory.toPath());
|
||||
|
||||
assertSame(registrar.discoveredDatapack, discovered);
|
||||
assertEquals(datapackDirectory.toPath(), registrar.discoveredPath);
|
||||
assertEquals(IrisBootstrap.PACK_ID, registrar.discoveredId);
|
||||
assertTrue(registrar.configurer.autoEnableOnServerStart);
|
||||
assertTrue(registrar.configurer.fixedPosition);
|
||||
assertEquals(Datapack.Position.TOP, registrar.configurer.position);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectedDiscoveryReportsDatapackPath() throws Exception {
|
||||
File datapackDirectory = temporaryFolder.newFolder("datapack");
|
||||
RecordingRegistrar registrar = new RecordingRegistrar(DiscoveryBehavior.REJECT);
|
||||
|
||||
IllegalStateException failure = assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> IrisBootstrap.discoverPack(registrar, datapackDirectory.toPath())
|
||||
);
|
||||
|
||||
assertTrue(failure.getMessage().contains(datapackDirectory.toPath().toString()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void discoveryIoFailurePropagates() throws Exception {
|
||||
File datapackDirectory = temporaryFolder.newFolder("datapack");
|
||||
RecordingRegistrar registrar = new RecordingRegistrar(DiscoveryBehavior.FAIL);
|
||||
|
||||
IOException failure = assertThrows(
|
||||
IOException.class,
|
||||
() -> IrisBootstrap.discoverPack(registrar, datapackDirectory.toPath())
|
||||
);
|
||||
|
||||
assertEquals("discovery failed", failure.getMessage());
|
||||
}
|
||||
|
||||
private enum DiscoveryBehavior {
|
||||
ACCEPT,
|
||||
REJECT,
|
||||
FAIL
|
||||
}
|
||||
|
||||
private static final class RecordingConfigurer implements DatapackRegistrar.Configurer {
|
||||
private boolean autoEnableOnServerStart;
|
||||
private boolean fixedPosition;
|
||||
private Datapack.Position position;
|
||||
|
||||
@Override
|
||||
public DatapackRegistrar.Configurer title(Component title) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatapackRegistrar.Configurer autoEnableOnServerStart(boolean autoEnableOnServerStart) {
|
||||
this.autoEnableOnServerStart = autoEnableOnServerStart;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatapackRegistrar.Configurer position(boolean fixed, Datapack.Position position) {
|
||||
this.fixedPosition = fixed;
|
||||
this.position = position;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RecordingRegistrar implements DatapackRegistrar {
|
||||
private final DiscoveryBehavior behavior;
|
||||
private final RecordingConfigurer configurer;
|
||||
private final DiscoveredDatapack discoveredDatapack;
|
||||
private Path discoveredPath;
|
||||
private String discoveredId;
|
||||
|
||||
private RecordingRegistrar(DiscoveryBehavior behavior) {
|
||||
this.behavior = behavior;
|
||||
this.configurer = new RecordingConfigurer();
|
||||
this.discoveredDatapack = new StubDiscoveredDatapack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPackDiscovered(String name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiscoveredDatapack getDiscoveredPack(String name) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeDiscoveredPack(String name) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, DiscoveredDatapack> getDiscoveredPacks() {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiscoveredDatapack discoverPack(URI uri, String id, Consumer<Configurer> configurer) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiscoveredDatapack discoverPack(Path path, String id, Consumer<Configurer> configurer) throws IOException {
|
||||
this.discoveredPath = path;
|
||||
this.discoveredId = id;
|
||||
if (behavior == DiscoveryBehavior.FAIL) {
|
||||
throw new IOException("discovery failed");
|
||||
}
|
||||
configurer.accept(this.configurer);
|
||||
return behavior == DiscoveryBehavior.ACCEPT ? discoveredDatapack : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiscoveredDatapack discoverPack(PluginMeta pluginMeta, URI uri, String id, Consumer<Configurer> configurer) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiscoveredDatapack discoverPack(PluginMeta pluginMeta, Path path, String id, Consumer<Configurer> configurer) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class StubDiscoveredDatapack implements DiscoveredDatapack {
|
||||
@Override
|
||||
public String getName() {
|
||||
return IrisBootstrap.PACK_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getTitle() {
|
||||
return Component.text(IrisBootstrap.PACK_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getDescription() {
|
||||
return Component.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRequired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Datapack.Compatibility getCompatibility() {
|
||||
return Datapack.Compatibility.COMPATIBLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<FeatureFlag> getRequiredFeatures() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatapackSource getSource() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package art.arcane.iris;
|
||||
|
||||
import art.arcane.iris.core.splash.IrisSplashPackScanner;
|
||||
import art.arcane.iris.core.service.CommandSVC;
|
||||
import art.arcane.iris.util.common.plugin.IrisService;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
@@ -13,9 +15,18 @@ import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisDiagnosticsTest {
|
||||
@Test
|
||||
public void serviceInitializationSkipsPackageHelperClasses() throws Exception {
|
||||
Class<?> registrar = Class.forName("art.arcane.iris.core.service.PaperCommandRegistrar");
|
||||
|
||||
assertFalse(Iris.isConcreteImplementation(registrar, IrisService.class));
|
||||
assertTrue(Iris.isConcreteImplementation(CommandSVC.class, IrisService.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reportErrorWithContextPrintsFullStacktrace() {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
|
||||
@@ -18,6 +18,30 @@ import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class PaperPluginMetadataTest {
|
||||
private static final List<String> OPTIONAL_PLUGIN_IDS = List.of(
|
||||
"PlaceholderAPI",
|
||||
"CraftEngine",
|
||||
"Nexo",
|
||||
"ItemsAdder",
|
||||
"SCore",
|
||||
"ExecutableItems",
|
||||
"MythicLib",
|
||||
"MMOItems",
|
||||
"eco",
|
||||
"EcoItems",
|
||||
"MythicMobs",
|
||||
"MythicCrucible",
|
||||
"KGenerators",
|
||||
"Multiverse-Core",
|
||||
"WorldEdit"
|
||||
);
|
||||
private static final List<String> JOINED_PLUGIN_IDS = OPTIONAL_PLUGIN_IDS.stream()
|
||||
.filter(pluginId -> !"ExecutableItems".equals(pluginId) && !"Multiverse-Core".equals(pluginId))
|
||||
.toList();
|
||||
private static final List<String> BUKKIT_SOFT_DEPEND_IDS = OPTIONAL_PLUGIN_IDS.stream()
|
||||
.filter(pluginId -> !"Multiverse-Core".equals(pluginId))
|
||||
.toList();
|
||||
|
||||
@Test
|
||||
public void paperMetadataDeclaresBootstrapAndFoliaSupport() throws Exception {
|
||||
String metadata;
|
||||
@@ -30,6 +54,11 @@ public class PaperPluginMetadataTest {
|
||||
assertTrue(metadata.contains("folia-supported: true"));
|
||||
assertTrue(metadata.contains("load: STARTUP"));
|
||||
assertFalse(metadata.contains("commands:"));
|
||||
for (String pluginId : JOINED_PLUGIN_IDS) {
|
||||
assertTrue(metadata.contains(optionalDependencyBlock(pluginId, "BEFORE", true)));
|
||||
}
|
||||
assertTrue(metadata.contains(optionalDependencyBlock("ExecutableItems", "BEFORE", false)));
|
||||
assertTrue(metadata.contains(optionalDependencyBlock("Multiverse-Core", "AFTER", true)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -45,6 +74,8 @@ public class PaperPluginMetadataTest {
|
||||
Map<String, Map<String, Object>> commands = metadata.getCommands();
|
||||
assertTrue(commands.containsKey("iris"));
|
||||
assertEquals(List.of("ir", "irs"), commands.get("iris").get("aliases"));
|
||||
assertEquals(BUKKIT_SOFT_DEPEND_IDS, metadata.getSoftDepend());
|
||||
assertEquals(List.of("Multiverse-Core"), metadata.getLoadBeforePlugins());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -56,4 +87,11 @@ public class PaperPluginMetadataTest {
|
||||
assertTrue(Files.isRegularFile(bukkitMetadata));
|
||||
assertFalse(Files.readString(bukkitMetadata, StandardCharsets.UTF_8).contains("${"));
|
||||
}
|
||||
|
||||
private static String optionalDependencyBlock(String pluginId, String loadOrder, boolean joinClasspath) {
|
||||
return " " + pluginId + ":\n"
|
||||
+ " load: " + loadOrder + "\n"
|
||||
+ " required: false\n"
|
||||
+ " join-classpath: " + joinClasspath + "\n";
|
||||
}
|
||||
}
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class BukkitNativeStructureLocatePolicyTest {
|
||||
@Test
|
||||
public void nativeMonumentLocateIsUnavailable() {
|
||||
assertTrue(BukkitNativeStructureLocatePolicy.isUnavailable("minecraft:monument"));
|
||||
assertTrue(BukkitNativeStructureLocatePolicy.isUnavailable(" MINECRAFT:MONUMENT "));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unrelatedAndNonCanonicalKeysRemainAvailable() {
|
||||
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable(null));
|
||||
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable(""));
|
||||
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable("minecraft:stronghold"));
|
||||
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable("minecraft:ocean_monument"));
|
||||
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable("minecraft:ocean_monuments"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectionExplainsSafetyAndGenerationBehavior() {
|
||||
String message = BukkitNativeStructureLocatePolicy.unavailableMessage().toLowerCase();
|
||||
|
||||
assertTrue(message.contains("unavailable"));
|
||||
assertTrue(message.contains("stall the server thread"));
|
||||
assertTrue(message.contains("generation is unaffected"));
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisStructureLocateCommandContractTest {
|
||||
@Test
|
||||
public void findReportsDensityLimitAndUsesExactLocatedOrigin() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.commandFindSource")));
|
||||
|
||||
assertTrue(source.contains("LocateStatus.SEARCH_LIMIT_REACHED"));
|
||||
assertTrue(source.contains("the density search safety limit was reached"));
|
||||
assertTrue(source.contains("result.originX(), result.baseY(), result.originZ()"));
|
||||
assertFalse(source.contains("at[0] + 8"));
|
||||
assertFalse(source.contains("at[2] + 8"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structureVerifyReportsDensityLimitAndUsesExactLocatedOrigin() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.commandStructureSource")));
|
||||
|
||||
assertTrue(source.contains("[iris-search-limit]"));
|
||||
assertTrue(source.contains("density searches safety-limited"));
|
||||
assertTrue(source.contains("result.originX() + \",\" + result.baseY() + \",\" + result.originZ()"));
|
||||
assertFalse(source.contains("at[0] + 8"));
|
||||
assertFalse(source.contains("at[2] + 8"));
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'net.fabricmc.fabric-loom' version '1.17.11'
|
||||
id 'net.fabricmc.fabric-loom' version '1.17.14'
|
||||
alias(libs.plugins.shadow)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ file('../../gradle.properties').withInputStream { InputStream stream -> rootProp
|
||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.2'))
|
||||
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2'))
|
||||
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(rootProperties.getProperty('fabricLoaderVersion', '0.19.3'))
|
||||
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:cdf7e8bb880199734dacd386a6616bda799fcde1'))
|
||||
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:4c38988b344792a79f925f57f8a675bc85fc1bed'))
|
||||
Closure<String> irisArtifactName = { String platform, String targetVersion ->
|
||||
return "Iris v${project.version} [${platform}] ${targetVersion}.jar"
|
||||
}
|
||||
@@ -49,6 +49,7 @@ sourceSets {
|
||||
main {
|
||||
java {
|
||||
srcDir '../modded-common/src/main/java'
|
||||
srcDir '../minecraft-common/src/main/java'
|
||||
srcDir '../client-common/src/main/java'
|
||||
}
|
||||
resources {
|
||||
@@ -99,6 +100,8 @@ configurations.named('bundle').configure {
|
||||
|
||||
configurations.compileClasspath.extendsFrom(configurations.devBundle)
|
||||
configurations.runtimeClasspath.extendsFrom(configurations.devBundle)
|
||||
configurations.testCompileClasspath.extendsFrom(configurations.devBundle)
|
||||
configurations.testRuntimeClasspath.extendsFrom(configurations.devBundle)
|
||||
|
||||
configurations.create('jij') {
|
||||
transitive = true
|
||||
@@ -127,24 +130,28 @@ dependencies {
|
||||
compileOnly(libs.spigot) {
|
||||
transitive = false
|
||||
}
|
||||
add('bundle', volmLibCoordinate) {
|
||||
transitive = false
|
||||
}
|
||||
add('devBundle', volmLibCoordinate) {
|
||||
transitive = false
|
||||
}
|
||||
add('bundle', libs.zip) {
|
||||
transitive = false
|
||||
}
|
||||
add('devBundle', libs.zip) {
|
||||
transitive = false
|
||||
}
|
||||
|
||||
List<Object> shared = [
|
||||
"art.arcane:core:${irisVersion}".toString(),
|
||||
"art.arcane:spi:${irisVersion}".toString(),
|
||||
volmLibCoordinate,
|
||||
libs.paralithic,
|
||||
libs.lru,
|
||||
libs.kotlin.stdlib,
|
||||
libs.kotlin.coroutines,
|
||||
libs.commons.lang,
|
||||
libs.commons.math3,
|
||||
libs.caffeine,
|
||||
libs.lz4,
|
||||
libs.zip,
|
||||
libs.sentry,
|
||||
libs.oshi,
|
||||
libs.byteBuddy.core,
|
||||
libs.byteBuddy.agent
|
||||
libs.dom4j,
|
||||
libs.jaxen,
|
||||
libs.sentry
|
||||
]
|
||||
shared.each { Object notation ->
|
||||
add('bundle', notation)
|
||||
@@ -157,6 +164,10 @@ tasks.named('compileJava', JavaCompile).configure {
|
||||
options.release.set(25)
|
||||
}
|
||||
|
||||
tasks.named('test').configure {
|
||||
systemProperty('iris.moddedCommonSources', file('../modded-common/src/main/java').absolutePath)
|
||||
}
|
||||
|
||||
loom {
|
||||
accessWidenerPath = file('src/main/resources/irisworldgen.accesswidener')
|
||||
runs {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,142 @@
|
||||
[21:25:27] [Test worker/INFO]: Iris registered custom content provider 'iris_deferred_test'
|
||||
[21:25:27] [Test worker/ERROR]: [worldcheck] server stop request failed
|
||||
java.lang.IllegalStateException: stop request failed
|
||||
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:218)
|
||||
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:171)
|
||||
at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:215)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
|
||||
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
|
||||
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
|
||||
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
|
||||
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
|
||||
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
|
||||
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
|
||||
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
|
||||
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
|
||||
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
|
||||
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
|
||||
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
|
||||
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175)
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84)
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47)
|
||||
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65)
|
||||
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
|
||||
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77)
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28)
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19)
|
||||
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
|
||||
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88)
|
||||
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
|
||||
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122)
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
|
||||
[21:25:27] [Test worker/ERROR]: [worldcheck] waiting for server shutdown failed
|
||||
java.lang.IllegalStateException: shutdown wait failed
|
||||
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:244)
|
||||
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:186)
|
||||
at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:242)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
|
||||
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
|
||||
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
|
||||
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
|
||||
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
|
||||
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
|
||||
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
|
||||
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
|
||||
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
|
||||
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
|
||||
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
|
||||
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
|
||||
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175)
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84)
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47)
|
||||
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65)
|
||||
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
|
||||
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77)
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28)
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19)
|
||||
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
|
||||
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88)
|
||||
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
|
||||
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122)
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
|
||||
[21:25:27] [Test worker/ERROR]: [worldcheck] check failed
|
||||
java.lang.IllegalStateException: check failed
|
||||
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:204)
|
||||
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:166)
|
||||
at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:202)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
|
||||
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
|
||||
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
|
||||
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
|
||||
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
|
||||
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
|
||||
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
|
||||
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
|
||||
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
|
||||
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
|
||||
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
|
||||
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
|
||||
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175)
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84)
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47)
|
||||
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65)
|
||||
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
|
||||
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77)
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28)
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19)
|
||||
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
|
||||
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88)
|
||||
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
|
||||
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122)
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
|
||||
@@ -19,15 +19,18 @@
|
||||
package art.arcane.iris.fabric;
|
||||
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.ModdedBlockBreakHandler;
|
||||
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
||||
import art.arcane.iris.modded.command.IrisModdedCommands;
|
||||
import art.arcane.iris.modded.command.ModdedWandService;
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLevelEvents;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
|
||||
import net.fabricmc.fabric.api.event.player.AttackBlockCallback;
|
||||
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
|
||||
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
|
||||
import net.minecraft.commands.CommandBuildContext;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
@@ -38,10 +41,13 @@ import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
|
||||
public final class IrisFabricBootstrap implements ModInitializer {
|
||||
@@ -51,8 +57,16 @@ public final class IrisFabricBootstrap implements ModInitializer {
|
||||
() -> Registry.register(BuiltInRegistries.CHUNK_GENERATOR, Identifier.fromNamespaceAndPath("irisworldgen", "iris"), IrisModdedChunkGenerator.CODEC));
|
||||
FabricProtocolNetworking.install();
|
||||
ServerLifecycleEvents.SERVER_STARTING.register((MinecraftServer server) -> ModdedEngineBootstrap.start(server));
|
||||
ServerLifecycleEvents.SERVER_STARTED.register((MinecraftServer server) -> ModdedEngineBootstrap.serverStarted(server));
|
||||
ServerLifecycleEvents.SERVER_STOPPING.register((MinecraftServer server) -> ModdedEngineBootstrap.stop());
|
||||
ServerLevelEvents.LOAD.register((MinecraftServer server, ServerLevel level) -> ModdedEngineBootstrap.levelLoaded(level));
|
||||
CommandRegistrationCallback.EVENT.register((CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext buildContext, Commands.CommandSelection selection) -> IrisModdedCommands.register(dispatcher));
|
||||
PlayerBlockBreakEvents.BEFORE.register((Level level, Player player, BlockPos pos, BlockState state, BlockEntity blockEntity) -> {
|
||||
if (level instanceof ServerLevel serverLevel) {
|
||||
ModdedBlockBreakHandler.prepare(serverLevel, pos, state);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
AttackBlockCallback.EVENT.register((Player player, Level level, InteractionHand hand, BlockPos pos, Direction direction) ->
|
||||
ModdedWandService.attackBlock(player, level, hand, pos) ? InteractionResult.SUCCESS : InteractionResult.PASS);
|
||||
UseBlockCallback.EVENT.register((Player player, Level level, InteractionHand hand, BlockHitResult hit) ->
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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.fabric.mixin;
|
||||
|
||||
import art.arcane.iris.modded.ModdedBlockBreakHandler;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemInstance;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Mixin(Block.class)
|
||||
public class BlockMixin {
|
||||
@Inject(method = "getDrops(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/entity/BlockEntity;Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/item/ItemInstance;)Ljava/util/List;", at = @At("RETURN"), cancellable = true)
|
||||
private static void iris$applyBlockDrops(BlockState state, ServerLevel level, BlockPos position, BlockEntity blockEntity,
|
||||
Entity breaker, ItemInstance tool, CallbackInfoReturnable<List<ItemStack>> info) {
|
||||
if (!(breaker instanceof Player)) {
|
||||
return;
|
||||
}
|
||||
ModdedBlockBreakHandler.Result result = ModdedBlockBreakHandler.completePrepared(level, position);
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
List<ItemStack> drops = result.replaceVanillaDrops()
|
||||
? new ArrayList<>()
|
||||
: new ArrayList<>(info.getReturnValue());
|
||||
drops.addAll(result.drops());
|
||||
info.setReturnValue(drops);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
"license": "GPL-3.0",
|
||||
"environment": "*",
|
||||
"accessWidener": "irisworldgen.accesswidener",
|
||||
"mixins": ["irisworldgen.mixins.json"],
|
||||
"mixins": ["irisworldgen.mixins.json", "irisworldgen.entity.mixins.json"],
|
||||
"entrypoints": {
|
||||
"main": ["art.arcane.iris.fabric.IrisFabricBootstrap"],
|
||||
"client": ["art.arcane.iris.fabric.IrisFabricClient"]
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"package": "art.arcane.iris.fabric.mixin",
|
||||
"compatibilityLevel": "JAVA_25",
|
||||
"mixins": [
|
||||
"LivingEntityMixin",
|
||||
"BlockMixin",
|
||||
"ServerPacksSourceMixin"
|
||||
],
|
||||
"injectors": {
|
||||
|
||||
+42
-35
@@ -22,7 +22,7 @@ import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'net.minecraftforge.gradle' version '7.0.27'
|
||||
id 'net.minecraftforge.gradle' version '7.0.31'
|
||||
alias(libs.plugins.shadow)
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ Properties rootProperties = new Properties()
|
||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.2'))
|
||||
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2'))
|
||||
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(rootProperties.getProperty('forgeVersion', '26.2-65.0.0'))
|
||||
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:cdf7e8bb880199734dacd386a6616bda799fcde1'))
|
||||
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(rootProperties.getProperty('forgeVersion', '26.2-65.0.4'))
|
||||
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:4c38988b344792a79f925f57f8a675bc85fc1bed'))
|
||||
Closure<String> loaderDisplayVersion = { String loaderVersion ->
|
||||
String coordinatePrefix = "${minecraftVersion}-"
|
||||
if (loaderVersion.startsWith(coordinatePrefix)) {
|
||||
@@ -57,12 +57,18 @@ sourceSets {
|
||||
main {
|
||||
java {
|
||||
srcDir '../modded-common/src/main/java'
|
||||
srcDir '../minecraft-common/src/main/java'
|
||||
srcDir '../client-common/src/main/java'
|
||||
}
|
||||
resources {
|
||||
srcDir '../modded-common/src/main/resources'
|
||||
}
|
||||
}
|
||||
test {
|
||||
java {
|
||||
srcDir '../modded-common/src/test/java'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
@@ -88,11 +94,6 @@ configurations {
|
||||
canBeConsumed = false
|
||||
canBeResolved = true
|
||||
}
|
||||
commonsLangRaw {
|
||||
canBeConsumed = false
|
||||
canBeResolved = true
|
||||
transitive = false
|
||||
}
|
||||
}
|
||||
|
||||
configurations.named('bundle').configure {
|
||||
@@ -116,8 +117,10 @@ configurations.named('runBundle').configure {
|
||||
|
||||
configurations.compileClasspath.extendsFrom(configurations.devBundle)
|
||||
configurations.runtimeClasspath.extendsFrom(configurations.runBundle)
|
||||
configurations.testCompileClasspath.extendsFrom(configurations.compileClasspath, configurations.devBundle)
|
||||
configurations.testRuntimeClasspath.extendsFrom(configurations.runtimeClasspath, configurations.runBundle)
|
||||
|
||||
['compileClasspath', 'runtimeClasspath'].each { String name ->
|
||||
['compileClasspath', 'runtimeClasspath', 'testCompileClasspath', 'testRuntimeClasspath'].each { String name ->
|
||||
configurations.named(name).configure {
|
||||
resolutionStrategy.capabilitiesResolution.withCapability('org.lz4:lz4-java') {
|
||||
selectHighestVersion()
|
||||
@@ -127,48 +130,45 @@ configurations.runtimeClasspath.extendsFrom(configurations.runBundle)
|
||||
|
||||
dependencies {
|
||||
implementation minecraft.dependency("net.minecraftforge:forge:${forgeVersion}")
|
||||
testImplementation('junit:junit:4.13.2')
|
||||
compileOnly('org.slf4j:slf4j-api:2.0.17')
|
||||
compileOnly(libs.spigot) {
|
||||
transitive = false
|
||||
}
|
||||
add('bundle', volmLibCoordinate) {
|
||||
transitive = false
|
||||
}
|
||||
add('devBundle', volmLibCoordinate) {
|
||||
transitive = false
|
||||
}
|
||||
add('runBundle', volmLibCoordinate) {
|
||||
transitive = false
|
||||
}
|
||||
add('bundle', libs.zip) {
|
||||
transitive = false
|
||||
}
|
||||
add('devBundle', libs.zip) {
|
||||
transitive = false
|
||||
}
|
||||
add('runBundle', libs.zip) {
|
||||
transitive = false
|
||||
}
|
||||
|
||||
List<Object> shared = [
|
||||
"art.arcane:core:${irisVersion}".toString(),
|
||||
"art.arcane:spi:${irisVersion}".toString(),
|
||||
volmLibCoordinate,
|
||||
libs.paralithic,
|
||||
libs.lru,
|
||||
libs.kotlin.stdlib,
|
||||
libs.kotlin.coroutines,
|
||||
libs.commons.math3,
|
||||
libs.caffeine,
|
||||
libs.lz4,
|
||||
libs.zip,
|
||||
libs.sentry,
|
||||
libs.oshi,
|
||||
libs.byteBuddy.core,
|
||||
libs.byteBuddy.agent
|
||||
libs.dom4j,
|
||||
libs.jaxen,
|
||||
libs.sentry
|
||||
]
|
||||
shared.each { Object notation ->
|
||||
add('bundle', notation)
|
||||
add('devBundle', notation)
|
||||
add('runBundle', notation)
|
||||
}
|
||||
add('bundle', libs.commons.lang)
|
||||
add('devBundle', libs.commons.lang)
|
||||
add('commonsLangRaw', libs.commons.lang)
|
||||
}
|
||||
|
||||
TaskProvider<Jar> sanitizedCommonsLang = tasks.register('sanitizedCommonsLang', Jar) {
|
||||
archiveBaseName.set('commons-lang-sanitized')
|
||||
destinationDirectory.set(layout.buildDirectory.dir('sanitized'))
|
||||
from({ zipTree(configurations.named('commonsLangRaw').get().singleFile) })
|
||||
exclude('org/apache/commons/lang/enum/**')
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
}
|
||||
|
||||
dependencies {
|
||||
add('runBundle', files(sanitizedCommonsLang))
|
||||
}
|
||||
|
||||
minecraft {
|
||||
@@ -193,7 +193,7 @@ minecraft {
|
||||
systemProperty('iris.worldcheck', worldCheck)
|
||||
}
|
||||
jvmArgs('-Xmx8G')
|
||||
args('--nogui')
|
||||
args('--mixin.config', 'irisworldgen.entity.mixins.json', '--nogui')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -203,6 +203,10 @@ tasks.named('compileJava', JavaCompile).configure {
|
||||
options.release.set(25)
|
||||
}
|
||||
|
||||
tasks.named('test').configure {
|
||||
systemProperty('iris.moddedCommonSources', file('../modded-common/src/main/java').absolutePath)
|
||||
}
|
||||
|
||||
processResources {
|
||||
inputs.property('version', project.version)
|
||||
inputs.property('minecraftVersion', minecraftVersion)
|
||||
@@ -215,6 +219,9 @@ tasks.named('shadowJar', ShadowJar).configure {
|
||||
doFirst {
|
||||
delete(layout.buildDirectory.file("libs/Iris-${project.version}+mc${minecraftVersion}-forge.jar"))
|
||||
}
|
||||
manifest {
|
||||
attributes('MixinConfigs': 'irisworldgen.entity.mixins.json')
|
||||
}
|
||||
archiveFileName.set(irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}"))
|
||||
configurations = [project.configurations.named('bundle').get()]
|
||||
from(sourceSets.main.output)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,145 @@
|
||||
[13Jul2026 21:25:26.811] [Test worker/DEBUG] [io.netty.util.internal.logging.InternalLoggerFactory/]: Using SLF4J as the default logging framework
|
||||
[13Jul2026 21:25:26.813] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.level: simple
|
||||
[13Jul2026 21:25:26.813] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.targetRecords: 4
|
||||
[13Jul2026 21:25:28.681] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test'
|
||||
[13Jul2026 21:25:28.758] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed
|
||||
java.lang.IllegalStateException: stop request failed
|
||||
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:218) ~[test/:?]
|
||||
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:171) ~[main/:?]
|
||||
at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:215) ~[test/:?]
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
|
||||
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
|
||||
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
|
||||
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
|
||||
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
|
||||
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
|
||||
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
|
||||
[13Jul2026 21:25:28.765] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed
|
||||
java.lang.IllegalStateException: shutdown wait failed
|
||||
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:244) ~[test/:?]
|
||||
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:186) ~[main/:?]
|
||||
at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:242) ~[test/:?]
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
|
||||
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
|
||||
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
|
||||
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
|
||||
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
|
||||
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
|
||||
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
|
||||
[13Jul2026 21:25:28.769] [Test worker/ERROR] [Iris/]: [worldcheck] check failed
|
||||
java.lang.IllegalStateException: check failed
|
||||
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:204) ~[test/:?]
|
||||
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:166) ~[main/:?]
|
||||
at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:202) ~[test/:?]
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
|
||||
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
|
||||
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
|
||||
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
|
||||
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
|
||||
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
|
||||
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
|
||||
@@ -0,0 +1,142 @@
|
||||
[13Jul2026 21:25:28.681] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test'
|
||||
[13Jul2026 21:25:28.758] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed
|
||||
java.lang.IllegalStateException: stop request failed
|
||||
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:218) ~[test/:?]
|
||||
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:171) ~[main/:?]
|
||||
at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:215) ~[test/:?]
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
|
||||
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
|
||||
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
|
||||
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
|
||||
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
|
||||
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
|
||||
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
|
||||
[13Jul2026 21:25:28.765] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed
|
||||
java.lang.IllegalStateException: shutdown wait failed
|
||||
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:244) ~[test/:?]
|
||||
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:186) ~[main/:?]
|
||||
at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:242) ~[test/:?]
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
|
||||
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
|
||||
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
|
||||
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
|
||||
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
|
||||
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
|
||||
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
|
||||
[13Jul2026 21:25:28.769] [Test worker/ERROR] [Iris/]: [worldcheck] check failed
|
||||
java.lang.IllegalStateException: check failed
|
||||
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:204) ~[test/:?]
|
||||
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:166) ~[main/:?]
|
||||
at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:202) ~[test/:?]
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
|
||||
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
|
||||
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
|
||||
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
|
||||
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
|
||||
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
|
||||
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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.forge;
|
||||
|
||||
import art.arcane.iris.modded.ModdedBlockBreakHandler;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.storage.loot.LootContext;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
|
||||
import net.minecraft.world.level.storage.loot.predicates.LootItemCondition;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.minecraftforge.common.loot.IGlobalLootModifier;
|
||||
import net.minecraftforge.common.loot.LootModifier;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public final class IrisForgeBlockLootModifier extends LootModifier {
|
||||
public static final MapCodec<IrisForgeBlockLootModifier> CODEC = RecordCodecBuilder.mapCodec(instance ->
|
||||
codecStart(instance).apply(instance, IrisForgeBlockLootModifier::new));
|
||||
|
||||
public IrisForgeBlockLootModifier(LootItemCondition[] conditions) {
|
||||
super(conditions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapCodec<? extends IGlobalLootModifier> codec() {
|
||||
return CODEC;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected ObjectArrayList<ItemStack> doApply(LootTable table, ObjectArrayList<ItemStack> generatedLoot, LootContext context) {
|
||||
Entity breaker = context.getOptionalParameter(LootContextParams.THIS_ENTITY);
|
||||
BlockState state = context.getOptionalParameter(LootContextParams.BLOCK_STATE);
|
||||
Vec3 origin = context.getOptionalParameter(LootContextParams.ORIGIN);
|
||||
if (!(breaker instanceof Player) || state == null || origin == null) {
|
||||
return generatedLoot;
|
||||
}
|
||||
ServerLevel level = context.getLevel();
|
||||
BlockPos position = BlockPos.containing(origin);
|
||||
ModdedBlockBreakHandler.Result result = ModdedBlockBreakHandler.completePrepared(level, position);
|
||||
if (result == null) {
|
||||
return generatedLoot;
|
||||
}
|
||||
if (result.replaceVanillaDrops()) {
|
||||
generatedLoot.clear();
|
||||
}
|
||||
generatedLoot.addAll(result.drops());
|
||||
return generatedLoot;
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
package art.arcane.iris.forge;
|
||||
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.ModdedDeathLoot;
|
||||
import art.arcane.iris.modded.ModdedBlockBreakHandler;
|
||||
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
||||
import art.arcane.iris.modded.ModdedForcedDatapack;
|
||||
import art.arcane.iris.modded.ModdedProtocolHandler;
|
||||
@@ -27,23 +27,29 @@ import art.arcane.iris.modded.command.IrisModdedCommands;
|
||||
import art.arcane.iris.modded.command.ModdedWandService;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.server.packs.PackType;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraftforge.event.AddPackFindersEvent;
|
||||
import net.minecraftforge.event.RegisterCommandsEvent;
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
import net.minecraftforge.event.entity.living.LivingDropsEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
|
||||
import net.minecraftforge.event.level.BlockEvent;
|
||||
import net.minecraftforge.event.level.LevelEvent;
|
||||
import net.minecraftforge.event.server.ServerAboutToStartEvent;
|
||||
import net.minecraftforge.event.server.ServerStartedEvent;
|
||||
import net.minecraftforge.event.server.ServerStartingEvent;
|
||||
import net.minecraftforge.event.server.ServerStoppingEvent;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.common.loot.IGlobalLootModifier;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
|
||||
import net.minecraftforge.fml.loading.FMLEnvironment;
|
||||
import net.minecraftforge.fml.loading.FMLLoader;
|
||||
import net.minecraftforge.registries.DeferredRegister;
|
||||
import net.minecraftforge.registries.ForgeRegistries;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@@ -54,6 +60,9 @@ public final class IrisForgeBootstrap {
|
||||
DeferredRegister<MapCodec<? extends ChunkGenerator>> chunkGenerators = DeferredRegister.create(Registries.CHUNK_GENERATOR, "irisworldgen");
|
||||
chunkGenerators.register("iris", () -> IrisModdedChunkGenerator.CODEC);
|
||||
chunkGenerators.register(context.getModBusGroup());
|
||||
DeferredRegister<MapCodec<? extends IGlobalLootModifier>> lootModifiers = DeferredRegister.create(ForgeRegistries.GLOBAL_LOOT_MODIFIER_SERIALIZERS, "irisworldgen");
|
||||
lootModifiers.register("block_drops", () -> IrisForgeBlockLootModifier.CODEC);
|
||||
lootModifiers.register(context.getModBusGroup());
|
||||
});
|
||||
|
||||
ForgeProtocolNetworking.register();
|
||||
@@ -62,8 +71,15 @@ public final class IrisForgeBootstrap {
|
||||
IrisForgeClient.init();
|
||||
}
|
||||
|
||||
ServerAboutToStartEvent.BUS.addListener((ServerAboutToStartEvent event) -> ModdedEngineBootstrap.serverAboutToStart(event.getServer()));
|
||||
ServerStartingEvent.BUS.addListener((ServerStartingEvent event) -> ModdedEngineBootstrap.start(event.getServer()));
|
||||
ServerStartedEvent.BUS.addListener((ServerStartedEvent event) -> ModdedEngineBootstrap.serverStarted(event.getServer()));
|
||||
ServerStoppingEvent.BUS.addListener((ServerStoppingEvent event) -> ModdedEngineBootstrap.stop());
|
||||
LevelEvent.Load.BUS.addListener((LevelEvent.Load event) -> {
|
||||
if (event.getLevel() instanceof ServerLevel level) {
|
||||
ModdedEngineBootstrap.levelLoaded(level);
|
||||
}
|
||||
});
|
||||
PlayerEvent.PlayerLoggedInEvent.BUS.addListener((PlayerEvent.PlayerLoggedInEvent event) -> {
|
||||
if (event.getEntity() instanceof ServerPlayer player) {
|
||||
ModdedProtocolHandler.onPlayerJoin(player);
|
||||
@@ -80,7 +96,11 @@ public final class IrisForgeBootstrap {
|
||||
}
|
||||
});
|
||||
RegisterCommandsEvent.BUS.addListener((RegisterCommandsEvent event) -> IrisModdedCommands.register(event.getDispatcher()));
|
||||
LivingDropsEvent.BUS.addListener((LivingDropsEvent event) -> ModdedDeathLoot.handle(event.getEntity()));
|
||||
BlockEvent.BreakEvent.BUS.addListener((BlockEvent.BreakEvent event) -> {
|
||||
if (event.getLevel() instanceof ServerLevel level && !event.getResult().isDenied()) {
|
||||
ModdedBlockBreakHandler.prepare(level, event.getPos(), event.getState());
|
||||
}
|
||||
});
|
||||
PlayerInteractEvent.LeftClickBlock.BUS.addListener((Predicate<PlayerInteractEvent.LeftClickBlock>) (PlayerInteractEvent.LeftClickBlock event) ->
|
||||
ModdedWandService.attackBlock(event.getEntity(), event.getLevel(), event.getHand(), event.getPos()));
|
||||
PlayerInteractEvent.RightClickBlock.BUS.addListener((Predicate<PlayerInteractEvent.RightClickBlock>) (PlayerInteractEvent.RightClickBlock event) ->
|
||||
|
||||
+307
@@ -0,0 +1,307 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import art.arcane.iris.engine.framework.StructureVerticalBounds;
|
||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.engine.object.IrisVanillaStructureStiltSettings;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
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.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||
import net.minecraft.world.level.levelgen.WorldgenRandom;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.JigsawJunction;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.BitSet;
|
||||
import java.util.List;
|
||||
|
||||
public final class NativeStructurePostProcessor {
|
||||
private static final int FOUNDATION_VERTICAL_TOLERANCE = 1;
|
||||
|
||||
private NativeStructurePostProcessor() {
|
||||
}
|
||||
|
||||
public static void place(WorldGenLevel world, StructureManager structureManager, ChunkGenerator generator,
|
||||
WorldgenRandom random, BoundingBox area, ChunkPos chunkPos, String structureId,
|
||||
StructureStart start, IrisNativeStructureDecision decision,
|
||||
StiltBlockResolver stiltBlockResolver) {
|
||||
IrisVanillaStructureStiltSettings stilt = decision.stilt();
|
||||
start.placeInChunk(world, structureManager, generator, random, area, chunkPos);
|
||||
if (stilt != null) {
|
||||
placeStilts(world, area, structureId, start, stilt, stiltBlockResolver);
|
||||
}
|
||||
}
|
||||
|
||||
public static int applyVerticalShift(StructureStart start, int requestedOffset, int worldMinY,
|
||||
int worldMaxYExclusive) {
|
||||
BoundingBox bounds = start.getBoundingBox();
|
||||
int offsetY = StructureVerticalBounds.clampOffset(
|
||||
bounds.minY(), bounds.maxY(), requestedOffset, worldMinY, worldMaxYExclusive);
|
||||
if (offsetY == 0) {
|
||||
return 0;
|
||||
}
|
||||
for (StructurePiece piece : start.getPieces()) {
|
||||
piece.move(0, offsetY, 0);
|
||||
if (piece instanceof PoolElementStructurePiece poolPiece) {
|
||||
List<JigsawJunction> junctions = poolPiece.getJunctions();
|
||||
for (int i = 0; i < junctions.size(); i++) {
|
||||
JigsawJunction junction = junctions.get(i);
|
||||
junctions.set(i, new JigsawJunction(
|
||||
junction.getSourceX(),
|
||||
junction.getSourceGroundY() + offsetY,
|
||||
junction.getSourceZ(),
|
||||
junction.getDeltaY(),
|
||||
junction.getDestProjection()));
|
||||
}
|
||||
}
|
||||
}
|
||||
bounds.move(0, offsetY, 0);
|
||||
return offsetY;
|
||||
}
|
||||
|
||||
public static boolean isUndergroundStep(GenerationStep.Decoration step) {
|
||||
return step == GenerationStep.Decoration.UNDERGROUND_STRUCTURES
|
||||
|| step == GenerationStep.Decoration.UNDERGROUND_DECORATION
|
||||
|| step == GenerationStep.Decoration.STRONGHOLDS;
|
||||
}
|
||||
|
||||
public static void clearIntersectingVegetation(WorldGenLevel world, ChunkAccess chunk, BoundingBox area,
|
||||
List<VegetationTarget> targets) {
|
||||
if (targets == null || targets.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
VegetationSnapshot snapshot = captureVegetation(chunk, area);
|
||||
if (snapshot.treeBlockCount() == 0) {
|
||||
return;
|
||||
}
|
||||
boolean[] clearColumns = new boolean[area.getXSpan() * area.getZSpan()];
|
||||
for (VegetationTarget target : targets) {
|
||||
if (target != null && target.start() != null && target.start().isValid()) {
|
||||
markVegetationColumns(area, snapshot, target, clearColumns);
|
||||
}
|
||||
}
|
||||
clearVegetationColumns(world, area, snapshot, clearColumns);
|
||||
}
|
||||
|
||||
private static VegetationSnapshot captureVegetation(ChunkAccess chunk, BoundingBox area) {
|
||||
int width = area.getXSpan();
|
||||
int depth = area.getZSpan();
|
||||
BitSet[] columns = new BitSet[width * depth];
|
||||
int[] lowestY = new int[columns.length];
|
||||
Arrays.fill(lowestY, Integer.MAX_VALUE);
|
||||
int treeBlockCount = 0;
|
||||
LevelChunkSection[] sections = chunk.getSections();
|
||||
int chunkMinX = chunk.getPos().getMinBlockX();
|
||||
int chunkMinZ = chunk.getPos().getMinBlockZ();
|
||||
for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
|
||||
LevelChunkSection section = sections[sectionIndex];
|
||||
int sectionMinY = chunk.getSectionYFromSectionIndex(sectionIndex) << 4;
|
||||
int minY = Math.max(area.minY(), sectionMinY);
|
||||
int maxY = Math.min(area.maxY(), sectionMinY + 15);
|
||||
if (minY > maxY || section.hasOnlyAir() || !section.maybeHas(NativeStructurePostProcessor::isTreeBlock)) {
|
||||
continue;
|
||||
}
|
||||
for (int y = minY; y <= maxY; y++) {
|
||||
int localY = y - sectionMinY;
|
||||
for (int z = area.minZ(); z <= area.maxZ(); z++) {
|
||||
int localZ = z - chunkMinZ;
|
||||
for (int x = area.minX(); x <= area.maxX(); x++) {
|
||||
int localX = x - chunkMinX;
|
||||
if (!isTreeBlock(section.getBlockState(localX, localY, localZ))) {
|
||||
continue;
|
||||
}
|
||||
int column = (z - area.minZ()) * width + x - area.minX();
|
||||
BitSet treeBlocks = columns[column];
|
||||
if (treeBlocks == null) {
|
||||
treeBlocks = new BitSet(area.getYSpan());
|
||||
columns[column] = treeBlocks;
|
||||
}
|
||||
treeBlocks.set(y - area.minY());
|
||||
lowestY[column] = Math.min(lowestY[column], y);
|
||||
treeBlockCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new VegetationSnapshot(columns, lowestY, treeBlockCount);
|
||||
}
|
||||
|
||||
private static void markVegetationColumns(BoundingBox area, VegetationSnapshot snapshot,
|
||||
VegetationTarget target, boolean[] clearColumns) {
|
||||
int width = area.getXSpan();
|
||||
int[] pieceTops = new int[clearColumns.length];
|
||||
Arrays.fill(pieceTops, Integer.MIN_VALUE);
|
||||
for (StructurePiece piece : target.start().getPieces()) {
|
||||
BoundingBox bounds = piece.getBoundingBox();
|
||||
int minX = Math.max(area.minX(), bounds.minX());
|
||||
int maxX = Math.min(area.maxX(), bounds.maxX());
|
||||
int minZ = Math.max(area.minZ(), bounds.minZ());
|
||||
int maxZ = Math.min(area.maxZ(), bounds.maxZ());
|
||||
if (minX > maxX || minZ > maxZ) {
|
||||
continue;
|
||||
}
|
||||
for (int z = minZ; z <= maxZ; z++) {
|
||||
for (int x = minX; x <= maxX; x++) {
|
||||
int column = (z - area.minZ()) * width + x - area.minX();
|
||||
pieceTops[column] = Math.max(pieceTops[column], bounds.maxY());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int column = 0; column < pieceTops.length; column++) {
|
||||
if (snapshot.columns()[column] == null || pieceTops[column] == Integer.MIN_VALUE) {
|
||||
continue;
|
||||
}
|
||||
if (shouldClearVegetationColumn(pieceTops[column], snapshot.lowestY()[column], target.force())) {
|
||||
clearColumns[column] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static boolean shouldClearVegetationColumn(int pieceTopY, int lowestTreeY, boolean force) {
|
||||
return force || pieceTopY >= lowestTreeY;
|
||||
}
|
||||
|
||||
private static void clearVegetationColumns(WorldGenLevel world, BoundingBox area,
|
||||
VegetationSnapshot snapshot, boolean[] clearColumns) {
|
||||
int width = area.getXSpan();
|
||||
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
|
||||
BlockState air = Blocks.AIR.defaultBlockState();
|
||||
for (int z = area.minZ(); z <= area.maxZ(); z++) {
|
||||
for (int x = area.minX(); x <= area.maxX(); x++) {
|
||||
int column = (z - area.minZ()) * width + x - area.minX();
|
||||
BitSet treeBlocks = snapshot.columns()[column];
|
||||
if (!clearColumns[column] || treeBlocks == null) {
|
||||
continue;
|
||||
}
|
||||
for (int bit = treeBlocks.nextSetBit(0); bit >= 0; bit = treeBlocks.nextSetBit(bit + 1)) {
|
||||
int y = area.minY() + bit;
|
||||
position.set(x, y, z);
|
||||
BlockState state = world.getBlockState(position);
|
||||
if (isTreeBlock(state)) {
|
||||
world.setBlock(position, air, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isTreeBlock(BlockState state) {
|
||||
return state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES);
|
||||
}
|
||||
|
||||
private static List<FoundationColumn> foundationEnvelope(BoundingBox area, StructureStart start) {
|
||||
BoundingBox structure = start.getBoundingBox();
|
||||
int minX = Math.max(area.minX(), structure.minX());
|
||||
int minZ = Math.max(area.minZ(), structure.minZ());
|
||||
int maxX = Math.min(area.maxX(), structure.maxX());
|
||||
int maxZ = Math.min(area.maxZ(), structure.maxZ());
|
||||
if (minX > maxX || minZ > maxZ) {
|
||||
return List.of();
|
||||
}
|
||||
List<StructurePiece> pieces = start.getPieces();
|
||||
List<FoundationColumn> columns = new ArrayList<>((maxX - minX + 1) * (maxZ - minZ + 1));
|
||||
BitSet envelope = new BitSet(area.getYSpan());
|
||||
for (int z = minZ; z <= maxZ; z++) {
|
||||
for (int x = minX; x <= maxX; x++) {
|
||||
envelope.clear();
|
||||
markFoundationEnvelope(envelope, pieces, area, x, z);
|
||||
int cellCount = envelope.cardinality();
|
||||
if (cellCount == 0) {
|
||||
continue;
|
||||
}
|
||||
int[] ys = new int[cellCount];
|
||||
int cell = 0;
|
||||
for (int bit = envelope.nextSetBit(0); bit >= 0; bit = envelope.nextSetBit(bit + 1)) {
|
||||
ys[cell++] = area.minY() + bit;
|
||||
}
|
||||
columns.add(new FoundationColumn(x, z, ys));
|
||||
}
|
||||
}
|
||||
return List.copyOf(columns);
|
||||
}
|
||||
|
||||
private static void markFoundationEnvelope(BitSet envelope, List<StructurePiece> pieces, BoundingBox area,
|
||||
int x, int z) {
|
||||
for (StructurePiece piece : pieces) {
|
||||
BoundingBox bounds = piece.getBoundingBox();
|
||||
if (x < bounds.minX() || x > bounds.maxX() || z < bounds.minZ() || z > bounds.maxZ()) {
|
||||
continue;
|
||||
}
|
||||
int groundY = bounds.minY();
|
||||
if (piece instanceof PoolElementStructurePiece poolPiece) {
|
||||
groundY += poolPiece.getGroundLevelDelta();
|
||||
if (groundY < bounds.minY()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
int minY = Math.max(area.minY(), bounds.minY());
|
||||
int maxY = Math.min(area.maxY(), Math.min(bounds.maxY(), groundY + FOUNDATION_VERTICAL_TOLERANCE));
|
||||
if (minY <= maxY) {
|
||||
envelope.set(minY - area.minY(), maxY - area.minY() + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void placeStilts(WorldGenLevel world, BoundingBox area, String structureId,
|
||||
StructureStart start, IrisVanillaStructureStiltSettings settings,
|
||||
StiltBlockResolver stiltBlockResolver) {
|
||||
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
|
||||
int structureHash = structureId == null ? 0 : structureId.hashCode();
|
||||
RNG rng = new RNG(world.getSeed() ^ structureHash);
|
||||
for (FoundationColumn column : foundationEnvelope(area, start)) {
|
||||
int foundationY = findFoundationY(world, column, position);
|
||||
if (foundationY == Integer.MIN_VALUE) {
|
||||
continue;
|
||||
}
|
||||
for (int depth = 0, y = foundationY - 1;
|
||||
depth < settings.getMaxDepth() && y >= area.minY(); depth++, y--) {
|
||||
position.set(column.x(), y, column.z());
|
||||
BlockState existingState = world.getBlockState(position);
|
||||
boolean vegetation = existingState.is(BlockTags.LOGS) || existingState.is(BlockTags.LEAVES);
|
||||
if (existingState.isSolid() && !vegetation) {
|
||||
break;
|
||||
}
|
||||
BlockState stilt = stiltBlockResolver.resolve(settings, rng, column.x(), y, column.z());
|
||||
world.setBlock(position, stilt == null ? Blocks.COBBLESTONE.defaultBlockState() : stilt, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int findFoundationY(WorldGenLevel world, FoundationColumn column,
|
||||
BlockPos.MutableBlockPos position) {
|
||||
for (int cell = 0; cell < column.ys().length; cell++) {
|
||||
int y = column.ys()[cell];
|
||||
BlockState state = world.getBlockState(position.set(column.x(), y, column.z()));
|
||||
if (state.isSolid()) {
|
||||
return y;
|
||||
}
|
||||
}
|
||||
return Integer.MIN_VALUE;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface StiltBlockResolver {
|
||||
BlockState resolve(IrisVanillaStructureStiltSettings settings, RNG rng, int x, int y, int z);
|
||||
}
|
||||
|
||||
private record FoundationColumn(int x, int z, int[] ys) {
|
||||
}
|
||||
|
||||
public record VegetationTarget(StructureStart start, boolean force) {
|
||||
}
|
||||
|
||||
private record VegetationSnapshot(BitSet[] columns, int[] lowestY, int treeBlockCount) {
|
||||
}
|
||||
}
|
||||
+534
@@ -0,0 +1,534 @@
|
||||
/*
|
||||
* 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.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.QuartPos;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.Climate;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
final class IrisModdedBiomeSource extends BiomeSource {
|
||||
private static final int BIOME_CACHE_MAX = 262144;
|
||||
|
||||
private final BiomeSource serializedSource;
|
||||
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<>();
|
||||
private volatile IrisModdedChunkGenerator generator;
|
||||
private volatile Set<String> possibleStructureBiomeKeys;
|
||||
|
||||
IrisModdedBiomeSource(BiomeSource serializedSource) {
|
||||
this.serializedSource = serializedSource;
|
||||
}
|
||||
|
||||
void bind(IrisModdedChunkGenerator generator) {
|
||||
this.generator = generator;
|
||||
}
|
||||
|
||||
void clearCaches() {
|
||||
visibleBiomeCache.clear();
|
||||
structureBiomeCache.clear();
|
||||
surfaceStructureBiomeCache.clear();
|
||||
possibleStructureBiomeKeys = null;
|
||||
}
|
||||
|
||||
BiomeSource forStructureState(HolderLookup<StructureSet> structureSets) {
|
||||
LinkedHashSet<Holder<Biome>> possible = new LinkedHashSet<>();
|
||||
Registry<Biome> registry = biomeRegistry();
|
||||
if (registry == null) {
|
||||
throw new IllegalStateException("Iris cannot create structure state without the biome registry");
|
||||
}
|
||||
Set<String> generatedBiomeKeys = exactStructureBiomeKeys();
|
||||
Set<String> missingBiomeKeys = new LinkedHashSet<>(generatedBiomeKeys);
|
||||
missingBiomeKeys.removeAll(registeredBiomeKeys(registry));
|
||||
if (!missingBiomeKeys.isEmpty()) {
|
||||
throw new IllegalStateException("Iris structure biomes are not registered: " + missingBiomeKeys);
|
||||
}
|
||||
structureSets.listElements().forEach((Holder.Reference<StructureSet> structureSet) -> {
|
||||
for (StructureSet.StructureSelectionEntry entry : structureSet.value().structures()) {
|
||||
for (Holder<Biome> biome : entry.structure().value().biomes()) {
|
||||
String key = holderKey(biome);
|
||||
if (isGeneratedBiomeKey(key, generatedBiomeKeys)) {
|
||||
possible.add(biome);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return new StructureStateBiomeSource(this, Set.copyOf(possible));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<? extends BiomeSource> codec() {
|
||||
throw new UnsupportedOperationException("IrisModdedBiomeSource is serialized through IrisModdedChunkGenerator");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Stream<Holder<Biome>> collectPossibleBiomes() {
|
||||
Registry<Biome> registry = biomeRegistry();
|
||||
if (registry == null) {
|
||||
return serializedSource.possibleBiomes().stream();
|
||||
}
|
||||
Set<String> generatedBiomeKeys = possibleStructureBiomeKeys();
|
||||
LinkedHashSet<Holder<Biome>> possible = new LinkedHashSet<>();
|
||||
Holder<Biome> fallback = fallbackHolder(registry);
|
||||
registry.listElements().forEach((Holder.Reference<Biome> reference) -> {
|
||||
String key = holderKey(reference);
|
||||
if (isGeneratedBiomeKey(key, generatedBiomeKeys)) {
|
||||
possible.add(reference);
|
||||
}
|
||||
});
|
||||
if (possible.isEmpty() && fallback != null) {
|
||||
possible.add(fallback);
|
||||
}
|
||||
return possible.stream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Holder<Biome> getNoiseBiome(int quartX, int quartY, int quartZ, Climate.Sampler sampler) {
|
||||
Engine engine = engineOrNull();
|
||||
if (!isReady(engine)) {
|
||||
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
|
||||
}
|
||||
return getNoiseBiome(engine, quartX, quartY, quartZ, sampler);
|
||||
}
|
||||
|
||||
private Holder<Biome> getNoiseBiome(Engine engine, int quartX, int quartY, int quartZ,
|
||||
Climate.Sampler sampler) {
|
||||
if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) {
|
||||
return getSurfaceStructureBiome(engine, quartX, quartZ, sampler);
|
||||
}
|
||||
long key = packNoiseKey(quartX, quartY, quartZ);
|
||||
Holder<Biome> cached = structureBiomeCache.get(key);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
Holder<Biome> resolved = resolveStructureBiome(engine, quartX, quartY, quartZ, sampler);
|
||||
Holder<Biome> existing = structureBiomeCache.putIfAbsent(key, resolved);
|
||||
if (structureBiomeCache.size() > BIOME_CACHE_MAX) {
|
||||
structureBiomeCache.clear();
|
||||
}
|
||||
return existing == null ? resolved : existing;
|
||||
}
|
||||
|
||||
Holder<Biome> getVisibleNoiseBiome(int quartX, int quartY, int quartZ, Climate.Sampler sampler) {
|
||||
Engine engine = engineOrNull();
|
||||
if (!isReady(engine)) {
|
||||
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
|
||||
}
|
||||
long key = packNoiseKey(quartX, quartY, quartZ);
|
||||
Holder<Biome> cached = visibleBiomeCache.get(key);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
Holder<Biome> resolved = resolveVisibleBiome(engine, quartX, quartY, quartZ, sampler);
|
||||
Holder<Biome> existing = visibleBiomeCache.putIfAbsent(key, resolved);
|
||||
if (visibleBiomeCache.size() > BIOME_CACHE_MAX) {
|
||||
visibleBiomeCache.clear();
|
||||
}
|
||||
return existing == null ? resolved : existing;
|
||||
}
|
||||
|
||||
boolean isStructureReachable(Holder<Structure> structure) {
|
||||
Set<String> possible = possibleStructureBiomeKeys();
|
||||
for (Holder<Biome> biome : structure.value().biomes()) {
|
||||
String key = holderKey(biome);
|
||||
if (isGeneratedBiomeKey(key, possible)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Holder<Biome>> getBiomesWithin(int x, int y, int z, int radius, Climate.Sampler sampler) {
|
||||
int minQuartY = QuartPos.fromBlock(y - radius);
|
||||
Engine engine = engineOrNull();
|
||||
if (!isReady(engine)) {
|
||||
return super.getBiomesWithin(x, y, z, radius, sampler);
|
||||
}
|
||||
boolean monumentQuery = isMonumentSurfaceBiomeQuery(
|
||||
y, radius, engine.getMinHeight(), engine.getDimension().getFluidHeight());
|
||||
if (!monumentQuery && !isGuaranteedSurfaceBiome(minQuartY, engine.getMinHeight())) {
|
||||
return super.getBiomesWithin(x, y, z, radius, sampler);
|
||||
}
|
||||
int minQuartX = QuartPos.fromBlock(x - radius);
|
||||
int maxQuartX = QuartPos.fromBlock(x + radius);
|
||||
int minQuartZ = QuartPos.fromBlock(z - radius);
|
||||
int maxQuartZ = QuartPos.fromBlock(z + radius);
|
||||
int columns = (maxQuartX - minQuartX + 1) * (maxQuartZ - minQuartZ + 1);
|
||||
Set<Holder<Biome>> biomes = new HashSet<>(columns);
|
||||
for (int quartZ = minQuartZ; quartZ <= maxQuartZ; quartZ++) {
|
||||
for (int quartX = minQuartX; quartX <= maxQuartX; quartX++) {
|
||||
biomes.add(getSurfaceStructureBiome(engine, quartX, quartZ, sampler));
|
||||
}
|
||||
}
|
||||
return biomes;
|
||||
}
|
||||
|
||||
private Holder<Biome> getSurfaceStructureBiome(Engine engine, int quartX, int quartZ,
|
||||
Climate.Sampler sampler) {
|
||||
long key = packColumnKey(quartX, quartZ);
|
||||
Holder<Biome> cached = surfaceStructureBiomeCache.get(key);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
Holder<Biome> resolved = resolveSurfaceStructureBiome(engine, quartX, quartZ, sampler);
|
||||
Holder<Biome> existing = surfaceStructureBiomeCache.putIfAbsent(key, resolved);
|
||||
if (surfaceStructureBiomeCache.size() > BIOME_CACHE_MAX) {
|
||||
surfaceStructureBiomeCache.clear();
|
||||
}
|
||||
return existing == null ? resolved : existing;
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveSurfaceStructureBiome(Engine engine, int quartX, int quartZ,
|
||||
Climate.Sampler sampler) {
|
||||
Registry<Biome> registry = biomeRegistry();
|
||||
if (!isReady(engine) || registry == null) {
|
||||
return serializedSource.getNoiseBiome(quartX, 0, quartZ, sampler);
|
||||
}
|
||||
IrisBiome irisBiome = engine.getComplex().getTrueBiomeStream().get(quartX << 2, quartZ << 2);
|
||||
Holder<Biome> resolved = irisBiome == null ? null : resolveHolder(registry, irisBiome.getVanillaDerivativeKey());
|
||||
return resolved == null ? serializedSource.getNoiseBiome(quartX, 0, quartZ, sampler) : resolved;
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveStructureBiome(Engine engine, int quartX, int quartY, int quartZ,
|
||||
Climate.Sampler sampler) {
|
||||
Registry<Biome> registry = biomeRegistry();
|
||||
BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ);
|
||||
if (resolution == null || registry == null) {
|
||||
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
|
||||
}
|
||||
Holder<Biome> resolved = resolveHolder(registry, resolution.irisBiome().getVanillaDerivativeKey());
|
||||
if (resolved == null && resolution.irisBiome().isCustom()) {
|
||||
IrisBiomeCustom customBiome = resolution.irisBiome().getCustomBiome(
|
||||
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
|
||||
if (customBiome != null) {
|
||||
resolved = resolveHolder(registry, engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT)
|
||||
+ ":" + customBiome.getId().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
return resolved == null ? fallbackBiome(registry, quartX, quartY, quartZ, sampler) : resolved;
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveVisibleBiome(Engine engine, int quartX, int quartY, int quartZ,
|
||||
Climate.Sampler sampler) {
|
||||
Registry<Biome> registry = biomeRegistry();
|
||||
BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ);
|
||||
if (resolution == null || registry == null) {
|
||||
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
|
||||
}
|
||||
String biomeKey;
|
||||
if (resolution.irisBiome().isCustom()) {
|
||||
IrisBiomeCustom customBiome = resolution.irisBiome().getCustomBiome(
|
||||
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
|
||||
if (customBiome == null) {
|
||||
return fallbackBiome(registry, quartX, quartY, quartZ, sampler);
|
||||
}
|
||||
biomeKey = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT)
|
||||
+ ":" + customBiome.getId().toLowerCase(Locale.ROOT);
|
||||
} else if (resolution.underground()) {
|
||||
biomeKey = resolution.irisBiome().getGroundBiomeKey(
|
||||
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
|
||||
} else {
|
||||
biomeKey = resolution.irisBiome().getSkyBiomeKey(
|
||||
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
|
||||
}
|
||||
Holder<Biome> resolved = resolveHolder(registry, biomeKey);
|
||||
return resolved == null
|
||||
? fallbackBiome(registry, quartX, quartY, quartZ, sampler)
|
||||
: resolved;
|
||||
}
|
||||
|
||||
private BiomeResolution resolveBiomeResolution(Engine engine, int quartX, int quartY, int quartZ) {
|
||||
if (!isReady(engine)) {
|
||||
return null;
|
||||
}
|
||||
int blockX = quartX << 2;
|
||||
int blockY = quartY << 2;
|
||||
int blockZ = quartZ << 2;
|
||||
int internalY = blockY - engine.getMinHeight();
|
||||
int caveSwitchY = Math.max(-8 - engine.getMinHeight(), 40);
|
||||
boolean underground = false;
|
||||
IrisBiome irisBiome;
|
||||
if (internalY <= caveSwitchY) {
|
||||
int surfaceY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
|
||||
underground = isUnderground(internalY, surfaceY);
|
||||
irisBiome = underground
|
||||
? engine.getCaveBiome(blockX, internalY, blockZ)
|
||||
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
} else {
|
||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
}
|
||||
if (irisBiome == null && underground) {
|
||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
}
|
||||
if (irisBiome == null) {
|
||||
return null;
|
||||
}
|
||||
IrisModdedChunkGenerator current = generator;
|
||||
long worldSeed = current == null ? engine.getWorld().getRawWorldSeed() : current.visibleBiomeSeed();
|
||||
long seed = biomeResolutionSeed(worldSeed, blockX, blockY, blockZ);
|
||||
return new BiomeResolution(irisBiome, underground, blockX, blockY, blockZ, new RNG(seed));
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveRequiredStructureBiome(int quartX, int quartY, int quartZ) {
|
||||
IrisModdedChunkGenerator current = generator;
|
||||
if (current == null) {
|
||||
throw new IllegalStateException("Iris structure biome source is not bound to its generator");
|
||||
}
|
||||
Engine engine = current.awaitStructureEngine();
|
||||
Registry<Biome> registry = biomeRegistry();
|
||||
if (registry == null) {
|
||||
throw new IllegalStateException("Iris structure biome lookup has no biome registry");
|
||||
}
|
||||
IrisBiome irisBiome;
|
||||
if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) {
|
||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(quartX << 2, quartZ << 2);
|
||||
} else {
|
||||
BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ);
|
||||
irisBiome = resolution == null ? null : resolution.irisBiome();
|
||||
}
|
||||
if (irisBiome == null) {
|
||||
throw new IllegalStateException("Iris returned no structure biome at quart "
|
||||
+ quartX + "," + quartY + "," + quartZ);
|
||||
}
|
||||
String derivativeKey = irisBiome.getVanillaDerivativeKey();
|
||||
Holder<Biome> resolved = resolveHolder(registry, derivativeKey);
|
||||
if (resolved == null) {
|
||||
throw new IllegalStateException("Iris structure biome derivative '" + derivativeKey
|
||||
+ "' is not registered at quart " + quartX + "," + quartY + "," + quartZ);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
static long biomeResolutionSeed(long worldSeed, int blockX, int blockY, int blockZ) {
|
||||
return worldSeed
|
||||
^ ((long) blockX * 341873128712L)
|
||||
^ ((long) blockY * 132897987541L)
|
||||
^ ((long) blockZ * 42317861L);
|
||||
}
|
||||
|
||||
static boolean isGeneratedBiomeKey(String key, Set<String> generatedBiomeKeys) {
|
||||
return key != null && generatedBiomeKeys.contains(key.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
static boolean isGuaranteedSurfaceBiome(int quartY, int minHeight) {
|
||||
int internalY = (quartY << 2) - minHeight;
|
||||
int caveSwitchY = Math.max(-8 - minHeight, 40);
|
||||
return internalY > caveSwitchY;
|
||||
}
|
||||
|
||||
static boolean isUnderground(int internalY, int surfaceY) {
|
||||
return internalY <= surfaceY - 8;
|
||||
}
|
||||
|
||||
static boolean isMonumentSurfaceBiomeQuery(int blockY, int radius, int minHeight, int fluidHeight) {
|
||||
return radius == 29 && blockY == minHeight + fluidHeight;
|
||||
}
|
||||
|
||||
private static boolean isReady(Engine engine) {
|
||||
return engine != null && !engine.isClosed() && engine.getComplex() != null;
|
||||
}
|
||||
|
||||
private Engine engineOrNull() {
|
||||
IrisModdedChunkGenerator current = generator;
|
||||
return current == null ? null : current.structureEngineOrNull();
|
||||
}
|
||||
|
||||
private Set<String> possibleStructureBiomeKeys() {
|
||||
Set<String> cached = possibleStructureBiomeKeys;
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
LinkedHashSet<String> possible = new LinkedHashSet<>(exactStructureBiomeKeys());
|
||||
Registry<Biome> registry = biomeRegistry();
|
||||
if (registry != null) {
|
||||
Set<String> registered = registeredBiomeKeys(registry);
|
||||
Holder<Biome> fallback = fallbackHolder(registry);
|
||||
String fallbackKey = fallback == null ? null : holderKey(fallback);
|
||||
if (fallbackKey != null && requiresPossibleBiomeFallback(possible, registered)) {
|
||||
possible.add(fallbackKey);
|
||||
}
|
||||
}
|
||||
Set<String> resolved = Set.copyOf(possible);
|
||||
possibleStructureBiomeKeys = resolved;
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private Set<String> exactStructureBiomeKeys() {
|
||||
LinkedHashSet<String> possible = new LinkedHashSet<>();
|
||||
Engine engine = engineOrNull();
|
||||
if (isReady(engine)) {
|
||||
String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
|
||||
for (IrisBiome irisBiome : engine.getAllBiomes()) {
|
||||
String derivative = normalizeKey(irisBiome.getVanillaDerivativeKey());
|
||||
if (derivative != null) {
|
||||
possible.add(derivative);
|
||||
}
|
||||
if (!irisBiome.isCustom()) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
possible.add(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
}
|
||||
IrisModdedChunkGenerator current = generator;
|
||||
if (current != null) {
|
||||
possible.addAll(current.configuredStructureBiomeKeys());
|
||||
}
|
||||
return Set.copyOf(possible);
|
||||
}
|
||||
|
||||
static boolean requiresPossibleBiomeFallback(Set<String> configured, Set<String> registered) {
|
||||
return configured.isEmpty() || !registered.containsAll(configured);
|
||||
}
|
||||
|
||||
private static Set<String> registeredBiomeKeys(Registry<Biome> registry) {
|
||||
Set<String> registered = new HashSet<>();
|
||||
registry.listElements().forEach((Holder.Reference<Biome> reference) -> {
|
||||
String key = holderKey(reference);
|
||||
if (key != null) {
|
||||
registered.add(key);
|
||||
}
|
||||
});
|
||||
return registered;
|
||||
}
|
||||
|
||||
private Registry<Biome> biomeRegistry() {
|
||||
MinecraftServer server = ModdedEngineBootstrap.currentServer();
|
||||
return server == null ? null : server.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
}
|
||||
|
||||
private static Holder<Biome> resolveHolder(Registry<Biome> registry, String key) {
|
||||
if (key == null || key.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
Identifier identifier = Identifier.tryParse(key);
|
||||
if (identifier == null) {
|
||||
return null;
|
||||
}
|
||||
return registry.get(identifier).<Holder<Biome>>map((Holder.Reference<Biome> reference) -> reference).orElse(null);
|
||||
}
|
||||
|
||||
private static String holderKey(Holder<Biome> holder) {
|
||||
return holder.unwrapKey()
|
||||
.map((ResourceKey<Biome> key) -> key.identifier().toString().toLowerCase(Locale.ROOT))
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private static String normalizeKey(String key) {
|
||||
Identifier identifier = key == null ? null : Identifier.tryParse(key);
|
||||
return identifier == null ? null : identifier.toString().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private static Holder<Biome> fallbackHolder(Registry<Biome> registry) {
|
||||
if (registry == null) {
|
||||
return null;
|
||||
}
|
||||
Holder<Biome> plains = resolveHolder(registry, "minecraft:plains");
|
||||
if (plains != null) {
|
||||
return plains;
|
||||
}
|
||||
return registry.listElements().findFirst().<Holder<Biome>>map(
|
||||
(Holder.Reference<Biome> reference) -> reference).orElse(null);
|
||||
}
|
||||
|
||||
private Holder<Biome> fallbackBiome(Registry<Biome> registry, int quartX, int quartY, int quartZ,
|
||||
Climate.Sampler sampler) {
|
||||
Holder<Biome> plains = resolveHolder(registry, "minecraft:plains");
|
||||
return plains == null ? serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler) : plains;
|
||||
}
|
||||
|
||||
private static long packNoiseKey(int x, int y, int z) {
|
||||
return (((long) x & 67108863L) << 38)
|
||||
| (((long) z & 67108863L) << 12)
|
||||
| ((long) y & 4095L);
|
||||
}
|
||||
|
||||
private static long packColumnKey(int x, int z) {
|
||||
return ((long) x << 32) ^ ((long) z & 4294967295L);
|
||||
}
|
||||
|
||||
private record BiomeResolution(IrisBiome irisBiome, boolean underground, int blockX, int blockY,
|
||||
int blockZ, RNG rng) {
|
||||
}
|
||||
|
||||
private static final class StructureStateBiomeSource extends BiomeSource {
|
||||
private final IrisModdedBiomeSource delegate;
|
||||
private final Set<Holder<Biome>> possibleBiomes;
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> resolvedBiomes = new ConcurrentHashMap<>();
|
||||
|
||||
private StructureStateBiomeSource(IrisModdedBiomeSource delegate, Set<Holder<Biome>> possibleBiomes) {
|
||||
this.delegate = delegate;
|
||||
this.possibleBiomes = possibleBiomes;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<? extends BiomeSource> codec() {
|
||||
throw new UnsupportedOperationException("Structure state biome sources are not serializable");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Stream<Holder<Biome>> collectPossibleBiomes() {
|
||||
return possibleBiomes.stream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
||||
long key = packNoiseKey(x, y, z);
|
||||
Holder<Biome> cached = resolvedBiomes.get(key);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
Holder<Biome> resolved = delegate.resolveRequiredStructureBiome(x, y, z);
|
||||
Holder<Biome> existing = resolvedBiomes.putIfAbsent(key, resolved);
|
||||
if (resolvedBiomes.size() > BIOME_CACHE_MAX) {
|
||||
resolvedBiomes.clear();
|
||||
}
|
||||
return existing == null ? resolved : existing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Holder<Biome>> getBiomesWithin(int x, int y, int z, int radius, Climate.Sampler sampler) {
|
||||
return super.getBiomesWithin(x, y, z, radius, sampler);
|
||||
}
|
||||
}
|
||||
}
|
||||
+658
-107
@@ -18,22 +18,33 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.engine.object.IrisImportedStructureControl;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.engine.object.IrisVanillaStructureStiltSettings;
|
||||
import art.arcane.iris.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.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;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.QuartPos;
|
||||
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;
|
||||
@@ -52,40 +63,59 @@ import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeManager;
|
||||
import net.minecraft.world.level.biome.BiomeResolver;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.Climate;
|
||||
import net.minecraft.world.level.biome.MobSpawnSettings;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.EntityBlock;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
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.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;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
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.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.biomeSource),
|
||||
BiomeSource.CODEC.fieldOf("biome_source").forGetter((IrisModdedChunkGenerator generator) -> generator.serializedBiomeSource),
|
||||
Codec.STRING.fieldOf("dimension").forGetter((IrisModdedChunkGenerator generator) -> generator.dimensionKey)
|
||||
).apply(instance, IrisModdedChunkGenerator::new));
|
||||
|
||||
@@ -141,10 +171,12 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
private final String dimensionKey;
|
||||
private final String defaultPack;
|
||||
private final String defaultDimensionKey;
|
||||
private final ConcurrentHashMap<String, Holder<Biome>> biomeHolders = new ConcurrentHashMap<>();
|
||||
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 Set<String> missingBiomeWarnings = ConcurrentHashMap.newKeySet();
|
||||
private final ConcurrentHashMap<NativeStructureStartKey, Integer> worldCheckStructureShifts = new ConcurrentHashMap<>();
|
||||
private final AtomicBoolean announced = new AtomicBoolean(false);
|
||||
private volatile boolean vanillaSpawnBiomesInitialized;
|
||||
private volatile Engine engine;
|
||||
@@ -152,10 +184,20 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
private volatile String activeDimensionKey;
|
||||
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;
|
||||
|
||||
public IrisModdedChunkGenerator(BiomeSource biomeSource, String dimensionKey) {
|
||||
super(biomeSource);
|
||||
this(biomeSource, dimensionKey, new IrisModdedBiomeSource(biomeSource));
|
||||
}
|
||||
|
||||
private IrisModdedChunkGenerator(BiomeSource serializedBiomeSource, String dimensionKey, IrisModdedBiomeSource structureBiomeSource) {
|
||||
super(structureBiomeSource);
|
||||
this.dimensionKey = dimensionKey;
|
||||
this.serializedBiomeSource = serializedBiomeSource;
|
||||
this.structureBiomeSource = structureBiomeSource;
|
||||
this.structureBiomeSource.bind(this);
|
||||
int colon = dimensionKey.indexOf(':');
|
||||
this.defaultPack = colon >= 0 ? dimensionKey.substring(0, colon) : dimensionKey;
|
||||
this.defaultDimensionKey = colon >= 0 ? dimensionKey.substring(colon + 1) : dimensionKey;
|
||||
@@ -172,9 +214,12 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
this.activeDimensionKey = packDimensionKey;
|
||||
this.seedOverride = seed;
|
||||
this.engine = null;
|
||||
this.configuredStructureBiomeKeys = null;
|
||||
this.configuredPack = null;
|
||||
this.engineBinding.reset();
|
||||
this.announced.set(false);
|
||||
this.biomeHolders.clear();
|
||||
this.missingBiomeWarnings.clear();
|
||||
this.structureBiomeSource.clearCaches();
|
||||
this.worldCheckStructureShifts.clear();
|
||||
resetVanillaSpawnBiomes();
|
||||
}
|
||||
|
||||
@@ -184,9 +229,12 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
ModdedWorldEngines.evict(level);
|
||||
}
|
||||
this.engine = null;
|
||||
this.configuredStructureBiomeKeys = null;
|
||||
this.configuredPack = null;
|
||||
this.engineBinding.reset();
|
||||
this.announced.set(false);
|
||||
this.biomeHolders.clear();
|
||||
this.missingBiomeWarnings.clear();
|
||||
this.structureBiomeSource.clearCaches();
|
||||
this.worldCheckStructureShifts.clear();
|
||||
resetVanillaSpawnBiomes();
|
||||
}
|
||||
|
||||
@@ -207,6 +255,107 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
return CODEC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> structureSets, RandomState randomState, long seed) {
|
||||
return ChunkGeneratorStructureState.createForNormal(
|
||||
randomState, seed, structureBiomeSource.forStructureState(structureSets), structureSets);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pair<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel level, HolderSet<Structure> holders,
|
||||
BlockPos pos, int radius,
|
||||
boolean findUnexplored) {
|
||||
Engine current = engineOrNull();
|
||||
if (current == null) {
|
||||
return super.findNearestMapStructure(level, holders, pos, radius, findUnexplored);
|
||||
}
|
||||
try {
|
||||
Pair<BlockPos, Holder<Structure>> irisPlaced = findNearestIrisStructure(
|
||||
level, holders, pos, Math.max(1, radius), current);
|
||||
if (irisPlaced != null) {
|
||||
return irisPlaced;
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris-placed structure locate failed near {},{}", pos.getX(), pos.getZ(), e);
|
||||
}
|
||||
IrisImportedStructureControl control = current.getDimension().getImportedStructures();
|
||||
if (control == null || !control.active()) {
|
||||
return null;
|
||||
}
|
||||
HolderSet<Structure> reachable = filterReachableNativeStructures(level, holders, current, control);
|
||||
if (reachable.size() == 0) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return super.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Native structure locate failed near {},{}", pos.getX(), pos.getZ(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isNativeStructureReachable(Holder<Structure> structure) {
|
||||
return structure != null && structureBiomeSource.isStructureReachable(structure);
|
||||
}
|
||||
|
||||
private Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(ServerLevel level,
|
||||
HolderSet<Structure> holders,
|
||||
BlockPos pos, int radius,
|
||||
Engine current) {
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
IrisStructureLocator.LocateResult result = IrisStructureLocator.locate(
|
||||
current, id.toString(), pos.getX(), pos.getZ(), radius);
|
||||
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
|
||||
continue;
|
||||
}
|
||||
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,
|
||||
IrisImportedStructureControl control) {
|
||||
try {
|
||||
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
List<Holder<Structure>> kept = new ArrayList<>(holders.size());
|
||||
for (Holder<Structure> holder : holders) {
|
||||
Identifier id = registry.getKey(holder.value());
|
||||
String key = id == null ? null : id.toString();
|
||||
IrisNativeStructureDecision decision = control.resolve(
|
||||
key, NativeStructurePostProcessor.isUndergroundStep(holder.value().step()));
|
||||
if (key == null || !decision.generate()
|
||||
|| IrisStructureLocator.suppressesVanilla(current, key)
|
||||
|| !structureBiomeSource.isStructureReachable(holder)) {
|
||||
continue;
|
||||
}
|
||||
kept.add(holder);
|
||||
}
|
||||
return kept.size() == holders.size() ? holders : HolderSet.direct(kept);
|
||||
} catch (Throwable error) {
|
||||
LOGGER.error("Iris could not resolve native structure biome reachability; native locate failed closed", error);
|
||||
return HolderSet.direct(List.of());
|
||||
}
|
||||
}
|
||||
|
||||
private ServerLevel boundLevel() {
|
||||
MinecraftServer server = ModdedEngineBootstrap.currentServer();
|
||||
if (server == null) {
|
||||
@@ -225,17 +374,53 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
ServerLevel level = boundLevel();
|
||||
if (level == null) {
|
||||
throw new IllegalStateException("Iris generator '" + dimensionKey + "' has no bound ServerLevel yet");
|
||||
}
|
||||
return bindEngine(level);
|
||||
}
|
||||
|
||||
void bindLevel(ServerLevel level) {
|
||||
if (level.getChunkSource().getGenerator() != this) {
|
||||
throw new IllegalArgumentException("ServerLevel does not use Iris generator '" + dimensionKey + "'");
|
||||
}
|
||||
bindEngine(level);
|
||||
}
|
||||
|
||||
private Engine bindEngine(ServerLevel level) {
|
||||
Engine cached = engine;
|
||||
if (cached != null && !cached.isClosed() && cached.getComplex() != null) {
|
||||
engineBinding.complete(cached);
|
||||
return cached;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (engine != null) {
|
||||
return engine;
|
||||
Engine existing = engine;
|
||||
if (existing != null && !existing.isClosed() && existing.getComplex() != null) {
|
||||
engineBinding.complete(existing);
|
||||
return existing;
|
||||
}
|
||||
ServerLevel level = boundLevel();
|
||||
if (level == null) {
|
||||
throw new IllegalStateException("Iris generator '" + dimensionKey + "' has no bound ServerLevel yet");
|
||||
try {
|
||||
Engine created = ModdedWorldEngines.get(level, activePack, activeDimensionKey, seedOverride);
|
||||
if (created.isClosed() || created.getComplex() == null) {
|
||||
throw new IllegalStateException("Iris generator '" + dimensionKey
|
||||
+ "' created an engine without a ready biome complex");
|
||||
}
|
||||
engine = created;
|
||||
configuredStructureBiomeKeys = null;
|
||||
structureBiomeSource.clearCaches();
|
||||
engineBinding.complete(created);
|
||||
return created;
|
||||
} catch (Throwable error) {
|
||||
engineBinding.fail(error);
|
||||
if (error instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
if (error instanceof Error fatalError) {
|
||||
throw fatalError;
|
||||
}
|
||||
throw new IllegalStateException("Iris generator '" + dimensionKey + "' failed to bind", error);
|
||||
}
|
||||
Engine created = ModdedWorldEngines.get(level, activePack, activeDimensionKey, seedOverride);
|
||||
engine = created;
|
||||
return created;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,6 +436,141 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
Engine structureEngineOrNull() {
|
||||
return engineOrNull();
|
||||
}
|
||||
|
||||
Engine awaitStructureEngine() {
|
||||
Engine current = engine;
|
||||
if (current != null && !current.isClosed() && current.getComplex() != null) {
|
||||
return current;
|
||||
}
|
||||
Engine bound = engineBinding.await(dimensionKey);
|
||||
if (bound.isClosed() || bound.getComplex() == null) {
|
||||
throw new IllegalStateException("Iris generator '" + dimensionKey
|
||||
+ "' completed bootstrap without a ready biome complex");
|
||||
}
|
||||
return bound;
|
||||
}
|
||||
|
||||
private Engine requireDataQueryEngine(String operation) {
|
||||
Engine current = engine;
|
||||
if (current != null && !current.isClosed() && current.getComplex() != null) {
|
||||
return current;
|
||||
}
|
||||
ServerLevel level = boundLevel();
|
||||
if (level != null) {
|
||||
return bindEngine(level);
|
||||
}
|
||||
MinecraftServer server = ModdedEngineBootstrap.currentServer();
|
||||
if (server == null) {
|
||||
throw new IllegalStateException("Iris generator '" + dimensionKey + "' cannot answer "
|
||||
+ operation + " without an active server");
|
||||
}
|
||||
if (server.isSameThread()) {
|
||||
throw new IllegalStateException("Iris generator '" + dimensionKey + "' cannot answer "
|
||||
+ operation + " on the server thread before its ServerLevel is bound");
|
||||
}
|
||||
return awaitStructureEngine();
|
||||
}
|
||||
|
||||
long visibleBiomeSeed() {
|
||||
long configuredSeed = seedOverride;
|
||||
if (configuredSeed != Long.MIN_VALUE) {
|
||||
return configuredSeed;
|
||||
}
|
||||
ServerLevel level = boundLevel();
|
||||
if (level != null) {
|
||||
return level.getSeed();
|
||||
}
|
||||
Engine current = engine;
|
||||
return current == null ? 0L : current.getWorld().getRawWorldSeed();
|
||||
}
|
||||
|
||||
Set<String> configuredStructureBiomeKeys() {
|
||||
Set<String> cached = configuredStructureBiomeKeys;
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (configuredStructureBiomeKeys != null) {
|
||||
return configuredStructureBiomeKeys;
|
||||
}
|
||||
Engine current = engine;
|
||||
Iterable<IrisBiome> biomes;
|
||||
String namespace;
|
||||
if (current != null && !current.isClosed()) {
|
||||
biomes = current.getAllBiomes();
|
||||
namespace = current.getDimension().getLoadKey();
|
||||
} else {
|
||||
ConfiguredPack configured = configuredPack();
|
||||
Set<String> resolved = collectConfiguredBiomeKeys(configured.dimension(), configured.data());
|
||||
configuredStructureBiomeKeys = resolved;
|
||||
return resolved;
|
||||
}
|
||||
Set<String> resolved = collectConfiguredBiomeKeys(biomes, namespace);
|
||||
configuredStructureBiomeKeys = resolved;
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
private ConfiguredPack configuredPack() {
|
||||
ConfiguredPack cached = configuredPack;
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (configuredPack != null) {
|
||||
return configuredPack;
|
||||
}
|
||||
File packDirectory = ModdedWorldEngines.resolvePack(activePack, activeDimensionKey);
|
||||
IrisData data = IrisData.get(packDirectory);
|
||||
IrisDimension dimension = data.getDimensionLoader().load(activeDimensionKey);
|
||||
if (dimension == null) {
|
||||
throw new IllegalStateException("Iris dimension '" + activeDimensionKey
|
||||
+ "' missing from pack " + packDirectory.getAbsolutePath());
|
||||
}
|
||||
ConfiguredPack resolved = new ConfiguredPack(data, dimension, 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) {
|
||||
return collectConfiguredBiomeKeys(dimension.getReachableBiomes(() -> data), dimension.getLoadKey());
|
||||
}
|
||||
|
||||
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.getVanillaDerivativeKey());
|
||||
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;
|
||||
}
|
||||
@@ -268,8 +588,9 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
|
||||
public void onHotload() {
|
||||
biomeHolders.clear();
|
||||
missingBiomeWarnings.clear();
|
||||
configuredStructureBiomeKeys = null;
|
||||
structureBiomeSource.clearCaches();
|
||||
worldCheckStructureShifts.clear();
|
||||
resetVanillaSpawnBiomes();
|
||||
}
|
||||
|
||||
@@ -312,7 +633,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
|
||||
String namespace = current.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
|
||||
for (IrisBiome irisBiome : current.getDimension().getAllBiomes(current)) {
|
||||
for (IrisBiome irisBiome : current.getDimension().getReachableBiomes(current)) {
|
||||
if (irisBiome == null || !irisBiome.isCustom()) {
|
||||
continue;
|
||||
}
|
||||
@@ -348,6 +669,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
vanillaSpawnBiomesInitialized = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ChunkAccess> createBiomes(RandomState randomState, Blender blender,
|
||||
StructureManager structureManager, ChunkAccess chunk) {
|
||||
chunk.fillBiomesFromNoise(structureBiomeSource::getVisibleNoiseBiome, randomState.sampler());
|
||||
return CompletableFuture.completedFuture(chunk);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ChunkAccess> fillFromNoise(Blender blender, RandomState randomState, StructureManager structureManager, ChunkAccess chunk) {
|
||||
Engine generationEngine = engine();
|
||||
@@ -363,20 +691,18 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
int dimMaxY = generationEngine.getMaxHeight();
|
||||
int height = dimMaxY - dimMinY;
|
||||
PlatformBlockState air = IrisPlatforms.get().registries().air();
|
||||
Registry<Biome> biomeRegistry = structureManager.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
|
||||
if (PARALLEL_CHUNK_SYSTEM) {
|
||||
return CompletableFuture.completedFuture(
|
||||
generateTerrain(chunk, generationEngine, pos, dimMinY, height, air, biomeRegistry, randomState));
|
||||
generateTerrain(chunk, generationEngine, pos, dimMinY, height, air));
|
||||
}
|
||||
return CompletableFuture.supplyAsync(
|
||||
() -> generateTerrain(chunk, generationEngine, pos, dimMinY, height, air, biomeRegistry, randomState),
|
||||
() -> generateTerrain(chunk, generationEngine, pos, dimMinY, height, air),
|
||||
genPool);
|
||||
}
|
||||
|
||||
private ChunkAccess generateTerrain(ChunkAccess chunk, Engine generationEngine, ChunkPos pos,
|
||||
int dimMinY, int height, PlatformBlockState air,
|
||||
Registry<Biome> biomeRegistry, RandomState randomState) {
|
||||
int dimMinY, int height, PlatformBlockState air) {
|
||||
ModdedBlockBuffer blocks = new ModdedBlockBuffer(height, air);
|
||||
Hunk<PlatformBiome> biomes = Hunk.newArrayHunk(16, height, 16);
|
||||
try {
|
||||
@@ -394,50 +720,32 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
|
||||
writeBlocks(chunk, blocks, dimMinY, height);
|
||||
Heightmap.primeHeightmaps(chunk, EnumSet.of(Heightmap.Types.WORLD_SURFACE_WG, Heightmap.Types.OCEAN_FLOOR_WG));
|
||||
chunk.fillBiomesFromNoise(new HunkBiomeResolver(this, biomes, biomeRegistry, pos, dimMinY, height), randomState.sampler());
|
||||
writeTerrainHeightmaps(chunk, generationEngine, pos, height);
|
||||
Heightmap.primeHeightmaps(chunk, EnumSet.of(
|
||||
Heightmap.Types.MOTION_BLOCKING,
|
||||
Heightmap.Types.MOTION_BLOCKING_NO_LEAVES));
|
||||
ModdedWorldManager.enqueueGenerated(generationEngine, pos.x(), pos.z());
|
||||
return chunk;
|
||||
}
|
||||
|
||||
private Holder<Biome> fallbackBiome(Registry<Biome> registry) {
|
||||
Holder<Biome> existing = biomeHolders.get("minecraft:plains");
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
Holder<Biome> resolved = registry.get(Identifier.fromNamespaceAndPath("minecraft", "plains"))
|
||||
.<Holder<Biome>>map((Holder.Reference<Biome> reference) -> reference)
|
||||
.orElseThrow(() -> new IllegalStateException("minecraft:plains missing from biome registry"));
|
||||
Holder<Biome> raced = biomeHolders.putIfAbsent("minecraft:plains", resolved);
|
||||
return raced != null ? raced : resolved;
|
||||
private void writeTerrainHeightmaps(ChunkAccess chunk, Engine generationEngine, ChunkPos pos, int height) {
|
||||
int baseX = pos.getMinBlockX();
|
||||
int baseZ = pos.getMinBlockZ();
|
||||
writeTerrainHeightmap(chunk, Heightmap.Types.WORLD_SURFACE_WG, height,
|
||||
(x, z) -> generationEngine.getHeight(baseX + x, baseZ + z, false) + 1);
|
||||
writeTerrainHeightmap(chunk, Heightmap.Types.OCEAN_FLOOR_WG, height,
|
||||
(x, z) -> generationEngine.getHeight(baseX + x, baseZ + z, true) + 1);
|
||||
}
|
||||
|
||||
private Holder<Biome> holderFor(Registry<Biome> registry, PlatformBiome biome) {
|
||||
if (biome == null) {
|
||||
return fallbackBiome(registry);
|
||||
}
|
||||
String key = biome.key();
|
||||
Holder<Biome> existing = biomeHolders.get(key);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
Identifier identifier = Identifier.tryParse(key);
|
||||
Optional<Holder.Reference<Biome>> reference = identifier == null ? Optional.empty() : registry.get(identifier);
|
||||
Holder<Biome> resolved = reference.<Holder<Biome>>map((Holder.Reference<Biome> value) -> value).orElseGet(() -> {
|
||||
if (missingBiomeWarnings.size() < 256 && missingBiomeWarnings.add(key)) {
|
||||
LOGGER.warn("Iris biome '{}' (pack {}) is not in the biome registry; writing minecraft:plains. Restart so the forced datapack registers the pack biomes.", key, activePack);
|
||||
}
|
||||
return fallbackBiome(registry);
|
||||
});
|
||||
Holder<Biome> raced = biomeHolders.putIfAbsent(key, resolved);
|
||||
return raced != null ? raced : resolved;
|
||||
private void writeTerrainHeightmap(ChunkAccess chunk, Heightmap.Types type, int height,
|
||||
IntBinaryOperator heightResolver) {
|
||||
Heightmap heightmap = chunk.getOrCreateHeightmapUnprimed(type);
|
||||
heightmap.setRawData(chunk, type, ModdedHeightmaps.terrainRawData(height, heightResolver));
|
||||
}
|
||||
|
||||
public BiomeResolver regenBiomeResolver(Registry<Biome> registry, Hunk<PlatformBiome> biomes, ChunkPos pos) {
|
||||
Engine current = engine();
|
||||
int dimMinY = current.getMinHeight();
|
||||
int height = current.getMaxHeight() - dimMinY;
|
||||
return new HunkBiomeResolver(this, biomes, registry, pos, dimMinY, height);
|
||||
public BiomeResolver regenBiomeResolver() {
|
||||
engine();
|
||||
return structureBiomeSource::getVisibleNoiseBiome;
|
||||
}
|
||||
|
||||
private void writeBlocks(ChunkAccess chunk, ModdedBlockBuffer blocks, int dimMinY, int height) {
|
||||
@@ -445,6 +753,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
int chunkMaxY = chunkMinY + chunk.getHeight();
|
||||
int from = Math.max(dimMinY, chunkMinY);
|
||||
int to = Math.min(dimMinY + height, chunkMaxY);
|
||||
int baseX = chunk.getPos().getMinBlockX();
|
||||
int baseZ = chunk.getPos().getMinBlockZ();
|
||||
|
||||
for (int y = from; y < to; ) {
|
||||
int sectionIndex = chunk.getSectionIndex(y);
|
||||
@@ -462,7 +772,11 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
continue;
|
||||
}
|
||||
PlatformBlockState state = blocks.getRaw(x, bufferY, z);
|
||||
section.setBlockState(x, localY, z, (BlockState) state.nativeHandle(), false);
|
||||
BlockState blockState = (BlockState) state.nativeHandle();
|
||||
section.setBlockState(x, localY, z, blockState, false);
|
||||
if (blockState.hasBlockEntity()) {
|
||||
createDefaultBlockEntity(chunk, new BlockPos(baseX + x, blockY, baseZ + z), blockState);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -473,30 +787,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class HunkBiomeResolver implements BiomeResolver {
|
||||
private final IrisModdedChunkGenerator generator;
|
||||
private final Hunk<PlatformBiome> biomes;
|
||||
private final Registry<Biome> registry;
|
||||
private final ChunkPos pos;
|
||||
private final int dimMinY;
|
||||
private final int height;
|
||||
|
||||
private HunkBiomeResolver(IrisModdedChunkGenerator generator, Hunk<PlatformBiome> biomes, Registry<Biome> registry, ChunkPos pos, int dimMinY, int height) {
|
||||
this.generator = generator;
|
||||
this.biomes = biomes;
|
||||
this.registry = registry;
|
||||
this.pos = pos;
|
||||
this.dimMinY = dimMinY;
|
||||
this.height = height;
|
||||
static void createDefaultBlockEntity(ChunkAccess chunk, BlockPos position, BlockState state) {
|
||||
if (!(state.getBlock() instanceof EntityBlock entityBlock)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Holder<Biome> getNoiseBiome(int quartX, int quartY, int quartZ, Climate.Sampler sampler) {
|
||||
int localX = QuartPos.toBlock(quartX) - pos.getMinBlockX();
|
||||
int localZ = QuartPos.toBlock(quartZ) - pos.getMinBlockZ();
|
||||
int bufferY = Math.max(0, Math.min(height - 1, QuartPos.toBlock(quartY) - dimMinY));
|
||||
PlatformBiome biome = biomes.get(localX, bufferY, localZ);
|
||||
return generator.holderFor(registry, biome);
|
||||
BlockEntity blockEntity = entityBlock.newBlockEntity(position, state);
|
||||
if (blockEntity != null) {
|
||||
chunk.setBlockEntity(blockEntity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,14 +807,200 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
|
||||
@Override
|
||||
public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk, StructureManager structureManager) {
|
||||
if (importedControl().active()) {
|
||||
placeVanillaStructures(level, chunk, structureManager);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess chunk, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
|
||||
if (!importedControl().active()) {
|
||||
return;
|
||||
}
|
||||
Map<Structure, StructureStart> previousStarts = new HashMap<>(chunk.getAllStarts());
|
||||
super.createStructures(registryAccess, structureState, structureManager, chunk, templateManager, levelKey);
|
||||
adjustGeneratedStructures(registryAccess, chunk, previousStarts);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createReferences(WorldGenLevel level, StructureManager structureManager, ChunkAccess chunk) {
|
||||
super.createReferences(level, structureManager, chunk);
|
||||
}
|
||||
|
||||
private void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess chunk,
|
||||
Map<Structure, StructureStart> previousStarts) {
|
||||
Registry<Structure> registry = registryAccess.lookupOrThrow(Registries.STRUCTURE);
|
||||
IrisImportedStructureControl control = importedControl();
|
||||
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;
|
||||
}
|
||||
Identifier id = registry.getKey(structure);
|
||||
String structureId = id == null ? null : id.toString();
|
||||
IrisNativeStructureDecision decision = control.resolve(
|
||||
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
|
||||
if (!decision.generate() || IrisStructureLocator.suppressesVanilla(engine(), structureId)) {
|
||||
chunk.setStartForStructure(structure, StructureStart.INVALID_START);
|
||||
continue;
|
||||
}
|
||||
int offsetY;
|
||||
try {
|
||||
offsetY = NativeStructurePostProcessor.applyVerticalShift(
|
||||
start,
|
||||
decision.yShift(),
|
||||
chunk.getMinY(),
|
||||
chunk.getMinY() + chunk.getHeight());
|
||||
} catch (RuntimeException error) {
|
||||
chunk.setStartForStructure(structure, StructureStart.INVALID_START);
|
||||
LOGGER.error("Iris rejected native structure {} in chunk {},{} because its vertical bounds are invalid",
|
||||
structureId, chunk.getPos().x(), chunk.getPos().z(), error);
|
||||
continue;
|
||||
}
|
||||
recordWorldCheckStructureShift(structureId, start.getChunkPos(), offsetY);
|
||||
}
|
||||
}
|
||||
|
||||
private void placeVanillaStructures(WorldGenLevel world, ChunkAccess chunk, StructureManager structureManager) {
|
||||
if (!structureManager.shouldGenerateStructures()) {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
IrisImportedStructureControl control = importedControl();
|
||||
List<NativePlacementGroup> placementGroups = new ArrayList<>();
|
||||
List<NativeStructurePostProcessor.VegetationTarget> vegetationTargets = new ArrayList<>();
|
||||
for (int step = 0; step < steps; step++) {
|
||||
int index = 0;
|
||||
for (Structure structure : byStep.get(step)) {
|
||||
Identifier id = registry.getKey(structure);
|
||||
String structureId = id == null ? null : id.toString();
|
||||
IrisNativeStructureDecision decision = control.resolve(
|
||||
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
|
||||
if (decision.generate() && !IrisStructureLocator.suppressesVanilla(engine(), structureId)) {
|
||||
try {
|
||||
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
|
||||
if (!starts.isEmpty()) {
|
||||
List<StructureStart> resolvedStarts = List.copyOf(starts);
|
||||
placementGroups.add(new NativePlacementGroup(
|
||||
structureId, decision, index, step, resolvedStarts));
|
||||
for (StructureStart start : resolvedStarts) {
|
||||
vegetationTargets.add(new NativeStructurePostProcessor.VegetationTarget(
|
||||
start, decision.clearVegetation()));
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris failed to resolve native structure {} in chunk {},{}",
|
||||
structureId, chunkPos.x(), chunkPos.z(), e);
|
||||
}
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
try {
|
||||
NativeStructurePostProcessor.clearIntersectingVegetation(
|
||||
world, chunk, area, vegetationTargets);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris failed to clear vegetation from native structures in chunk {},{}",
|
||||
chunkPos.x(), chunkPos.z(), e);
|
||||
}
|
||||
for (NativePlacementGroup group : placementGroups) {
|
||||
random.setFeatureSeed(decorationSeed, group.featureIndex(), group.step());
|
||||
try {
|
||||
for (StructureStart start : group.starts()) {
|
||||
placeVanillaStructure(world, structureManager, random, area, chunkPos,
|
||||
group.structureId(), start, group.decision());
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris failed to place native structure {} in chunk {},{}",
|
||||
group.structureId(), chunkPos.x(), chunkPos.z(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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::resolveStiltBlock);
|
||||
}
|
||||
|
||||
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 resolveStiltBlock(IrisVanillaStructureStiltSettings settings, RNG rng,
|
||||
int x, int y, int z) {
|
||||
if (settings.getPalette() == null) {
|
||||
return Blocks.COBBLESTONE.defaultBlockState();
|
||||
}
|
||||
PlatformBlockState platformState = settings.getPalette().get(rng, x, y, z, engine().getData());
|
||||
if (platformState == null || !(platformState.nativeHandle() instanceof BlockState blockState)) {
|
||||
return Blocks.COBBLESTONE.defaultBlockState();
|
||||
}
|
||||
return blockState;
|
||||
}
|
||||
|
||||
private IrisImportedStructureControl importedControl() {
|
||||
return engine().getDimension().getImportedStructures();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -526,48 +1009,58 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
|
||||
@Override
|
||||
public int getGenDepth() {
|
||||
Engine current = engineOrNull();
|
||||
return current == null ? 384 : current.getMaxHeight() - current.getMinHeight();
|
||||
Engine current = engine;
|
||||
return current == null || current.isClosed()
|
||||
? configuredPack().metadata().depth()
|
||||
: current.getMaxHeight() - current.getMinHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSeaLevel() {
|
||||
Engine current = engineOrNull();
|
||||
return current == null ? 63 : current.getMinHeight() + current.getDimension().getFluidHeight();
|
||||
Engine current = engine;
|
||||
return current == null || current.isClosed()
|
||||
? configuredPack().metadata().seaLevel()
|
||||
: current.getMinHeight() + current.getDimension().getFluidHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinY() {
|
||||
Engine current = engineOrNull();
|
||||
return current == null ? -64 : current.getMinHeight();
|
||||
Engine current = engine;
|
||||
return current == null || current.isClosed()
|
||||
? configuredPack().metadata().minY()
|
||||
: current.getMinHeight();
|
||||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor heightAccessor, RandomState randomState) {
|
||||
Engine current = engineOrNull();
|
||||
if (current == null) {
|
||||
return heightAccessor.getMinY() + Math.min(heightAccessor.getHeight(), 64);
|
||||
}
|
||||
boolean ignoreFluid = type == Heightmap.Types.OCEAN_FLOOR || type == Heightmap.Types.OCEAN_FLOOR_WG;
|
||||
return current.getMinHeight() + current.getHeight(x, z, ignoreFluid) + 1;
|
||||
Engine current = requireDataQueryEngine("base height");
|
||||
boolean ignoreFluid = !type.isOpaque().test(Blocks.WATER.defaultBlockState());
|
||||
return heightAccessor.getMinY() + current.getHeight(x, z, ignoreFluid) + 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor heightAccessor, RandomState randomState) {
|
||||
int minY = heightAccessor.getMinY();
|
||||
BlockState[] states = new BlockState[heightAccessor.getHeight()];
|
||||
Engine current = engineOrNull();
|
||||
Engine current = requireDataQueryEngine("base column");
|
||||
BlockState airState = Blocks.AIR.defaultBlockState();
|
||||
if (current == null) {
|
||||
Arrays.fill(states, airState);
|
||||
return new NoiseColumn(minY, states);
|
||||
}
|
||||
int surface = current.getMinHeight() + current.getHeight(x, z, true);
|
||||
int fluid = current.getMinHeight() + current.getDimension().getFluidHeight();
|
||||
int surface = current.getHeight(x, z, true);
|
||||
int fluid = current.getHeight(x, z, false);
|
||||
BlockState stone = Blocks.STONE.defaultBlockState();
|
||||
BlockState water = Blocks.WATER.defaultBlockState();
|
||||
int solidEnd = Math.max(0, Math.min(states.length, surface - minY + 1));
|
||||
int fluidEnd = Math.max(solidEnd, Math.max(0, Math.min(states.length, fluid - minY + 1)));
|
||||
int solidEnd = Math.max(0, Math.min(states.length, surface + 1));
|
||||
int fluidEnd = Math.max(solidEnd, Math.max(0, Math.min(states.length, fluid + 1)));
|
||||
Arrays.fill(states, 0, solidEnd, stone);
|
||||
Arrays.fill(states, solidEnd, fluidEnd, water);
|
||||
Arrays.fill(states, fluidEnd, states.length, airState);
|
||||
@@ -581,4 +1074,62 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
|
||||
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 NativePlacementGroup(String structureId, IrisNativeStructureDecision decision,
|
||||
int featureIndex, int step, List<StructureStart> starts) {
|
||||
}
|
||||
|
||||
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 reset() {
|
||||
future = new CompletableFuture<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
* 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.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBlockData;
|
||||
import art.arcane.iris.engine.object.IrisBlockDrops;
|
||||
import art.arcane.iris.engine.object.IrisLoot;
|
||||
import art.arcane.iris.engine.object.IrisMarker;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.entity.EntityTypes;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class ModdedBlockBreakHandler {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final ConcurrentHashMap<BreakKey, PendingBreak> PENDING = new ConcurrentHashMap<>();
|
||||
|
||||
private ModdedBlockBreakHandler() {
|
||||
}
|
||||
|
||||
public static void prepare(ServerLevel level, BlockPos position, BlockState brokenState) {
|
||||
if (engineFor(level) == null) {
|
||||
return;
|
||||
}
|
||||
BreakKey key = new BreakKey(level, position.asLong());
|
||||
PendingBreak pending = new PendingBreak(brokenState);
|
||||
PENDING.put(key, pending);
|
||||
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
|
||||
if (scheduler != null) {
|
||||
scheduler.laterGlobal(() -> finishPending(key, pending, position), 1);
|
||||
}
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
PENDING.clear();
|
||||
}
|
||||
|
||||
public static Result complete(ServerLevel level, BlockPos position, BlockState fallbackState) {
|
||||
BreakKey key = new BreakKey(level, position.asLong());
|
||||
PendingBreak pending = PENDING.remove(key);
|
||||
BlockState brokenState = pending == null ? fallbackState : pending.brokenState();
|
||||
return evaluateSafely(level, position, brokenState);
|
||||
}
|
||||
|
||||
public static Result completePrepared(ServerLevel level, BlockPos position) {
|
||||
BreakKey key = new BreakKey(level, position.asLong());
|
||||
PendingBreak pending = PENDING.get(key);
|
||||
if (pending == null || level.getBlockState(position).equals(pending.brokenState())) {
|
||||
return null;
|
||||
}
|
||||
return PENDING.remove(key, pending) ? evaluateSafely(level, position, pending.brokenState()) : null;
|
||||
}
|
||||
|
||||
public static void spawn(ServerLevel level, BlockPos position, KList<ItemStack> drops) {
|
||||
for (ItemStack stack : drops) {
|
||||
ItemEntity item = createDrop(level, position, stack);
|
||||
if (item != null) {
|
||||
level.addFreshEntity(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static ItemEntity createDrop(ServerLevel level, BlockPos position, ItemStack stack) {
|
||||
if (stack == null || stack.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
RandomSource random = level.getRandom();
|
||||
double x = position.getX() + 0.5D + Mth.nextDouble(random, -0.25D, 0.25D);
|
||||
double y = position.getY() + 0.5D + Mth.nextDouble(random, -0.25D, 0.25D) - EntityTypes.ITEM.getHeight() / 2D;
|
||||
double z = position.getZ() + 0.5D + Mth.nextDouble(random, -0.25D, 0.25D);
|
||||
ItemEntity item = new ItemEntity(level, x, y, z, stack);
|
||||
item.setDefaultPickUpDelay();
|
||||
return item;
|
||||
}
|
||||
|
||||
private static void finishPending(BreakKey key, PendingBreak pending, BlockPos position) {
|
||||
if (!PENDING.remove(key, pending)) {
|
||||
return;
|
||||
}
|
||||
ServerLevel level = key.level();
|
||||
if (level.getBlockState(position).equals(pending.brokenState())) {
|
||||
return;
|
||||
}
|
||||
Result result = evaluateSafely(level, position, pending.brokenState());
|
||||
spawn(level, position, result.drops());
|
||||
}
|
||||
|
||||
private static Result evaluateSafely(ServerLevel level, BlockPos position, BlockState brokenState) {
|
||||
try {
|
||||
return evaluate(level, position, brokenState);
|
||||
} catch (Throwable error) {
|
||||
LOGGER.error("Iris block-break processing failed at {},{},{} in {}", position.getX(), position.getY(), position.getZ(),
|
||||
level.dimension().identifier(), error);
|
||||
return Result.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private static Result evaluate(ServerLevel level, BlockPos position, BlockState brokenState) {
|
||||
Engine engine = engineFor(level);
|
||||
if (engine == null || engine.isClosed()) {
|
||||
return Result.empty();
|
||||
}
|
||||
|
||||
removeMarker(engine, position);
|
||||
KList<IrisBlockDrops> providers = providers(engine, position, brokenState);
|
||||
if (providers.isEmpty()) {
|
||||
return Result.empty();
|
||||
}
|
||||
|
||||
KList<ItemStack> drops = new KList<>();
|
||||
boolean replaceVanillaDrops = false;
|
||||
for (IrisBlockDrops provider : providers) {
|
||||
replaceVanillaDrops |= provider.isReplaceVanillaDrops();
|
||||
for (IrisLoot loot : provider.getDrops()) {
|
||||
if (RNG.r.i(1, loot.getRarity()) != loot.getRarity()) {
|
||||
continue;
|
||||
}
|
||||
ItemStack stack = ModdedItemTranslator.stack(loot, RNG.r, level);
|
||||
if (stack != null && !stack.isEmpty()) {
|
||||
drops.add(stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new Result(drops, replaceVanillaDrops);
|
||||
}
|
||||
|
||||
private static void removeMarker(Engine engine, BlockPos position) {
|
||||
int mantleY = position.getY() - engine.getMinHeight();
|
||||
MatterMarker marker = engine.getMantle().getMantle().get(position.getX(), mantleY, position.getZ(), MatterMarker.class);
|
||||
if (marker == null) {
|
||||
return;
|
||||
}
|
||||
String tag = marker.getTag();
|
||||
if ("cave_floor".equals(tag) || "cave_ceiling".equals(tag)) {
|
||||
return;
|
||||
}
|
||||
IrisMarker configured = engine.getData().getMarkerLoader().load(tag);
|
||||
if (configured == null || configured.isRemoveOnChange()) {
|
||||
engine.getMantle().getMantle().remove(position.getX(), mantleY, position.getZ(), MatterMarker.class);
|
||||
}
|
||||
}
|
||||
|
||||
private static KList<IrisBlockDrops> providers(Engine engine, BlockPos position, BlockState brokenState) {
|
||||
KList<IrisBlockDrops> providers = new KList<>();
|
||||
IrisData data = engine.getData();
|
||||
int relativeY = position.getY() - engine.getMinHeight();
|
||||
IrisBiome biome = engine.getBiome(position.getX(), relativeY, position.getZ());
|
||||
if (biome != null) {
|
||||
addMatching(providers, biome.getBlockDrops(), brokenState, data);
|
||||
}
|
||||
if (skipsParents(providers)) {
|
||||
return providers;
|
||||
}
|
||||
IrisRegion region = engine.getRegion(position.getX(), position.getZ());
|
||||
if (region != null) {
|
||||
addMatching(providers, region.getBlockDrops(), brokenState, data);
|
||||
}
|
||||
addMatching(providers, engine.getDimension().getBlockDrops(), brokenState, data);
|
||||
return providers;
|
||||
}
|
||||
|
||||
private static void addMatching(KList<IrisBlockDrops> matches, KList<IrisBlockDrops> candidates, BlockState brokenState, IrisData data) {
|
||||
for (IrisBlockDrops candidate : candidates) {
|
||||
if (matches(candidate, brokenState, data)) {
|
||||
matches.add(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean skipsParents(KList<IrisBlockDrops> providers) {
|
||||
for (IrisBlockDrops provider : providers) {
|
||||
if (provider.isSkipParents()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean matches(IrisBlockDrops provider, BlockState brokenState, IrisData data) {
|
||||
for (IrisBlockData configuredBlock : provider.getBlocks()) {
|
||||
PlatformBlockState resolved = configuredBlock.getBlockData(data);
|
||||
if (resolved == null || !(resolved.nativeHandle() instanceof BlockState configuredState)) {
|
||||
continue;
|
||||
}
|
||||
if (matchesState(configuredState, brokenState, provider.isExactBlocks())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static boolean matchesState(BlockState configuredState, BlockState brokenState, boolean exact) {
|
||||
return exact ? configuredState.equals(brokenState) : configuredState.getBlock() == brokenState.getBlock();
|
||||
}
|
||||
|
||||
private static Engine engineFor(ServerLevel level) {
|
||||
ChunkGenerator generator = level.getChunkSource().getGenerator();
|
||||
if (!(generator instanceof IrisModdedChunkGenerator irisGenerator)) {
|
||||
return null;
|
||||
}
|
||||
Engine engine = irisGenerator.engineIfBound();
|
||||
if (engine != null) {
|
||||
return engine;
|
||||
}
|
||||
try {
|
||||
return irisGenerator.commandEngine();
|
||||
} catch (Throwable error) {
|
||||
LOGGER.error("Iris could not resolve the engine for a block break in {}", level.dimension().identifier(), error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public record Result(KList<ItemStack> drops, boolean replaceVanillaDrops) {
|
||||
private static Result empty() {
|
||||
return new Result(new KList<>(), false);
|
||||
}
|
||||
}
|
||||
|
||||
private record BreakKey(ServerLevel level, long position) {
|
||||
}
|
||||
|
||||
private record PendingBreak(BlockState brokenState) {
|
||||
}
|
||||
}
|
||||
+21
-14
@@ -20,6 +20,7 @@ package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
|
||||
import art.arcane.iris.modded.api.ModdedBlockData;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.data.UnresolvedKeyLog;
|
||||
import com.mojang.brigadier.StringReader;
|
||||
@@ -89,7 +90,7 @@ public final class ModdedBlockResolution {
|
||||
private ModdedBlockResolution() {
|
||||
}
|
||||
|
||||
record Parsed(BlockState state, Map<Property<?>, Comparable<?>> properties) {
|
||||
record Parsed(BlockState state, Map<Property<?>, Comparable<?>> properties, String deferredPlacementKey) {
|
||||
}
|
||||
|
||||
private static Set<Block> blockSet(String... ids) {
|
||||
@@ -171,12 +172,12 @@ public final class ModdedBlockResolution {
|
||||
|
||||
public static ModdedBlockState get(String bdxf) {
|
||||
Parsed parsed = resolveGet(bdxf);
|
||||
return ModdedBlockState.of(parsed.state(), parsed.properties());
|
||||
return stateFrom(parsed);
|
||||
}
|
||||
|
||||
public static ModdedBlockState getNoCompat(String bdxf) {
|
||||
Parsed parsed = resolveNoCompat(bdxf);
|
||||
return ModdedBlockState.of(parsed.state(), parsed.properties());
|
||||
return stateFrom(parsed);
|
||||
}
|
||||
|
||||
public static ModdedBlockState getOrNull(String bdxf) {
|
||||
@@ -185,7 +186,13 @@ public final class ModdedBlockResolution {
|
||||
|
||||
public static ModdedBlockState getOrNull(String bdxf, boolean warn) {
|
||||
Parsed parsed = resolveOrNull(bdxf, warn);
|
||||
return parsed == null ? null : ModdedBlockState.of(parsed.state(), parsed.properties());
|
||||
return parsed == null ? null : stateFrom(parsed);
|
||||
}
|
||||
|
||||
private static ModdedBlockState stateFrom(Parsed parsed) {
|
||||
return parsed.deferredPlacementKey() == null
|
||||
? ModdedBlockState.of(parsed.state(), parsed.properties())
|
||||
: ModdedBlockState.deferred(parsed.state(), parsed.properties(), parsed.deferredPlacementKey());
|
||||
}
|
||||
|
||||
static Parsed resolveGet(String bdxf) {
|
||||
@@ -194,7 +201,7 @@ public final class ModdedBlockResolution {
|
||||
return parsed;
|
||||
}
|
||||
IrisLogging.error("Can't find block data for " + bdxf);
|
||||
return new Parsed(AIR, null);
|
||||
return new Parsed(AIR, null, null);
|
||||
}
|
||||
|
||||
static Parsed resolveNoCompat(String bdxf) {
|
||||
@@ -202,7 +209,7 @@ public final class ModdedBlockResolution {
|
||||
if (parsed != null) {
|
||||
return parsed;
|
||||
}
|
||||
return new Parsed(AIR, null);
|
||||
return new Parsed(AIR, null, null);
|
||||
}
|
||||
|
||||
static Parsed resolveOrNull(String bdxf, boolean warn) {
|
||||
@@ -214,20 +221,20 @@ public final class ModdedBlockResolution {
|
||||
}
|
||||
|
||||
if (bd.equals("minecraft:grass_path")) {
|
||||
return new Parsed(Blocks.DIRT_PATH.defaultBlockState(), null);
|
||||
return new Parsed(Blocks.DIRT_PATH.defaultBlockState(), null, null);
|
||||
}
|
||||
|
||||
Parsed bdx = parseBlockData(bd, warn);
|
||||
|
||||
if (bdx == null) {
|
||||
BlockState provided = ModdedCustomContentRegistry.resolveBlock(bd);
|
||||
ModdedBlockData provided = ModdedCustomContentRegistry.resolveBlock(bd);
|
||||
if (provided != null) {
|
||||
return new Parsed(provided, null);
|
||||
return new Parsed(provided.state(), null, provided.deferredPlacement() ? bd : null);
|
||||
}
|
||||
if (warn) {
|
||||
warnUnresolved(bd, "Unknown Block Data '" + bd + "'");
|
||||
}
|
||||
return new Parsed(AIR, null);
|
||||
return new Parsed(AIR, null, null);
|
||||
}
|
||||
|
||||
return bdx;
|
||||
@@ -243,7 +250,7 @@ public final class ModdedBlockResolution {
|
||||
|
||||
public static ModdedBlockState strictParse(String key) {
|
||||
Parsed parsed = parseStrict(key);
|
||||
return ModdedBlockState.of(parsed.state(), parsed.properties());
|
||||
return stateFrom(parsed);
|
||||
}
|
||||
|
||||
private static Parsed parseStrict(String key) {
|
||||
@@ -257,7 +264,7 @@ public final class ModdedBlockResolution {
|
||||
if (reader.canRead()) {
|
||||
throw new IllegalArgumentException("Could not parse remainder: " + reader.getRemaining());
|
||||
}
|
||||
return new Parsed(result.blockState(), result.properties());
|
||||
return new Parsed(result.blockState(), result.properties(), null);
|
||||
}
|
||||
|
||||
private static Parsed createBlockData(String s, boolean warn) {
|
||||
@@ -283,7 +290,7 @@ public final class ModdedBlockResolution {
|
||||
if (identifier == null || !BuiltInRegistries.BLOCK.containsKey(identifier)) {
|
||||
return null;
|
||||
}
|
||||
return new Parsed(BuiltInRegistries.BLOCK.getValue(identifier).defaultBlockState(), null);
|
||||
return new Parsed(BuiltInRegistries.BLOCK.getValue(identifier).defaultBlockState(), null, null);
|
||||
}
|
||||
|
||||
private static Parsed parseBlockData(String ix, boolean warn) {
|
||||
@@ -307,7 +314,7 @@ public final class ModdedBlockResolution {
|
||||
|
||||
if (bx.state().getBlock() instanceof LeavesBlock) {
|
||||
BlockState mutated = bx.state().setValue(LeavesBlock.PERSISTENT, shouldPreventLeafDecay());
|
||||
bx = new Parsed(mutated, bx.properties());
|
||||
bx = new Parsed(mutated, bx.properties(), bx.deferredPlacementKey());
|
||||
}
|
||||
|
||||
return bx;
|
||||
|
||||
@@ -20,11 +20,13 @@ package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.Property;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -35,24 +37,31 @@ public final class ModdedBlockState implements PlatformBlockState {
|
||||
private final Map<Property<?>, Comparable<?>> parsedProperties;
|
||||
private final String key;
|
||||
private final String namespace;
|
||||
private final String deferredPlacementKey;
|
||||
private volatile Boolean air;
|
||||
private volatile Boolean solid;
|
||||
private volatile Boolean occluding;
|
||||
private volatile Boolean fluid;
|
||||
private volatile Boolean water;
|
||||
private volatile Boolean foliage;
|
||||
private volatile Boolean treeBlock;
|
||||
private volatile Boolean decorant;
|
||||
|
||||
private ModdedBlockState(BlockState state, Map<Property<?>, Comparable<?>> parsedProperties, String key) {
|
||||
private ModdedBlockState(BlockState state, Map<Property<?>, Comparable<?>> parsedProperties, String key, String deferredPlacementKey) {
|
||||
this.state = state;
|
||||
this.parsedProperties = parsedProperties;
|
||||
this.key = key;
|
||||
this.namespace = parseNamespace(key);
|
||||
this.deferredPlacementKey = deferredPlacementKey;
|
||||
}
|
||||
|
||||
public static ModdedBlockState of(BlockState state, Map<Property<?>, Comparable<?>> parsedProperties) {
|
||||
String key = serialize(state);
|
||||
return CACHE.computeIfAbsent(key, (String k) -> new ModdedBlockState(state, parsedProperties, k));
|
||||
return CACHE.computeIfAbsent(key, (String k) -> new ModdedBlockState(state, parsedProperties, k, null));
|
||||
}
|
||||
|
||||
public static ModdedBlockState deferred(BlockState state, Map<Property<?>, Comparable<?>> parsedProperties, String placementKey) {
|
||||
return new ModdedBlockState(state, parsedProperties, serialize(state), Objects.requireNonNull(placementKey));
|
||||
}
|
||||
|
||||
public static String serialize(BlockState state) {
|
||||
@@ -83,12 +92,12 @@ public final class ModdedBlockState implements PlatformBlockState {
|
||||
if (!(other instanceof ModdedBlockState blockState)) {
|
||||
return false;
|
||||
}
|
||||
return state.equals(blockState.state);
|
||||
return state.equals(blockState.state) && Objects.equals(deferredPlacementKey, blockState.deferredPlacementKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return key.hashCode();
|
||||
return Objects.hash(state, deferredPlacementKey);
|
||||
}
|
||||
|
||||
private static String parseNamespace(String key) {
|
||||
@@ -171,7 +180,17 @@ public final class ModdedBlockState implements PlatformBlockState {
|
||||
|
||||
@Override
|
||||
public boolean isCustom() {
|
||||
return false;
|
||||
return deferredPlacementKey != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String deferredPlacementKey() {
|
||||
return deferredPlacementKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlockState placementBaseState() {
|
||||
return deferredPlacementKey == null ? this : of(state, parsedProperties);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -219,6 +238,16 @@ public final class ModdedBlockState implements PlatformBlockState {
|
||||
return cached;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTreeBlock() {
|
||||
Boolean cached = treeBlock;
|
||||
if (cached == null) {
|
||||
cached = state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES);
|
||||
treeBlock = cached;
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFoliagePlantable() {
|
||||
return ModdedBlockResolution.isFoliagePlantable(state);
|
||||
@@ -298,7 +327,17 @@ public final class ModdedBlockState implements PlatformBlockState {
|
||||
@Override
|
||||
public PlatformBlockState withProperty(String name, String value) {
|
||||
String merged = mergeProperty(key, name, value);
|
||||
return ModdedBlockResolution.strictParse(merged);
|
||||
ModdedBlockState resolved = ModdedBlockResolution.strictParse(merged);
|
||||
return withHandle(resolved.handle(), resolved.parsedProperties());
|
||||
}
|
||||
|
||||
ModdedBlockState withHandle(BlockState updated, Map<Property<?>, Comparable<?>> properties) {
|
||||
if (updated == state && properties == parsedProperties) {
|
||||
return this;
|
||||
}
|
||||
return deferredPlacementKey == null
|
||||
? of(updated, properties)
|
||||
: deferred(updated, properties, deferredPlacementKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -24,9 +24,12 @@ import art.arcane.iris.engine.object.IrisLootTable;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.entity.Mob;
|
||||
import net.minecraft.world.entity.vehicle.ContainerEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
|
||||
@@ -37,81 +40,125 @@ public final class ModdedDeathLoot {
|
||||
private static final String TAG_PREFIX = "iris_loot|";
|
||||
private static final int LOOT_RNG_SALT = 345911;
|
||||
private static final Set<String> WARNED_TABLES = ConcurrentHashMap.newKeySet();
|
||||
private static final Set<String> WARNED_ENTITY_TYPES = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private ModdedDeathLoot() {
|
||||
}
|
||||
|
||||
public static void tag(LivingEntity entity, KList<String> tableKeys, int spawnX, int spawnY, int spawnZ, RNG rng) {
|
||||
if (entity == null || tableKeys == null || tableKeys.isEmpty()) {
|
||||
public static void bind(Engine engine, Entity entity, KList<String> tableKeys, int spawnX, int spawnY, int spawnZ, RNG rng) {
|
||||
if (engine == null || entity == null || tableKeys == null || tableKeys.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
entity.addTag(TAG_PREFIX + rng.getSeed() + "|" + spawnX + "|" + spawnY + "|" + spawnZ + "|" + String.join(",", tableKeys));
|
||||
LootBinding binding = new LootBinding(rng.getSeed(), spawnX, spawnY, spawnZ, new KList<>(tableKeys));
|
||||
if (entity instanceof Mob) {
|
||||
entity.addTag(encode(binding));
|
||||
return;
|
||||
}
|
||||
if (entity instanceof ContainerEntity container && entity.level() instanceof ServerLevel level) {
|
||||
fillContainer(engine, level, container, binding);
|
||||
return;
|
||||
}
|
||||
|
||||
String type = String.valueOf(BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()));
|
||||
if (WARNED_ENTITY_TYPES.add(type)) {
|
||||
IrisLogging.warn("Iris entity loot: entity type '" + type + "' does not expose a vanilla lootable path");
|
||||
}
|
||||
}
|
||||
|
||||
public static void handle(LivingEntity entity) {
|
||||
public static boolean replaceBaseLoot(LivingEntity entity) {
|
||||
if (entity == null || !(entity.level() instanceof ServerLevel level)) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
String encoded = findTag(entity);
|
||||
if (encoded == null) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
String[] parts = encoded.substring(TAG_PREFIX.length()).split("\\|", 5);
|
||||
if (parts.length < 5) {
|
||||
IrisLogging.debug("Iris death loot: malformed loot tag '" + encoded + "'");
|
||||
return;
|
||||
}
|
||||
long seed;
|
||||
int spawnX;
|
||||
int spawnY;
|
||||
int spawnZ;
|
||||
try {
|
||||
seed = Long.parseLong(parts[0]);
|
||||
spawnX = Integer.parseInt(parts[1]);
|
||||
spawnY = Integer.parseInt(parts[2]);
|
||||
spawnZ = Integer.parseInt(parts[3]);
|
||||
} catch (NumberFormatException error) {
|
||||
IrisLogging.debug("Iris death loot: malformed loot tag '" + encoded + "'");
|
||||
return;
|
||||
LootBinding binding = decode(encoded);
|
||||
if (binding == null) {
|
||||
return true;
|
||||
}
|
||||
Engine engine = engineFor(level);
|
||||
if (engine == null) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
RNG lootRng = new RNG(seed).nextParallelRNG(LOOT_RNG_SALT);
|
||||
KList<ItemStack> drops = new KList<>();
|
||||
for (String key : parts[4].split(",")) {
|
||||
String trimmed = key.trim();
|
||||
if (trimmed.isEmpty()) {
|
||||
continue;
|
||||
KList<ItemStack> stacks = resolve(engine, level, binding);
|
||||
emit(level, entity, stacks);
|
||||
return true;
|
||||
}
|
||||
|
||||
static boolean hasBindingTag(Set<String> tags) {
|
||||
for (String tag : tags) {
|
||||
if (tag.startsWith(TAG_PREFIX)) {
|
||||
return true;
|
||||
}
|
||||
IrisLootTable table = engine.getData().getLootLoader().load(trimmed);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void fillContainer(Engine engine, ServerLevel level, ContainerEntity container, LootBinding binding) {
|
||||
container.setContainerLootTable(null);
|
||||
container.clearItemStacks();
|
||||
KList<ItemStack> items = resolve(engine, level, binding);
|
||||
ModdedLootApplier.fillContainer(container, items, new RNG(binding.seed()));
|
||||
}
|
||||
|
||||
private static KList<ItemStack> resolve(Engine engine, ServerLevel level, LootBinding binding) {
|
||||
KList<ItemStack> drops = new KList<>();
|
||||
for (String key : binding.tableKeys()) {
|
||||
IrisLootTable table = engine.getData().getLootLoader().load(key);
|
||||
if (table == null) {
|
||||
if (WARNED_TABLES.add(trimmed)) {
|
||||
IrisLogging.warn("Iris death loot: unknown loot table '" + trimmed + "'");
|
||||
if (WARNED_TABLES.add(key)) {
|
||||
IrisLogging.warn("Iris death loot: unknown loot table '" + key + "'");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
drops.addAll(ModdedItemTranslator.loot(table, lootRng, InventorySlotType.STORAGE, level, spawnX, spawnY, spawnZ));
|
||||
RNG lootRng = new RNG(binding.seed()).nextParallelRNG(LOOT_RNG_SALT);
|
||||
drops.addAll(ModdedItemTranslator.loot(table, lootRng, InventorySlotType.STORAGE, level,
|
||||
binding.spawnX(), binding.spawnY(), binding.spawnZ()));
|
||||
}
|
||||
emit(level, entity, drops);
|
||||
return drops;
|
||||
}
|
||||
|
||||
private static void emit(ServerLevel level, LivingEntity entity, KList<ItemStack> drops) {
|
||||
double x = entity.getX();
|
||||
double y = entity.getY() + 0.5D;
|
||||
double z = entity.getZ();
|
||||
for (ItemStack stack : drops) {
|
||||
if (stack == null || stack.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
ItemEntity itemEntity = new ItemEntity(level, x, y, z, stack);
|
||||
itemEntity.setDefaultPickUpDelay();
|
||||
level.addFreshEntity(itemEntity);
|
||||
entity.spawnAtLocation(level, stack);
|
||||
}
|
||||
}
|
||||
|
||||
private static String findTag(LivingEntity entity) {
|
||||
private static String encode(LootBinding binding) {
|
||||
return TAG_PREFIX + binding.seed() + "|" + binding.spawnX() + "|" + binding.spawnY() + "|"
|
||||
+ binding.spawnZ() + "|" + String.join(",", binding.tableKeys());
|
||||
}
|
||||
|
||||
private static LootBinding decode(String encoded) {
|
||||
String[] parts = encoded.substring(TAG_PREFIX.length()).split("\\|", 5);
|
||||
if (parts.length < 5) {
|
||||
IrisLogging.debug("Iris death loot: malformed loot tag '" + encoded + "'");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
long seed = Long.parseLong(parts[0]);
|
||||
int spawnX = Integer.parseInt(parts[1]);
|
||||
int spawnY = Integer.parseInt(parts[2]);
|
||||
int spawnZ = Integer.parseInt(parts[3]);
|
||||
KList<String> tableKeys = new KList<>();
|
||||
for (String key : parts[4].split(",")) {
|
||||
String trimmed = key.trim();
|
||||
if (!trimmed.isEmpty()) {
|
||||
tableKeys.add(trimmed);
|
||||
}
|
||||
}
|
||||
return new LootBinding(seed, spawnX, spawnY, spawnZ, tableKeys);
|
||||
} catch (NumberFormatException error) {
|
||||
IrisLogging.debug("Iris death loot: malformed loot tag '" + encoded + "'");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String findTag(Entity entity) {
|
||||
for (String tag : entity.entityTags()) {
|
||||
if (tag.startsWith(TAG_PREFIX)) {
|
||||
return tag;
|
||||
@@ -127,4 +174,7 @@ public final class ModdedDeathLoot {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private record LootBinding(long seed, int spawnX, int spawnY, int spawnZ, KList<String> tableKeys) {
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -91,7 +91,7 @@ public final class ModdedDecoratorHooks implements DecoratorPlatformHooks.FaceFi
|
||||
}
|
||||
}
|
||||
|
||||
return ModdedBlockState.of(cloned, fabric.parsedProperties());
|
||||
return fabric.withHandle(cloned, fabric.parsedProperties());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+9
-17
@@ -39,7 +39,6 @@ import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeManager;
|
||||
import net.minecraft.world.level.biome.Biomes;
|
||||
import net.minecraft.world.level.biome.FixedBiomeSource;
|
||||
import net.minecraft.world.level.dimension.BuiltinDimensionTypes;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import net.minecraft.world.level.dimension.LevelStem;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
@@ -53,7 +52,6 @@ import org.slf4j.LoggerFactory;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -64,7 +62,6 @@ public final class ModdedDimensionManager {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final Object LOCK = new Object();
|
||||
private static final ConcurrentHashMap<String, Handle> HANDLES = new ConcurrentHashMap<>();
|
||||
private static final Set<String> TYPE_FALLBACK_WARNINGS = ConcurrentHashMap.newKeySet();
|
||||
private static final TicketType TELEPORT_WARM_TICKET = new TicketType(TicketType.NO_TIMEOUT, TicketType.FLAG_LOADING);
|
||||
private static volatile ModdedServerAccess access;
|
||||
|
||||
@@ -234,21 +231,15 @@ public final class ModdedDimensionManager {
|
||||
private static Holder<DimensionType> resolveDimensionType(RegistryAccess registryAccess, String pack, String packDimensionKey) {
|
||||
Registry<DimensionType> registry = registryAccess.lookupOrThrow(Registries.DIMENSION_TYPE);
|
||||
IrisDimension dimension = loadPackDimension(pack, packDimensionKey);
|
||||
if (dimension != null) {
|
||||
String typeRef = ModdedForcedDatapack.dimensionTypeRef(dimension);
|
||||
ResourceKey<DimensionType> typeKey = ResourceKey.create(Registries.DIMENSION_TYPE, Identifier.parse(typeRef));
|
||||
Optional<Holder.Reference<DimensionType>> packType = registry.get(typeKey);
|
||||
if (packType.isPresent()) {
|
||||
return packType.get();
|
||||
}
|
||||
if (TYPE_FALLBACK_WARNINGS.add(typeRef)) {
|
||||
LOGGER.warn("Iris dimension type {} (pack {} dim {}) is not registered yet; injecting with fallback heights. Restart the server so the forced datapack installs it.", typeRef, pack, packDimensionKey);
|
||||
}
|
||||
if (dimension == null) {
|
||||
throw new IllegalStateException("Iris cannot resolve dimension type for missing pack dimension '"
|
||||
+ packDimensionKey + "' in pack '" + pack + "'");
|
||||
}
|
||||
ResourceKey<DimensionType> studioPool = ResourceKey.create(Registries.DIMENSION_TYPE, Identifier.parse("irisworldgen:studio_pool"));
|
||||
return registry.get(studioPool)
|
||||
.map(reference -> (Holder<DimensionType>) reference)
|
||||
.orElseGet(() -> registry.getOrThrow(BuiltinDimensionTypes.OVERWORLD));
|
||||
String typeRef = ModdedForcedDatapack.dimensionTypeRef(dimension);
|
||||
ResourceKey<DimensionType> typeKey = ResourceKey.create(Registries.DIMENSION_TYPE, Identifier.parse(typeRef));
|
||||
Holder.Reference<DimensionType> packType = ModdedForcedDatapack.requireRegisteredDimensionType(
|
||||
typeRef, registry.get(typeKey), pack, packDimensionKey);
|
||||
return packType;
|
||||
}
|
||||
|
||||
private static IrisDimension loadPackDimension(String pack, String packDimensionKey) {
|
||||
@@ -295,6 +286,7 @@ public final class ModdedDimensionManager {
|
||||
false);
|
||||
|
||||
serverAccess.putLevel(server, key, level);
|
||||
generator.bindLevel(level);
|
||||
server.getPlayerList().addWorldborderListener(level);
|
||||
return new Handle(dimensionId, pack, packDimensionKey, seed, level, generator);
|
||||
}
|
||||
|
||||
+85
-1
@@ -21,6 +21,8 @@ package art.arcane.iris.modded;
|
||||
import art.arcane.iris.core.gui.GuiHost;
|
||||
import art.arcane.iris.engine.decorator.DecoratorPlatformHooks;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineEffectsProvider;
|
||||
import art.arcane.iris.engine.framework.EnginePlatformHooks;
|
||||
import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
|
||||
import art.arcane.iris.engine.framework.PreservationRegistry;
|
||||
import art.arcane.iris.engine.object.BlockDataMergeSupport;
|
||||
@@ -42,7 +44,12 @@ import art.arcane.iris.modded.service.ModdedSettingsHotloadService;
|
||||
import art.arcane.iris.modded.service.ModdedStudioHotloadService;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.storage.LevelData;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -58,6 +65,8 @@ public final class ModdedEngineBootstrap {
|
||||
private static volatile ModdedLoader loader;
|
||||
private static volatile ModdedPlatform platform;
|
||||
private static volatile MinecraftServer currentServer;
|
||||
private static volatile MinecraftServer spawnCaptureServer;
|
||||
private static volatile boolean initialSpawnWasDefault;
|
||||
|
||||
private ModdedEngineBootstrap() {
|
||||
}
|
||||
@@ -81,8 +90,10 @@ public final class ModdedEngineBootstrap {
|
||||
}
|
||||
|
||||
public static void start(MinecraftServer server) {
|
||||
captureInitialSpawn(server);
|
||||
currentServer = server;
|
||||
bind();
|
||||
bindWorldGenerators(server);
|
||||
ModdedStartup.reset();
|
||||
ModdedScheduler scheduler = schedulerOrNull();
|
||||
if (scheduler != null) {
|
||||
@@ -94,11 +105,38 @@ public final class ModdedEngineBootstrap {
|
||||
ModdedSentry.start(loader());
|
||||
}
|
||||
|
||||
private static void bindWorldGenerators(MinecraftServer server) {
|
||||
for (ServerLevel level : server.getAllLevels()) {
|
||||
if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator) {
|
||||
generator.bindLevel(level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void serverAboutToStart(MinecraftServer server) {
|
||||
captureInitialSpawn(server);
|
||||
}
|
||||
|
||||
public static void serverStarted(MinecraftServer server) {
|
||||
bindWorldGenerators(server);
|
||||
reconcileSpawn(server);
|
||||
ModdedWorldCheck.serverStarted(server);
|
||||
}
|
||||
|
||||
public static void levelLoaded(ServerLevel level) {
|
||||
if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator) {
|
||||
generator.bindLevel(level);
|
||||
}
|
||||
}
|
||||
|
||||
public static void stop() {
|
||||
MinecraftServer stoppingServer = currentServer;
|
||||
ModdedWorldCheck.serverStopped(stoppingServer);
|
||||
ModdedProtocolHandler.stop();
|
||||
ModdedPregenJob.shutdown();
|
||||
ModdedObjectUndo.clearAll();
|
||||
ModdedWandService.clearAll();
|
||||
ModdedBlockBreakHandler.clear();
|
||||
ModdedStudioCommands.clear();
|
||||
ModdedWorldEngines.shutdown();
|
||||
ModdedPrimaryWorldRouter.clear();
|
||||
@@ -112,6 +150,49 @@ public final class ModdedEngineBootstrap {
|
||||
ModdedSentry.flush();
|
||||
ModdedStartup.reset();
|
||||
currentServer = null;
|
||||
spawnCaptureServer = null;
|
||||
initialSpawnWasDefault = false;
|
||||
}
|
||||
|
||||
private static void captureInitialSpawn(MinecraftServer server) {
|
||||
if (spawnCaptureServer == server) {
|
||||
return;
|
||||
}
|
||||
LevelData.RespawnData respawnData = server.getRespawnData();
|
||||
initialSpawnWasDefault = respawnData == null || LevelData.RespawnData.DEFAULT.equals(respawnData);
|
||||
spawnCaptureServer = server;
|
||||
}
|
||||
|
||||
private static void reconcileSpawn(MinecraftServer server) {
|
||||
LevelData.RespawnData current = server.getRespawnData();
|
||||
if (current == null) {
|
||||
return;
|
||||
}
|
||||
ServerLevel level = server.getLevel(current.dimension());
|
||||
if (level == null || !(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator)) {
|
||||
return;
|
||||
}
|
||||
String dimensionId = level.dimension().identifier().toString();
|
||||
boolean studio = dimensionId.startsWith("irisworldgen:studio_");
|
||||
if (!shouldReconcileSpawn(initialSpawnWasDefault, studio, current.pos().getX(), current.pos().getZ())) {
|
||||
return;
|
||||
}
|
||||
LevelChunk originChunk = level.getChunk(0, 0);
|
||||
int surfaceY = originChunk.getHeight(Heightmap.Types.MOTION_BLOCKING, 0, 0) + 1;
|
||||
BlockPos position = reconciledSpawnPosition(surfaceY, level.getMinY(), level.getHeight());
|
||||
server.setRespawnData(LevelData.RespawnData.of(
|
||||
level.dimension(), position, current.yaw(), current.pitch()));
|
||||
LOGGER.info("Iris spawn reconciled for {} at {},{},{}", dimensionId,
|
||||
position.getX(), position.getY(), position.getZ());
|
||||
}
|
||||
|
||||
static boolean shouldReconcileSpawn(boolean initialDefault, boolean studio, int currentX, int currentZ) {
|
||||
return initialDefault || studio || currentX == 0 && currentZ == 0;
|
||||
}
|
||||
|
||||
static BlockPos reconciledSpawnPosition(int surfaceY, int minY, int height) {
|
||||
int y = Math.max(minY + 1, Math.min(minY + height - 2, surfaceY));
|
||||
return new BlockPos(0, y, 0);
|
||||
}
|
||||
|
||||
public static void bootCommon(ModdedLoader moddedLoader, String loaderDescription, Runnable chunkGeneratorRegistration) {
|
||||
@@ -191,6 +272,7 @@ public final class ModdedEngineBootstrap {
|
||||
IrisObjectRotation.bindFallbackRotator(new ModdedStateRotator());
|
||||
BlockDataMergeSupport.bindFallbackMerger(new ModdedStateMerger());
|
||||
TileData.bindFallbackReader(new ModdedTileReader(boundLoader::currentServer));
|
||||
TileData.bindFallbackFactory(ModdedTileData::fromProperties);
|
||||
ModdedGuiHost.install();
|
||||
ModdedDecoratorHooks decoratorHooks = new ModdedDecoratorHooks();
|
||||
DecoratorPlatformHooks.bind(decoratorHooks, decoratorHooks);
|
||||
@@ -198,10 +280,12 @@ public final class ModdedEngineBootstrap {
|
||||
SERVICE_MANAGER.register(ModdedLogFilterService.class, new ModdedLogFilterService());
|
||||
SERVICE_MANAGER.register(ModdedEngineMaintenanceService.class, new ModdedEngineMaintenanceService());
|
||||
SERVICE_MANAGER.register(ModdedSettingsHotloadService.class, new ModdedSettingsHotloadService());
|
||||
SERVICE_MANAGER.register(ModdedStudioHotloadService.class, new ModdedStudioHotloadService());
|
||||
ModdedStudioHotloadService studioHotloadService = SERVICE_MANAGER.register(ModdedStudioHotloadService.class, new ModdedStudioHotloadService());
|
||||
SERVICE_MANAGER.register(ModdedChunkUpdateService.class, new ModdedChunkUpdateService());
|
||||
SERVICE_MANAGER.register(ModdedEntitySpawnService.class, new ModdedEntitySpawnService());
|
||||
IrisServices.register(PreservationRegistry.class, preservation);
|
||||
IrisServices.register(EngineEffectsProvider.class, (EngineEffectsProvider) ModdedEngineEffects::new);
|
||||
IrisServices.register(EnginePlatformHooks.class, studioHotloadService);
|
||||
IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> new ModdedWorldManager(engine));
|
||||
ModdedCustomContentRegistry.discover();
|
||||
platform = created;
|
||||
|
||||
@@ -0,0 +1,499 @@
|
||||
/*
|
||||
* 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.IrisSettings;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineAssignedComponent;
|
||||
import art.arcane.iris.engine.framework.EngineEffects;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisCommand;
|
||||
import art.arcane.iris.engine.object.IrisCommandRegistry;
|
||||
import art.arcane.iris.engine.object.IrisEffect;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.particles.ParticleType;
|
||||
import net.minecraft.core.particles.SimpleParticleType;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.network.protocol.game.ClientboundSoundPacket;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.sounds.SoundEvent;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.effect.MobEffect;
|
||||
import net.minecraft.world.effect.MobEffectInstance;
|
||||
import net.minecraft.world.effect.MobEffects;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public final class ModdedEngineEffects extends EngineAssignedComponent implements EngineEffects {
|
||||
private static final long EFFECT_BUDGET_NANOS = 1_500_000L;
|
||||
private static final long SAMPLE_INTERVAL_MILLIS = 55L;
|
||||
private static final double SAMPLE_DISTANCE_SQUARED = 81.0D;
|
||||
private static final double PLAYER_REFRESH_CHANCE = 0.02D;
|
||||
private static final Set<String> UNKNOWN_POTIONS = ConcurrentHashMap.newKeySet();
|
||||
private static final Set<String> UNKNOWN_SOUNDS = ConcurrentHashMap.newKeySet();
|
||||
private static final Set<String> UNKNOWN_PARTICLES = ConcurrentHashMap.newKeySet();
|
||||
private static final Set<String> UNSUPPORTED_PARTICLES = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private final Map<UUID, PlayerState> players;
|
||||
private final AtomicBoolean updateRequested;
|
||||
private final AtomicBoolean tickRequested;
|
||||
private final AtomicBoolean passQueued;
|
||||
|
||||
public ModdedEngineEffects(Engine engine) {
|
||||
super(engine, "FX");
|
||||
players = new HashMap<>();
|
||||
updateRequested = new AtomicBoolean(false);
|
||||
tickRequested = new AtomicBoolean(false);
|
||||
passQueued = new AtomicBoolean(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updatePlayerMap() {
|
||||
updateRequested.set(true);
|
||||
queueMainPass();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tickRandomPlayer() {
|
||||
tickRequested.set(true);
|
||||
queueMainPass();
|
||||
}
|
||||
|
||||
private void queueMainPass() {
|
||||
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
|
||||
if (scheduler == null || !passQueued.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
scheduler.global(this::runQueuedPass);
|
||||
}
|
||||
|
||||
private void runQueuedPass() {
|
||||
try {
|
||||
boolean shouldUpdate = updateRequested.getAndSet(false);
|
||||
boolean shouldTick = tickRequested.getAndSet(false);
|
||||
if (!shouldUpdate && !shouldTick) {
|
||||
return;
|
||||
}
|
||||
|
||||
ServerLevel level = resolveLevel();
|
||||
if (level == null) {
|
||||
players.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldUpdate) {
|
||||
syncPlayers(level);
|
||||
}
|
||||
if (shouldTick) {
|
||||
tickPlayers(level);
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError(error);
|
||||
} finally {
|
||||
passQueued.set(false);
|
||||
if (updateRequested.get() || tickRequested.get()) {
|
||||
queueMainPass();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ServerLevel resolveLevel() {
|
||||
Engine engine = getEngine();
|
||||
if (engine.isClosed() || !engine.getWorld().hasPlatformWorld()) {
|
||||
return null;
|
||||
}
|
||||
Object nativeWorld = engine.getWorld().platformWorld().nativeHandle();
|
||||
return nativeWorld instanceof ServerLevel ? (ServerLevel) nativeWorld : null;
|
||||
}
|
||||
|
||||
private void syncPlayers(ServerLevel level) {
|
||||
List<ServerPlayer> activePlayers = level.players();
|
||||
Set<UUID> activeIds = new HashSet<>(Math.max(16, activePlayers.size() * 2));
|
||||
for (ServerPlayer player : activePlayers) {
|
||||
UUID playerId = player.getUUID();
|
||||
activeIds.add(playerId);
|
||||
PlayerState state = players.get(playerId);
|
||||
if (state == null) {
|
||||
players.put(playerId, new PlayerState(player));
|
||||
} else {
|
||||
state.player(player);
|
||||
}
|
||||
}
|
||||
|
||||
Iterator<Map.Entry<UUID, PlayerState>> iterator = players.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<UUID, PlayerState> entry = iterator.next();
|
||||
if (!activeIds.contains(entry.getKey())) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void tickPlayers(ServerLevel level) {
|
||||
if (players.isEmpty()) {
|
||||
syncPlayers(level);
|
||||
} else if (RNG.r.d() < PLAYER_REFRESH_CHANCE) {
|
||||
syncPlayers(level);
|
||||
return;
|
||||
}
|
||||
if (players.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<PlayerState> snapshot = new ArrayList<>(players.values());
|
||||
long started = System.nanoTime();
|
||||
int remaining = snapshot.size();
|
||||
while (remaining-- > 0 && System.nanoTime() - started < EFFECT_BUDGET_NANOS) {
|
||||
PlayerState state = snapshot.get(RNG.r.nextInt(snapshot.size()));
|
||||
try {
|
||||
tickPlayer(level, state);
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void tickPlayer(ServerLevel level, PlayerState state) {
|
||||
ServerPlayer player = state.player();
|
||||
if (!player.isAlive() || player.isRemoved() || player.level() != level) {
|
||||
return;
|
||||
}
|
||||
|
||||
samplePlayer(state);
|
||||
if (!IrisSettings.get().getWorld().isEffectSystem()) {
|
||||
return;
|
||||
}
|
||||
|
||||
IrisRegion region = state.region();
|
||||
if (region != null) {
|
||||
applyEffects(level, player, region.getEffects());
|
||||
}
|
||||
IrisBiome biome = state.biome();
|
||||
if (biome != null) {
|
||||
applyEffects(level, player, biome.getEffects());
|
||||
}
|
||||
}
|
||||
|
||||
private void samplePlayer(PlayerState state) {
|
||||
ServerPlayer player = state.player();
|
||||
double deltaX = player.getX() - state.lastX();
|
||||
double deltaY = player.getY() - state.lastY();
|
||||
double deltaZ = player.getZ() - state.lastZ();
|
||||
double distanceSquared = deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ;
|
||||
long now = System.currentTimeMillis();
|
||||
if (!needsSample(state.sampled(), now - state.lastSample(), distanceSquared)) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.sampled(true);
|
||||
state.lastPosition(player.getX(), player.getY(), player.getZ());
|
||||
state.lastSample(now);
|
||||
int blockX = floor(player.getX());
|
||||
int blockY = floor(player.getY()) - getEngine().getWorld().minHeight();
|
||||
int blockZ = floor(player.getZ());
|
||||
state.biome(getEngine().getBiome(blockX, blockY, blockZ));
|
||||
state.region(getEngine().getRegion(blockX, blockZ));
|
||||
}
|
||||
|
||||
private void applyEffects(ServerLevel level, ServerPlayer player, Iterable<IrisEffect> effects) {
|
||||
for (IrisEffect effect : effects) {
|
||||
try {
|
||||
applyEffect(level, player, effect);
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void applyEffect(ServerLevel level, ServerPlayer player, IrisEffect effect) {
|
||||
if (!effect.shouldApplyNow()) {
|
||||
return;
|
||||
}
|
||||
|
||||
applySound(player, effect);
|
||||
applyParticles(level, player, effect);
|
||||
applyCommands(level, player, effect.getCommandRegistry());
|
||||
applyPotion(player, effect);
|
||||
}
|
||||
|
||||
private void applySound(ServerPlayer player, IrisEffect effect) {
|
||||
String key = normalizeRegistryKey(effect.getSoundKey());
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
Identifier identifier = Identifier.tryParse(key);
|
||||
Optional<Holder.Reference<SoundEvent>> sound = identifier == null
|
||||
? Optional.empty()
|
||||
: BuiltInRegistries.SOUND_EVENT.get(identifier);
|
||||
if (sound.isEmpty()) {
|
||||
warnUnknown(UNKNOWN_SOUNDS, "sound", key);
|
||||
return;
|
||||
}
|
||||
|
||||
int distance = effect.getSoundDistance();
|
||||
double x = player.getX() + RNG.r.i(-distance, distance);
|
||||
double y = player.getY() + RNG.r.i(-distance, distance);
|
||||
double z = player.getZ() + RNG.r.i(-distance, distance);
|
||||
float volume = (float) effect.getVolume();
|
||||
float pitch = (float) RNG.r.d(effect.getMinPitch(), effect.getMaxPitch());
|
||||
player.connection.send(new ClientboundSoundPacket(
|
||||
sound.get(), SoundSource.MASTER, x, y, z, volume, pitch, ThreadLocalRandom.current().nextLong()));
|
||||
}
|
||||
|
||||
private void applyParticles(ServerLevel level, ServerPlayer player, IrisEffect effect) {
|
||||
String key = normalizeRegistryKey(effect.getParticleEffectKey());
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
Identifier identifier = Identifier.tryParse(key);
|
||||
ParticleType<?> particleType = identifier == null
|
||||
? null
|
||||
: BuiltInRegistries.PARTICLE_TYPE.getValue(identifier);
|
||||
if (particleType == null) {
|
||||
warnUnknown(UNKNOWN_PARTICLES, "particle", key);
|
||||
return;
|
||||
}
|
||||
if (!(particleType instanceof SimpleParticleType simpleParticle)) {
|
||||
if (UNSUPPORTED_PARTICLES.add(key)) {
|
||||
IrisLogging.warn("Particle type \"" + key + "\" requires particle data and cannot be used by an Iris effect without data.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Vec3 direction = player.getLookAngle();
|
||||
double forward = RNG.r.i(effect.getParticleDistance()) + effect.getParticleAway();
|
||||
double sideways = RNG.r.d(-effect.getParticleDistanceWidth(), effect.getParticleDistanceWidth());
|
||||
double surfaceX = player.getX() + direction.x * forward + direction.z * sideways;
|
||||
double surfaceZ = player.getZ() + direction.z * forward - direction.x * sideways;
|
||||
int surfaceY = getEngine().getHeight(floor(surfaceX), floor(surfaceZ));
|
||||
double x = surfaceX + RNG.r.d();
|
||||
double y = surfaceY + 1.0D + level.getMinY() + RNG.r.i(effect.getParticleOffset());
|
||||
double z = surfaceZ + RNG.r.d();
|
||||
double altX = randomized(effect.getParticleAltX(), effect.isRandomAltX());
|
||||
double altY = randomized(effect.getParticleAltY(), effect.isRandomAltY());
|
||||
double altZ = randomized(effect.getParticleAltZ(), effect.isRandomAltZ());
|
||||
level.sendParticles(player, simpleParticle, false, false,
|
||||
x, y, z, effect.getParticleCount(), altX, altY, altZ, effect.getExtra());
|
||||
}
|
||||
|
||||
private void applyCommands(ServerLevel level, ServerPlayer player, IrisCommandRegistry registry) {
|
||||
if (registry == null || registry.getRawCommands() == null || registry.getRawCommands().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ModdedPlatformWorld world = new ModdedPlatformWorld(level);
|
||||
double x = commandCoordinate(player.getX(), registry.getCommandOffsetX(), registry.isCommandRandomAltX());
|
||||
double y = commandCoordinate(player.getY(), registry.getCommandOffsetY(), registry.isCommandRandomAltY());
|
||||
double z = commandCoordinate(player.getZ(), registry.getCommandOffsetZ(), registry.isCommandRandomAltZ());
|
||||
for (IrisCommand command : registry.getRawCommands()) {
|
||||
command.run(world, floor(x), floor(y), floor(z));
|
||||
if (registry.isCommandAllRandomLocations()) {
|
||||
x = commandCoordinate(player.getX(), registry.getCommandOffsetX(), registry.isCommandRandomAltX());
|
||||
y = commandCoordinate(player.getY(), registry.getCommandOffsetY(), registry.isCommandRandomAltY());
|
||||
z = commandCoordinate(player.getZ(), registry.getCommandOffsetZ(), registry.isCommandRandomAltZ());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void applyPotion(ServerPlayer player, IrisEffect effect) {
|
||||
int strength = effect.getPotionStrength();
|
||||
if (strength < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Holder<MobEffect> type = resolvePotion(effect.getPotionEffect());
|
||||
MobEffectInstance current = player.getEffect(type);
|
||||
if (current != null && !shouldReplacePotionEffect(current.getAmplifier(), strength)) {
|
||||
return;
|
||||
}
|
||||
if (current != null) {
|
||||
player.removeEffect(type);
|
||||
}
|
||||
|
||||
int minimum = Math.min(effect.getPotionTicksMin(), effect.getPotionTicksMax());
|
||||
int maximum = Math.max(effect.getPotionTicksMin(), effect.getPotionTicksMax());
|
||||
int duration = RNG.r.i(minimum, maximum);
|
||||
player.addEffect(new MobEffectInstance(type, duration, strength, true, false, false));
|
||||
}
|
||||
|
||||
private Holder<MobEffect> resolvePotion(String rawKey) {
|
||||
String key = normalizePotionEffectKey(rawKey);
|
||||
Identifier identifier = Identifier.tryParse(key);
|
||||
if (identifier != null) {
|
||||
Optional<Holder.Reference<MobEffect>> effect = BuiltInRegistries.MOB_EFFECT.get(identifier);
|
||||
if (effect.isPresent()) {
|
||||
return effect.get();
|
||||
}
|
||||
}
|
||||
if (UNKNOWN_POTIONS.add(key)) {
|
||||
IrisLogging.warn("Unknown Potion Effect Type: \"" + rawKey + "\". Using LUCK instead.");
|
||||
}
|
||||
return MobEffects.LUCK;
|
||||
}
|
||||
|
||||
static String normalizeRegistryKey(String rawKey) {
|
||||
if (rawKey == null || rawKey.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String normalized = rawKey.trim().toLowerCase(Locale.ROOT).replace(' ', '_');
|
||||
return normalized.contains(":") ? normalized : "minecraft:" + normalized;
|
||||
}
|
||||
|
||||
static String normalizePotionEffectKey(String rawKey) {
|
||||
String normalized = normalizeRegistryKey(rawKey == null || rawKey.isBlank() ? "luck" : rawKey);
|
||||
int separator = normalized.indexOf(':');
|
||||
String namespace = normalized.substring(0, separator);
|
||||
String path = normalized.substring(separator + 1);
|
||||
String aliased = switch (path) {
|
||||
case "slow" -> "slowness";
|
||||
case "fast_digging" -> "haste";
|
||||
case "slow_digging" -> "mining_fatigue";
|
||||
case "increase_damage" -> "strength";
|
||||
case "heal" -> "instant_health";
|
||||
case "harm" -> "instant_damage";
|
||||
case "jump" -> "jump_boost";
|
||||
case "confusion" -> "nausea";
|
||||
case "damage_resistance" -> "resistance";
|
||||
default -> path;
|
||||
};
|
||||
return namespace + ":" + aliased;
|
||||
}
|
||||
|
||||
static boolean needsSample(boolean sampled, long elapsedMillis, double distanceSquared) {
|
||||
return !sampled || elapsedMillis > SAMPLE_INTERVAL_MILLIS && distanceSquared > SAMPLE_DISTANCE_SQUARED;
|
||||
}
|
||||
|
||||
static boolean shouldReplacePotionEffect(int existingAmplifier, int configuredAmplifier) {
|
||||
return existingAmplifier <= configuredAmplifier;
|
||||
}
|
||||
|
||||
private static int floor(double value) {
|
||||
return (int) Math.floor(value);
|
||||
}
|
||||
|
||||
private static double randomized(double value, boolean random) {
|
||||
return random ? RNG.r.d(-value, value) : value;
|
||||
}
|
||||
|
||||
private static double commandCoordinate(double base, double offset, boolean random) {
|
||||
return base + randomized(offset, random);
|
||||
}
|
||||
|
||||
private static void warnUnknown(Set<String> warned, String type, String key) {
|
||||
if (warned.add(key)) {
|
||||
IrisLogging.warn("Unknown " + type + " type: \"" + key + "\".");
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PlayerState {
|
||||
private ServerPlayer player;
|
||||
private IrisBiome biome;
|
||||
private IrisRegion region;
|
||||
private double lastX;
|
||||
private double lastY;
|
||||
private double lastZ;
|
||||
private long lastSample;
|
||||
private boolean sampled;
|
||||
|
||||
private PlayerState(ServerPlayer player) {
|
||||
this.player = player;
|
||||
lastX = player.getX();
|
||||
lastY = player.getY();
|
||||
lastZ = player.getZ();
|
||||
lastSample = -1L;
|
||||
sampled = false;
|
||||
}
|
||||
|
||||
private ServerPlayer player() {
|
||||
return player;
|
||||
}
|
||||
|
||||
private void player(ServerPlayer player) {
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
private IrisBiome biome() {
|
||||
return biome;
|
||||
}
|
||||
|
||||
private void biome(IrisBiome biome) {
|
||||
this.biome = biome;
|
||||
}
|
||||
|
||||
private IrisRegion region() {
|
||||
return region;
|
||||
}
|
||||
|
||||
private void region(IrisRegion region) {
|
||||
this.region = region;
|
||||
}
|
||||
|
||||
private double lastX() {
|
||||
return lastX;
|
||||
}
|
||||
|
||||
private double lastY() {
|
||||
return lastY;
|
||||
}
|
||||
|
||||
private double lastZ() {
|
||||
return lastZ;
|
||||
}
|
||||
|
||||
private void lastPosition(double x, double y, double z) {
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
lastZ = z;
|
||||
}
|
||||
|
||||
private long lastSample() {
|
||||
return lastSample;
|
||||
}
|
||||
|
||||
private void lastSample(long lastSample) {
|
||||
this.lastSample = lastSample;
|
||||
}
|
||||
|
||||
private boolean sampled() {
|
||||
return sampled;
|
||||
}
|
||||
|
||||
private void sampled(boolean sampled) {
|
||||
this.sampled = sampled;
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import net.minecraft.world.entity.Mob;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public final class ModdedEntityAwareness {
|
||||
private static final String UNAWARE_TAG = "iris_unaware";
|
||||
|
||||
private ModdedEntityAwareness() {
|
||||
}
|
||||
|
||||
public static void configure(Mob mob, boolean aware) {
|
||||
if (aware) {
|
||||
mob.removeTag(UNAWARE_TAG);
|
||||
return;
|
||||
}
|
||||
if (!mob.entityTags().contains(UNAWARE_TAG) && !mob.addTag(UNAWARE_TAG)) {
|
||||
IrisLogging.warn("Iris could not mark generated mob '" + mob.getStringUUID() + "' as unaware");
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isAware(Mob mob) {
|
||||
return isAware(mob.entityTags());
|
||||
}
|
||||
|
||||
static boolean isAware(Set<String> tags) {
|
||||
return !tags.contains(UNAWARE_TAG);
|
||||
}
|
||||
|
||||
static void configureTags(Set<String> tags, boolean aware) {
|
||||
if (aware) {
|
||||
tags.remove(UNAWARE_TAG);
|
||||
} else {
|
||||
tags.add(UNAWARE_TAG);
|
||||
}
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisCommand;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
|
||||
final class ModdedEntityCommandRunner {
|
||||
private ModdedEntityCommandRunner() {
|
||||
}
|
||||
|
||||
static void run(KList<IrisCommand> commands, ServerLevel level, int blockX, int blockY, int blockZ) {
|
||||
if (commands.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
MinecraftServer server = level.getServer();
|
||||
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
|
||||
if (server == null || scheduler == null) {
|
||||
IrisLogging.error("Iris could not schedule entity commands because the modded server scheduler is unavailable.");
|
||||
return;
|
||||
}
|
||||
|
||||
ModdedPlatformWorld world = new ModdedPlatformWorld(level);
|
||||
for (IrisCommand command : commands) {
|
||||
if (command == null || !command.isValid(world)) {
|
||||
continue;
|
||||
}
|
||||
schedule(command, server, scheduler, blockX, blockY, blockZ);
|
||||
}
|
||||
}
|
||||
|
||||
private static void schedule(IrisCommand command, MinecraftServer server, ModdedScheduler scheduler, int blockX, int blockY, int blockZ) {
|
||||
int delay = clampDelay(command.getDelay(), 0);
|
||||
int repeatDelay = clampDelay(command.getRepeatDelay(), 1);
|
||||
for (String raw : command.getCommands()) {
|
||||
String prepared = prepareCommand(raw, blockX, blockY, blockZ);
|
||||
if (prepared == null) {
|
||||
continue;
|
||||
}
|
||||
if (command.isRepeat()) {
|
||||
scheduler.laterGlobal(() -> new RepeatingCommand(server, scheduler, prepared, repeatDelay).run(), delay);
|
||||
} else {
|
||||
scheduler.laterGlobal(() -> ModdedServerCommands.dispatch(server, prepared), delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static String prepareCommand(String raw, int blockX, int blockY, int blockZ) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return (raw.startsWith("/") ? raw.substring(1) : raw)
|
||||
.replace("{x}", String.valueOf(blockX))
|
||||
.replace("{y}", String.valueOf(blockY))
|
||||
.replace("{z}", String.valueOf(blockZ));
|
||||
}
|
||||
|
||||
static int clampDelay(long delay, int minimum) {
|
||||
return (int) Math.max(minimum, Math.min(Integer.MAX_VALUE, delay));
|
||||
}
|
||||
|
||||
private static final class RepeatingCommand implements Runnable {
|
||||
private final MinecraftServer server;
|
||||
private final ModdedScheduler scheduler;
|
||||
private final String command;
|
||||
private final int interval;
|
||||
|
||||
private RepeatingCommand(MinecraftServer server, ModdedScheduler scheduler, String command, int interval) {
|
||||
this.server = server;
|
||||
this.scheduler = scheduler;
|
||||
this.command = command;
|
||||
this.interval = interval;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
ModdedServerCommands.dispatch(server, command);
|
||||
scheduler.laterGlobal(this, interval);
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.Mob;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public final class ModdedEntityPersistence {
|
||||
private static final String NON_PERSISTENT_TAG = "iris_non_persistent";
|
||||
|
||||
private ModdedEntityPersistence() {
|
||||
}
|
||||
|
||||
public static void configure(Entity entity, boolean persistent) {
|
||||
if (persistent) {
|
||||
entity.removeTag(NON_PERSISTENT_TAG);
|
||||
if (entity instanceof Mob mob) {
|
||||
mob.setPersistenceRequired();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!entity.entityTags().contains(NON_PERSISTENT_TAG) && !entity.addTag(NON_PERSISTENT_TAG)) {
|
||||
IrisLogging.warn("Iris could not mark generated entity '" + entity.getStringUUID() + "' as non-persistent");
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean shouldSave(Entity entity, boolean vanillaResult) {
|
||||
return shouldSave(entity.entityTags(), vanillaResult);
|
||||
}
|
||||
|
||||
static boolean shouldSave(Set<String> tags, boolean vanillaResult) {
|
||||
return vanillaResult && !tags.contains(NON_PERSISTENT_TAG);
|
||||
}
|
||||
|
||||
static void configureTags(Set<String> tags, boolean persistent) {
|
||||
if (persistent) {
|
||||
tags.remove(NON_PERSISTENT_TAG);
|
||||
} else {
|
||||
tags.add(NON_PERSISTENT_TAG);
|
||||
}
|
||||
}
|
||||
}
|
||||
+309
-62
@@ -21,8 +21,8 @@ package art.arcane.iris.modded;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisAttributeModifier;
|
||||
import art.arcane.iris.engine.object.IrisCommand;
|
||||
import art.arcane.iris.engine.object.IrisEntity;
|
||||
import art.arcane.iris.engine.object.IrisEffect;
|
||||
import art.arcane.iris.engine.object.IrisLoot;
|
||||
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
@@ -31,13 +31,20 @@ import art.arcane.volmlib.util.math.RNG;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.particles.ItemParticleOption;
|
||||
import net.minecraft.core.particles.ParticleOptions;
|
||||
import net.minecraft.core.particles.ParticleType;
|
||||
import net.minecraft.core.particles.ParticleTypes;
|
||||
import net.minecraft.core.particles.SimpleParticleType;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.AgeableMob;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.sounds.SoundEvent;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.EntitySpawnReason;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
@@ -48,21 +55,28 @@ import net.minecraft.world.entity.ai.attributes.Attribute;
|
||||
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
|
||||
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
|
||||
import net.minecraft.world.entity.animal.panda.Panda;
|
||||
import net.minecraft.world.entity.monster.zombie.Zombie;
|
||||
import net.minecraft.world.entity.npc.villager.Villager;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public final class ModdedEntitySpawner {
|
||||
private static final int PASSENGER_RNG_BASE = 234858;
|
||||
private static final int LEASH_RNG_SEED = 234548;
|
||||
private static final int PLAYER_EFFECT_RADIUS = 32;
|
||||
private static final int RISE_MAX_MOVES = 101;
|
||||
private static final String COLOR_CODES = "0123456789AaBbCcDdEeFfKkLlMmNnOoRrXx";
|
||||
private static final Set<String> WARNED_TYPES = ConcurrentHashMap.newKeySet();
|
||||
private static final Set<String> WARNED_ATTRIBUTES = ConcurrentHashMap.newKeySet();
|
||||
private static boolean warnedSpawnEffect = false;
|
||||
private static final Set<String> WARNED_EFFECT_SOUNDS = ConcurrentHashMap.newKeySet();
|
||||
private static final Set<String> WARNED_EFFECT_PARTICLES = ConcurrentHashMap.newKeySet();
|
||||
private static final Set<String> WARNED_PERSISTENCE_TYPES = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private ModdedEntitySpawner() {
|
||||
}
|
||||
@@ -71,12 +85,18 @@ public final class ModdedEntitySpawner {
|
||||
if (engine == null || irisEntity == null || level == null) {
|
||||
return null;
|
||||
}
|
||||
if (!chunksSafe(level, blockX >> 4, blockZ >> 4)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
double x = blockX + 0.5;
|
||||
double y = blockY + 0.5;
|
||||
double z = blockZ + 0.5;
|
||||
boolean riseEffectActive = irisEntity.isSpawnEffectRiseOutOfGround() && hasPlayersNearby(level, x, y, z);
|
||||
int spawnBlockY = riseEffectActive ? blockY - 5 : blockY;
|
||||
double spawnY = spawnBlockY + 0.5;
|
||||
|
||||
Entity created = create(irisEntity, level, x, y, z);
|
||||
Entity created = create(irisEntity, level, x, spawnY, z);
|
||||
if (created == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -85,7 +105,7 @@ public final class ModdedEntitySpawner {
|
||||
return created;
|
||||
}
|
||||
|
||||
applyConfig(engine, irisEntity, created, level, blockX, blockY, blockZ, rng);
|
||||
applyConfig(engine, irisEntity, created, level, blockX, spawnBlockY, blockZ, rng, riseEffectActive);
|
||||
return created;
|
||||
}
|
||||
|
||||
@@ -99,12 +119,16 @@ public final class ModdedEntitySpawner {
|
||||
return null;
|
||||
}
|
||||
|
||||
return type.spawn(level, BlockPos.containing(x, y, z), reasonFor(irisEntity.getReason()));
|
||||
Entity entity = type.spawn(level, BlockPos.containing(x, y, z), reasonFor(irisEntity.getReason()));
|
||||
if (entity != null) {
|
||||
entity.snapTo(x, y, z, 0F, 0F);
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static void applyConfig(Engine engine, IrisEntity irisEntity, Entity entity, ServerLevel level, int blockX, int blockY, int blockZ, RNG rng) {
|
||||
private static void applyConfig(Engine engine, IrisEntity irisEntity, Entity entity, ServerLevel level, int blockX, int blockY, int blockZ, RNG rng, boolean riseEffectActive) {
|
||||
String customName = irisEntity.getCustomName();
|
||||
if (customName != null && !customName.isBlank()) {
|
||||
if (customName != null) {
|
||||
entity.setCustomName(Component.literal(colorize(customName)));
|
||||
}
|
||||
entity.setCustomNameVisible(irisEntity.isCustomNameVisible());
|
||||
@@ -114,9 +138,7 @@ public final class ModdedEntitySpawner {
|
||||
entity.setSilent(irisEntity.isSilent());
|
||||
|
||||
boolean persistent = irisEntity.isKeepEntity() || forcePersist();
|
||||
if (persistent && entity instanceof Mob persistentMob) {
|
||||
persistentMob.setPersistenceRequired();
|
||||
}
|
||||
applyPersistence(entity, persistent);
|
||||
|
||||
applyPassengers(engine, irisEntity, entity, level, blockX, blockY, blockZ, rng);
|
||||
|
||||
@@ -124,12 +146,13 @@ public final class ModdedEntitySpawner {
|
||||
applyAttributes(irisEntity, living, level, rng);
|
||||
}
|
||||
|
||||
if (entity instanceof LivingEntity lootHolder && !irisEntity.getLoot().getTables().isEmpty()) {
|
||||
ModdedDeathLoot.tag(lootHolder, irisEntity.getLoot().getTables(), blockX, blockY, blockZ, rng);
|
||||
if (!irisEntity.getLoot().getTables().isEmpty()) {
|
||||
ModdedDeathLoot.bind(engine, entity, irisEntity.getLoot().getTables(), blockX, blockY, blockZ, rng);
|
||||
}
|
||||
|
||||
if (entity instanceof Mob mob) {
|
||||
mob.setNoAi(!irisEntity.isAi());
|
||||
mob.setNoAi(shouldDisableAi(irisEntity.isAi()));
|
||||
ModdedEntityAwareness.configure(mob, irisEntity.isAware());
|
||||
mob.setCanPickUpLoot(irisEntity.isPickupItems());
|
||||
if (!irisEntity.isRemovable()) {
|
||||
mob.setPersistenceRequired();
|
||||
@@ -156,14 +179,14 @@ public final class ModdedEntitySpawner {
|
||||
}
|
||||
|
||||
if (entity instanceof Villager villager) {
|
||||
villager.setPersistenceRequired();
|
||||
ModdedEntityPersistence.configure(villager, true);
|
||||
}
|
||||
|
||||
if (irisEntity.getSpawnEffect() != null || irisEntity.isSpawnEffectRiseOutOfGround()) {
|
||||
noteSpawnEffectGap();
|
||||
applySpawnEffect(irisEntity.getSpawnEffect(), entity, level);
|
||||
ModdedEntityCommandRunner.run(irisEntity.getRawCommands(), level, blockX, blockY, blockZ);
|
||||
if (riseEffectActive && entity instanceof LivingEntity living) {
|
||||
startRiseEffect(engine, level, entity, living);
|
||||
}
|
||||
|
||||
applyRawCommands(irisEntity, level, blockX, blockY, blockZ);
|
||||
}
|
||||
|
||||
private static void applyPassengers(Engine engine, IrisEntity irisEntity, Entity entity, ServerLevel level, int blockX, int blockY, int blockZ, RNG rng) {
|
||||
@@ -228,46 +251,153 @@ public final class ModdedEntitySpawner {
|
||||
}
|
||||
|
||||
private static void applyBaby(Entity entity) {
|
||||
if (entity instanceof AgeableMob ageable) {
|
||||
ageable.setBaby(true);
|
||||
return;
|
||||
}
|
||||
if (entity instanceof Zombie zombie) {
|
||||
zombie.setBaby(true);
|
||||
if (entity instanceof Mob mob) {
|
||||
mob.setBaby(true);
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyRawCommands(IrisEntity irisEntity, ServerLevel level, int blockX, int blockY, int blockZ) {
|
||||
KList<IrisCommand> rawCommands = irisEntity.getRawCommands();
|
||||
if (rawCommands.isEmpty()) {
|
||||
return;
|
||||
static boolean isAreaClearForSpawn(ServerLevel level, IrisEntity irisEntity, int blockX, int blockY, int blockZ) {
|
||||
if (irisEntity.isSpecialType()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
MinecraftServer server = level.getServer();
|
||||
if (server == null) {
|
||||
return;
|
||||
EntityType<?> type = resolveType(irisEntity.getType());
|
||||
if (type == null) {
|
||||
return true;
|
||||
}
|
||||
return isAreaClearForSpawn(blockX, blockY, blockZ, type.getWidth(), type.getHeight(),
|
||||
position -> level.getBlockState(position).is(Blocks.AIR));
|
||||
}
|
||||
|
||||
for (IrisCommand command : rawCommands) {
|
||||
for (String raw : command.getCommands()) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
continue;
|
||||
static boolean isAreaClearForSpawn(int blockX, int blockY, int blockZ, float width, float height, Predicate<BlockPos> isAir) {
|
||||
int radius = (int) (width / 2F);
|
||||
int endY = blockY + (int) height;
|
||||
for (int x = blockX - radius; x <= blockX + radius; x++) {
|
||||
for (int y = blockY; y <= endY; y++) {
|
||||
for (int z = blockZ - radius; z <= blockZ + radius; z++) {
|
||||
if (!isAir.test(new BlockPos(x, y, z))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
String prepared = (raw.startsWith("/") ? raw.substring(1) : raw)
|
||||
.replace("{x}", String.valueOf(blockX))
|
||||
.replace("{y}", String.valueOf(blockY))
|
||||
.replace("{z}", String.valueOf(blockZ));
|
||||
ModdedServerCommands.dispatch(server, prepared);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static EntityType<?> resolveType(String key) {
|
||||
static boolean chunksSafe(ServerLevel level, int chunkX, int chunkZ) {
|
||||
return allNeighborChunksLoaded(chunkX, chunkZ,
|
||||
(x, z) -> level.getChunkSource().getChunkNow(x, z) != null);
|
||||
}
|
||||
|
||||
static boolean allNeighborChunksLoaded(int chunkX, int chunkZ, ChunkLoadedLookup lookup) {
|
||||
for (int x = chunkX - 1; x <= chunkX + 1; x++) {
|
||||
for (int z = chunkZ - 1; z <= chunkZ + 1; z++) {
|
||||
if (!lookup.loaded(x, z)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static boolean shouldDisableAi(boolean ai) {
|
||||
return !ai;
|
||||
}
|
||||
|
||||
private static boolean hasPlayersNearby(ServerLevel level, double x, double y, double z) {
|
||||
AABB bounds = new AABB(
|
||||
x - PLAYER_EFFECT_RADIUS,
|
||||
y - PLAYER_EFFECT_RADIUS,
|
||||
z - PLAYER_EFFECT_RADIUS,
|
||||
x + PLAYER_EFFECT_RADIUS,
|
||||
y + PLAYER_EFFECT_RADIUS,
|
||||
z + PLAYER_EFFECT_RADIUS);
|
||||
for (ServerPlayer player : level.players()) {
|
||||
if (player.getBoundingBox().intersects(bounds)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void applySpawnEffect(IrisEffect effect, Entity entity, ServerLevel level) {
|
||||
if (effect == null || !effect.shouldApplyNow()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SoundEvent sound = resolveSound(effect.getSoundKey());
|
||||
if (sound != null) {
|
||||
double soundX = entity.getX() + RNG.r.i(-effect.getSoundDistance(), effect.getSoundDistance());
|
||||
double soundY = entity.getY() + RNG.r.i(-effect.getSoundDistance(), effect.getSoundDistance());
|
||||
double soundZ = entity.getZ() + RNG.r.i(-effect.getSoundDistance(), effect.getSoundDistance());
|
||||
level.playSound(null, soundX, soundY, soundZ, sound, SoundSource.MASTER,
|
||||
(float) effect.getVolume(), (float) RNG.r.d(effect.getMinPitch(), effect.getMaxPitch()));
|
||||
}
|
||||
|
||||
ParticleOptions particle = resolveParticle(effect.getParticleEffectKey());
|
||||
if (particle == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 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();
|
||||
double altZ = effect.isRandomAltZ() ? RNG.r.d(-effect.getParticleAltZ(), effect.getParticleAltZ()) : effect.getParticleAltZ();
|
||||
level.sendParticles(particle, particleX, particleY, particleZ, effect.getParticleCount(),
|
||||
altX, altY, altZ, effect.getExtra());
|
||||
}
|
||||
|
||||
private static SoundEvent resolveSound(String key) {
|
||||
Identifier id = identifierFor(key);
|
||||
if (id == null || !BuiltInRegistries.SOUND_EVENT.containsKey(id)) {
|
||||
if (key != null && WARNED_EFFECT_SOUNDS.add(key)) {
|
||||
IrisLogging.warn("Iris entity effect: unknown sound '" + key + "'");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return BuiltInRegistries.SOUND_EVENT.getValue(id);
|
||||
}
|
||||
|
||||
private static ParticleOptions resolveParticle(String key) {
|
||||
Identifier id = identifierFor(key);
|
||||
if (id == null || !BuiltInRegistries.PARTICLE_TYPE.containsKey(id)) {
|
||||
if (key != null && WARNED_EFFECT_PARTICLES.add(key)) {
|
||||
IrisLogging.warn("Iris entity effect: unknown particle '" + key + "'");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
ParticleType<?> type = BuiltInRegistries.PARTICLE_TYPE.getValue(id);
|
||||
if (type instanceof SimpleParticleType simple) {
|
||||
return simple;
|
||||
}
|
||||
if (WARNED_EFFECT_PARTICLES.add(key)) {
|
||||
IrisLogging.warn("Iris entity effect: particle '" + key + "' requires data that the effect does not define");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Identifier identifierFor(String key) {
|
||||
if (key == null || key.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String normalized = key.trim().toLowerCase(Locale.ROOT);
|
||||
Identifier id = Identifier.tryParse(normalized.indexOf(':') >= 0 ? normalized : "minecraft:" + normalized);
|
||||
return Identifier.tryParse(normalized.indexOf(':') >= 0 ? normalized : "minecraft:" + normalized);
|
||||
}
|
||||
|
||||
private static void startRiseEffect(Engine engine, ServerLevel level, Entity entity, LivingEntity living) {
|
||||
RiseTask task = new RiseTask(engine, level, entity, living);
|
||||
task.start();
|
||||
}
|
||||
|
||||
static EntityType<?> resolveType(String key) {
|
||||
if (key == null || key.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String normalized = key.trim().toLowerCase(Locale.ROOT);
|
||||
Identifier id = identifierFor(normalized);
|
||||
if (id == null || !BuiltInRegistries.ENTITY_TYPE.containsKey(id)) {
|
||||
if (WARNED_TYPES.add(normalized)) {
|
||||
IrisLogging.warn("Iris entity: unknown entity type '" + key + "'; skipping spawn");
|
||||
@@ -277,23 +407,35 @@ public final class ModdedEntitySpawner {
|
||||
return BuiltInRegistries.ENTITY_TYPE.getValue(id);
|
||||
}
|
||||
|
||||
private static EntitySpawnReason reasonFor(String reason) {
|
||||
static EntitySpawnReason reasonFor(String reason) {
|
||||
if (reason == null || reason.isBlank()) {
|
||||
return EntitySpawnReason.NATURAL;
|
||||
}
|
||||
String value = reason.trim().toUpperCase(Locale.ROOT);
|
||||
String mapped = switch (value) {
|
||||
case "CHUNK_GEN" -> "CHUNK_GENERATION";
|
||||
case "SPAWNER_EGG" -> "SPAWN_EGG";
|
||||
case "DISPENSE_EGG" -> "DISPENSER";
|
||||
case "BUILD_SNOWMAN", "BUILD_IRONGOLEM", "BUILD_WITHER", "CUSTOM", "DEFAULT" -> "MOB_SUMMONED";
|
||||
default -> value;
|
||||
return switch (value) {
|
||||
case "NATURAL", "DEFAULT" -> EntitySpawnReason.NATURAL;
|
||||
case "JOCKEY", "MOUNT" -> EntitySpawnReason.JOCKEY;
|
||||
case "CHUNK_GEN" -> EntitySpawnReason.CHUNK_GENERATION;
|
||||
case "SPAWNER" -> EntitySpawnReason.SPAWNER;
|
||||
case "TRIAL_SPAWNER" -> EntitySpawnReason.TRIAL_SPAWNER;
|
||||
case "EGG", "BUILD_SNOWMAN", "BUILD_IRONGOLEM", "BUILD_COPPERGOLEM", "BUILD_WITHER",
|
||||
"SLIME_SPLIT", "SILVERFISH_BLOCK", "TRAP", "ENDER_PEARL", "EXPLOSION",
|
||||
"ENCHANTMENT", "OMINOUS_ITEM_SPAWNER", "POTION_EFFECT", "REANIMATE" -> EntitySpawnReason.TRIGGERED;
|
||||
case "SPAWNER_EGG" -> EntitySpawnReason.SPAWN_ITEM_USE;
|
||||
case "LIGHTNING", "VILLAGE_INVASION", "RAID", "CUSTOM" -> EntitySpawnReason.EVENT;
|
||||
case "VILLAGE_DEFENSE", "SPELL" -> EntitySpawnReason.MOB_SUMMONED;
|
||||
case "BREEDING", "OCELOT_BABY", "DUPLICATION", "REHYDRATION" -> EntitySpawnReason.BREEDING;
|
||||
case "REINFORCEMENTS" -> EntitySpawnReason.REINFORCEMENT;
|
||||
case "NETHER_PORTAL" -> EntitySpawnReason.STRUCTURE;
|
||||
case "DISPENSE_EGG" -> EntitySpawnReason.DISPENSER;
|
||||
case "INFECTION", "CURED", "DROWNED", "SHEARED", "PIGLIN_ZOMBIFIED", "FROZEN",
|
||||
"METAMORPHOSIS" -> EntitySpawnReason.CONVERSION;
|
||||
case "SHOULDER_ENTITY", "BEEHIVE" -> EntitySpawnReason.LOAD;
|
||||
case "PATROL" -> EntitySpawnReason.PATROL;
|
||||
case "COMMAND" -> EntitySpawnReason.COMMAND;
|
||||
case "BUCKET" -> EntitySpawnReason.BUCKET;
|
||||
default -> EntitySpawnReason.NATURAL;
|
||||
};
|
||||
try {
|
||||
return EntitySpawnReason.valueOf(mapped);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return EntitySpawnReason.NATURAL;
|
||||
}
|
||||
}
|
||||
|
||||
private static Panda.Gene gene(String value) {
|
||||
@@ -311,10 +453,13 @@ public final class ModdedEntitySpawner {
|
||||
return IrisSettings.get().getWorld().isForcePersistEntities();
|
||||
}
|
||||
|
||||
private static void noteSpawnEffectGap() {
|
||||
if (!warnedSpawnEffect) {
|
||||
warnedSpawnEffect = true;
|
||||
IrisLogging.debug("Iris entity spawnEffect / spawnEffectRiseOutOfGround are Bukkit-only visual paths and are skipped on modded.");
|
||||
private static void applyPersistence(Entity entity, boolean persistent) {
|
||||
ModdedEntityPersistence.configure(entity, persistent);
|
||||
if (persistent && (!entity.getType().canSerialize() || !entity.shouldBeSaved())) {
|
||||
String type = String.valueOf(BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()));
|
||||
if (WARNED_PERSISTENCE_TYPES.add(type)) {
|
||||
IrisLogging.warn("Iris entity: vanilla cannot persist non-serializable entity type '" + type + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,4 +479,106 @@ public final class ModdedEntitySpawner {
|
||||
}
|
||||
return new String(chars);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface ChunkLoadedLookup {
|
||||
boolean loaded(int chunkX, int chunkZ);
|
||||
}
|
||||
|
||||
private static final class RiseTask implements Runnable {
|
||||
private final Engine engine;
|
||||
private final ServerLevel level;
|
||||
private final Entity entity;
|
||||
private final LivingEntity living;
|
||||
private final double x;
|
||||
private final double z;
|
||||
private final boolean originalInvulnerable;
|
||||
private final boolean originalNoPhysics;
|
||||
private final int originalInvulnerableTime;
|
||||
private final boolean originalNoAi;
|
||||
private double y;
|
||||
private int moves;
|
||||
private boolean restored;
|
||||
|
||||
private RiseTask(Engine engine, ServerLevel level, Entity entity, LivingEntity living) {
|
||||
this.engine = engine;
|
||||
this.level = level;
|
||||
this.entity = entity;
|
||||
this.living = living;
|
||||
this.x = entity.getX();
|
||||
this.y = entity.getY();
|
||||
this.z = entity.getZ();
|
||||
this.originalInvulnerable = entity.isInvulnerable();
|
||||
this.originalNoPhysics = entity.noPhysics;
|
||||
this.originalInvulnerableTime = entity.invulnerableTime;
|
||||
this.originalNoAi = entity instanceof Mob mob && mob.isNoAi();
|
||||
}
|
||||
|
||||
private void start() {
|
||||
entity.setInvulnerable(true);
|
||||
entity.noPhysics = true;
|
||||
entity.invulnerableTime = 100_000;
|
||||
if (entity instanceof Mob mob) {
|
||||
mob.setNoAi(true);
|
||||
}
|
||||
run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (moves >= RISE_MAX_MOVES || !entity.isAlive() || engine.isClosed() || entity.level() != level) {
|
||||
restore();
|
||||
return;
|
||||
}
|
||||
BlockPos current = entity.blockPosition();
|
||||
if (level.getChunkSource().getChunkNow(current.getX() >> 4, current.getZ() >> 4) == null) {
|
||||
restore();
|
||||
return;
|
||||
}
|
||||
BlockPos eye = BlockPos.containing(entity.getX(), living.getEyeY(), entity.getZ());
|
||||
if (!ModdedBlockResolution.isSolid(level.getBlockState(current))
|
||||
&& !ModdedBlockResolution.isSolid(level.getBlockState(eye))) {
|
||||
restore();
|
||||
return;
|
||||
}
|
||||
|
||||
moves++;
|
||||
y += 0.1;
|
||||
entity.setPos(x, y, z);
|
||||
emitEffects();
|
||||
|
||||
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
|
||||
if (scheduler == null) {
|
||||
IrisLogging.error("Iris could not continue an entity rise effect because the modded scheduler is unavailable.");
|
||||
restore();
|
||||
return;
|
||||
}
|
||||
scheduler.laterGlobal(this, 1);
|
||||
}
|
||||
|
||||
private void emitEffects() {
|
||||
BlockPos source = BlockPos.containing(entity.getX(), living.getEyeY() - 2, entity.getZ());
|
||||
BlockState state = level.getBlockState(source);
|
||||
ItemParticleOption item = new ItemParticleOption(ParticleTypes.ITEM, state.getBlock().asItem());
|
||||
level.sendParticles(item, entity.getX(), living.getEyeY(), entity.getZ(), 6,
|
||||
0.2, 0.4, 0.2, 0.06);
|
||||
if (RNG.r.nextDouble() < 0.2) {
|
||||
level.playSound(null, entity.getX(), entity.getY(), entity.getZ(), SoundEvents.CHORUS_FLOWER_GROW,
|
||||
SoundSource.MASTER, 0.8F, 0.1F);
|
||||
}
|
||||
}
|
||||
|
||||
private void restore() {
|
||||
if (restored) {
|
||||
return;
|
||||
}
|
||||
restored = true;
|
||||
entity.invulnerableTime = originalInvulnerableTime;
|
||||
entity.noPhysics = originalNoPhysics;
|
||||
entity.setInvulnerable(originalInvulnerable);
|
||||
if (entity instanceof Mob mob) {
|
||||
mob.setNoAi(originalNoAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+45
-35
@@ -38,13 +38,14 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Stream;
|
||||
@@ -53,10 +54,6 @@ public final class ModdedForcedDatapack {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final String PACK_ID = "iris_worldgen";
|
||||
private static final String PACK_FOLDER = "iris";
|
||||
private static final String STUDIO_POOL_TYPE_KEY = "studio_pool";
|
||||
private static final String STUDIO_POOL_TYPE_RESOURCE = "/data/irisworldgen/dimension_type/overworld.json";
|
||||
private static final int STUDIO_POOL_MIN_Y = -256;
|
||||
private static final int STUDIO_POOL_MAX_Y = 512;
|
||||
private static final Object LOCK = new Object();
|
||||
|
||||
private ModdedForcedDatapack() {
|
||||
@@ -80,7 +77,7 @@ public final class ModdedForcedDatapack {
|
||||
}
|
||||
|
||||
private static Pack buildPack() {
|
||||
Path directory = generate();
|
||||
Path directory = regenerate();
|
||||
if (directory == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -98,7 +95,7 @@ public final class ModdedForcedDatapack {
|
||||
return pack;
|
||||
}
|
||||
|
||||
private static Path generate() {
|
||||
public static Path regenerate() {
|
||||
synchronized (LOCK) {
|
||||
try {
|
||||
return write();
|
||||
@@ -122,7 +119,7 @@ public final class ModdedForcedDatapack {
|
||||
File packFolder = packDirectory.toFile();
|
||||
KList<File> folders = new KList<>();
|
||||
folders.add(packFolder);
|
||||
KSet<String> seenBiomes = new KSet<>();
|
||||
Map<String, KSet<String>> seenBiomes = new LinkedHashMap<>();
|
||||
IDataFixer fixer = DataVersion.getLatest().get();
|
||||
|
||||
int packCount = 0;
|
||||
@@ -137,18 +134,20 @@ public final class ModdedForcedDatapack {
|
||||
}
|
||||
|
||||
writePackMeta(packDirectory);
|
||||
writeStudioPoolType(packDirectory);
|
||||
if ("forge".equalsIgnoreCase(ModdedEngineBootstrap.loader().platformName())) {
|
||||
writeForgeBlockLootModifier(packDirectory);
|
||||
}
|
||||
if (!presetIds.isEmpty()) {
|
||||
writeWorldPresetTag(packDirectory, presetIds);
|
||||
}
|
||||
LOGGER.info("Iris forced startup datapack regenerated: {} pack(s), {} world preset(s), {} custom biome(s) at {}", packCount, presetIds.size(), seenBiomes.size(), packDirectory);
|
||||
LOGGER.info("Iris forced startup datapack regenerated: {} pack(s), {} world preset(s), {} custom biome(s) at {}", packCount, presetIds.size(), countBiomes(seenBiomes), packDirectory);
|
||||
if (packCount == 0) {
|
||||
LOGGER.warn("Iris installed NO worldgen packs into the forced datapack - custom biomes and their colors will NOT generate. Install a pack (e.g. /iris download overworld) and restart the server before creating an Iris world.");
|
||||
}
|
||||
return packDirectory;
|
||||
}
|
||||
|
||||
private static boolean installPack(File packFolder, IDataFixer fixer, KList<File> folders, KSet<String> seenBiomes, KList<String> presetIds) {
|
||||
private static boolean installPack(File packFolder, IDataFixer fixer, KList<File> folders, Map<String, KSet<String>> seenBiomes, KList<String> presetIds) {
|
||||
String packName = packFolder.getName();
|
||||
File[] dimensionFiles = new File(packFolder, "dimensions").listFiles((File file) -> file.isFile() && file.getName().endsWith(".json"));
|
||||
if (dimensionFiles == null || dimensionFiles.length == 0) {
|
||||
@@ -163,7 +162,7 @@ public final class ModdedForcedDatapack {
|
||||
if (dimension == null) {
|
||||
continue;
|
||||
}
|
||||
dimension.installBiomes(fixer, () -> data, folders, seenBiomes);
|
||||
dimension.installBiomes(fixer, () -> data, folders, biomesForNamespace(seenBiomes, dimension.getLoadKey()));
|
||||
writeDimensionType(folders, fixer, dimension);
|
||||
String presetKey = dimensionKey.equals(packName) ? packName : packName + "_" + dimensionKey;
|
||||
writeWorldPreset(folders, dimension, packName, dimensionKey, presetKey);
|
||||
@@ -176,10 +175,19 @@ public final class ModdedForcedDatapack {
|
||||
return installed;
|
||||
}
|
||||
|
||||
static KSet<String> biomesForNamespace(Map<String, KSet<String>> biomes, String namespace) {
|
||||
return biomes.computeIfAbsent(namespace, ignored -> new KSet<>());
|
||||
}
|
||||
|
||||
public static String dimensionTypeRef(IrisDimension dimension) {
|
||||
return fitsStudioPool(dimension)
|
||||
? "irisworldgen:" + STUDIO_POOL_TYPE_KEY
|
||||
: "irisworldgen:" + dimension.getDimensionTypeKey();
|
||||
return "irisworldgen:" + dimension.getDimensionTypeKey();
|
||||
}
|
||||
|
||||
static <T> T requireRegisteredDimensionType(String typeRef, Optional<T> registeredType,
|
||||
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."));
|
||||
}
|
||||
|
||||
private static void writeWorldPreset(KList<File> folders, IrisDimension dimension, String packName, String dimensionKey, String presetKey) throws IOException {
|
||||
@@ -250,10 +258,7 @@ public final class ModdedForcedDatapack {
|
||||
Files.writeString(output, json, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static void writeDimensionType(KList<File> folders, IDataFixer fixer, IrisDimension dimension) throws IOException {
|
||||
if (fitsStudioPool(dimension)) {
|
||||
return;
|
||||
}
|
||||
static void writeDimensionType(KList<File> folders, IDataFixer fixer, IrisDimension dimension) throws IOException {
|
||||
IrisDimensionType type = dimension.getDimensionType();
|
||||
String json = type.toJson(fixer);
|
||||
String typeKey = dimension.getDimensionTypeKey();
|
||||
@@ -264,23 +269,20 @@ public final class ModdedForcedDatapack {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean fitsStudioPool(IrisDimension dimension) {
|
||||
return dimension.getMinHeight() >= STUDIO_POOL_MIN_Y && dimension.getMaxHeight() <= STUDIO_POOL_MAX_Y;
|
||||
}
|
||||
static void writeForgeBlockLootModifier(Path packDirectory) throws IOException {
|
||||
Path list = packDirectory.resolve("data").resolve("forge").resolve("loot_modifiers").resolve("global_loot_modifiers.json");
|
||||
Files.createDirectories(list.getParent());
|
||||
Files.writeString(list, "{\n"
|
||||
+ " \"replace\": false,\n"
|
||||
+ " \"entries\": [\"irisworldgen:block_drops\"]\n"
|
||||
+ "}\n", StandardCharsets.UTF_8);
|
||||
|
||||
private static void writeStudioPoolType(Path packDirectory) throws IOException {
|
||||
Path output = packDirectory.resolve("data").resolve("irisworldgen").resolve("dimension_type").resolve(STUDIO_POOL_TYPE_KEY + ".json");
|
||||
Files.createDirectories(output.getParent());
|
||||
Files.writeString(output, readStudioPoolType(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static String readStudioPoolType() throws IOException {
|
||||
try (InputStream stream = ModdedForcedDatapack.class.getResourceAsStream(STUDIO_POOL_TYPE_RESOURCE)) {
|
||||
if (stream == null) {
|
||||
throw new IOException("Bundled studio pool dimension type resource is missing: " + STUDIO_POOL_TYPE_RESOURCE);
|
||||
}
|
||||
return new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
Path modifier = packDirectory.resolve("data").resolve("irisworldgen").resolve("loot_modifiers").resolve("block_drops.json");
|
||||
Files.createDirectories(modifier.getParent());
|
||||
Files.writeString(modifier, "{\n"
|
||||
+ " \"type\": \"irisworldgen:block_drops\",\n"
|
||||
+ " \"conditions\": []\n"
|
||||
+ "}\n", StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static void writePackMeta(Path packDirectory) throws IOException {
|
||||
@@ -296,6 +298,14 @@ public final class ModdedForcedDatapack {
|
||||
Files.writeString(packDirectory.resolve("pack.mcmeta"), json, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static int countBiomes(Map<String, KSet<String>> biomes) {
|
||||
int count = 0;
|
||||
for (KSet<String> values : biomes.values()) {
|
||||
count += values.size();
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static void clean(Path packDirectory) throws IOException {
|
||||
if (!Files.exists(packDirectory)) {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.util.SimpleBitStorage;
|
||||
|
||||
import java.util.function.IntBinaryOperator;
|
||||
|
||||
final class ModdedHeightmaps {
|
||||
private ModdedHeightmaps() {
|
||||
}
|
||||
|
||||
static long[] terrainRawData(int height, IntBinaryOperator heightResolver) {
|
||||
SimpleBitStorage storage = new SimpleBitStorage(Mth.ceillog2(height + 1), 256);
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
int value = Mth.clamp(heightResolver.applyAsInt(x, z), 0, height);
|
||||
storage.set(x + z * 16, value);
|
||||
}
|
||||
}
|
||||
return storage.getRaw();
|
||||
}
|
||||
}
|
||||
+125
-69
@@ -23,6 +23,8 @@ import art.arcane.iris.engine.framework.PlacedObject;
|
||||
import art.arcane.iris.engine.object.IObjectLoot;
|
||||
import art.arcane.iris.engine.object.InventorySlotType;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisLootMode;
|
||||
import art.arcane.iris.engine.object.IrisLootReference;
|
||||
import art.arcane.iris.engine.object.IrisLootTable;
|
||||
import art.arcane.iris.engine.object.IrisObjectLoot;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
@@ -43,41 +45,24 @@ import net.minecraft.world.Container;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.ChestBlock;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.storage.loot.LootParams;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets;
|
||||
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public final class ModdedLootApplier {
|
||||
private ModdedLootApplier() {
|
||||
}
|
||||
|
||||
private record Candidate(IrisLootTable table, Identifier vanillaId, int weight) {
|
||||
}
|
||||
|
||||
private record Resolution(KList<IrisLootTable> tables, Identifier vanillaTable) {
|
||||
boolean isEmpty() {
|
||||
return tables.isEmpty() && vanillaTable == null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void apply(Engine engine, ServerLevel level, BlockPos pos, BlockState state, MantleChunk<Matter> mc, RNG rng, int localX, int localZ) {
|
||||
Resolution resolution = resolveTables(engine, rng, pos, state, mc);
|
||||
if (resolution.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (resolution.vanillaTable() != null) {
|
||||
BlockEntity blockEntity = level.getBlockEntity(pos);
|
||||
if (blockEntity instanceof RandomizableContainerBlockEntity randomizable) {
|
||||
randomizable.setLootTable(ResourceKey.create(Registries.LOOT_TABLE, resolution.vanillaTable()), rng.nextLong());
|
||||
} else {
|
||||
IrisLogging.debug("Iris loot: no randomizable container at " + pos + " for vanilla table " + resolution.vanillaTable());
|
||||
}
|
||||
}
|
||||
|
||||
if (resolution.tables().isEmpty()) {
|
||||
List<LootSource> sources = resolveSources(engine, level, rng, pos, state, mc);
|
||||
if (sources.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -88,40 +73,43 @@ public final class ModdedLootApplier {
|
||||
}
|
||||
|
||||
KList<ItemStack> items = new KList<>();
|
||||
for (IrisLootTable table : resolution.tables()) {
|
||||
if (table == null) {
|
||||
LootParams nativeParams = null;
|
||||
for (LootSource source : sources) {
|
||||
if (source instanceof IrisLootSource irisSource) {
|
||||
IrisLootTable table = irisSource.table();
|
||||
if (table == null) {
|
||||
continue;
|
||||
}
|
||||
items.addAll(ModdedItemTranslator.loot(table, rng, InventorySlotType.STORAGE, level, localX, pos.getY(), localZ));
|
||||
continue;
|
||||
}
|
||||
items.addAll(ModdedItemTranslator.loot(table, rng, InventorySlotType.STORAGE, level, localX, pos.getY(), localZ));
|
||||
if (source instanceof NativeLootSource nativeSource) {
|
||||
if (nativeParams == null) {
|
||||
nativeParams = new LootParams.Builder(level)
|
||||
.withParameter(LootContextParams.ORIGIN, Vec3.atCenterOf(pos))
|
||||
.create(LootContextParamSets.CHEST);
|
||||
}
|
||||
items.addAll(nativeSource.table().getRandomItems(nativeParams, rng.nextLong()));
|
||||
}
|
||||
}
|
||||
|
||||
for (ItemStack item : items) {
|
||||
addItem(container, item);
|
||||
}
|
||||
|
||||
scramble(container, rng);
|
||||
container.setChanged();
|
||||
fillContainer(container, items, rng);
|
||||
}
|
||||
|
||||
private static Resolution resolveTables(Engine engine, RNG rng, BlockPos pos, BlockState state, MantleChunk<Matter> mc) {
|
||||
private static List<LootSource> resolveSources(Engine engine, ServerLevel level, RNG rng, BlockPos pos, BlockState state, MantleChunk<Matter> mc) {
|
||||
int rx = pos.getX();
|
||||
int rz = pos.getZ();
|
||||
int ry = pos.getY() - engine.getWorld().minHeight();
|
||||
double he = engine.getComplex().getHeightStream().get(rx, rz);
|
||||
KList<IrisLootTable> tables = new KList<>();
|
||||
Identifier vanillaTable = null;
|
||||
List<LootSource> sources = new ArrayList<>();
|
||||
|
||||
PlacedObject po = engine.getObjectPlacement(rx, ry, rz, mc);
|
||||
if (po != null && po.getPlacement() != null && ModdedBlockResolution.isStorageChest(state)) {
|
||||
Candidate picked = pickPlacementTable(engine, po.getPlacement(), state, rng);
|
||||
Candidate picked = pickPlacementTable(engine, level, po.getPlacement(), state, rng);
|
||||
if (picked != null) {
|
||||
if (picked.table() != null) {
|
||||
tables.add(picked.table());
|
||||
} else {
|
||||
vanillaTable = picked.vanillaId();
|
||||
}
|
||||
sources.add(picked.source());
|
||||
if (po.getPlacement().isOverrideGlobalLoot()) {
|
||||
return new Resolution(tables, vanillaTable);
|
||||
return sources;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,28 +119,16 @@ public final class ModdedLootApplier {
|
||||
IrisBiome biomeUnder = ry < he ? engine.getCaveBiome(rx, ry, rz) : biomeSurface;
|
||||
|
||||
double multiplier = 1D * engine.getDimension().getLoot().getMultiplier() * region.getLoot().getMultiplier() * biomeSurface.getLoot().getMultiplier() * biomeUnder.getLoot().getMultiplier();
|
||||
boolean fallback = tables.isEmpty() && vanillaTable == null;
|
||||
engine.injectTables(tables, engine.getDimension().getLoot(), fallback);
|
||||
engine.injectTables(tables, region.getLoot(), fallback);
|
||||
engine.injectTables(tables, biomeSurface.getLoot(), fallback);
|
||||
engine.injectTables(tables, biomeUnder.getLoot(), fallback);
|
||||
|
||||
if (tables.isNotEmpty()) {
|
||||
int target = (int) Math.round(tables.size() * multiplier);
|
||||
|
||||
while (tables.size() < target && tables.isNotEmpty()) {
|
||||
tables.add(tables.get(rng.i(tables.size() - 1)));
|
||||
}
|
||||
|
||||
while (tables.size() > target && tables.isNotEmpty()) {
|
||||
tables.remove(rng.i(tables.size() - 1));
|
||||
}
|
||||
}
|
||||
|
||||
return new Resolution(tables, vanillaTable);
|
||||
boolean fallback = sources.isEmpty();
|
||||
injectReferenceSources(sources, engine.getDimension().getLoot(), engine, fallback);
|
||||
injectReferenceSources(sources, region.getLoot(), engine, fallback);
|
||||
injectReferenceSources(sources, biomeSurface.getLoot(), engine, fallback);
|
||||
injectReferenceSources(sources, biomeUnder.getLoot(), engine, fallback);
|
||||
scaleSources(sources, multiplier, rng);
|
||||
return sources;
|
||||
}
|
||||
|
||||
private static Candidate pickPlacementTable(Engine engine, IrisObjectPlacement placement, BlockState state, RNG rng) {
|
||||
private static Candidate pickPlacementTable(Engine engine, ServerLevel level, IrisObjectPlacement placement, BlockState state, RNG rng) {
|
||||
List<Candidate> exact = new ArrayList<>();
|
||||
List<Candidate> basic = new ArrayList<>();
|
||||
List<Candidate> global = new ArrayList<>();
|
||||
@@ -166,25 +142,93 @@ public final class ModdedLootApplier {
|
||||
IrisLogging.warn("Couldn't find loot table " + loot.getName());
|
||||
continue;
|
||||
}
|
||||
bucket(engine, loot, new Candidate(table, null, loot.getWeight()), state, exact, basic, global);
|
||||
bucket(engine, loot, new Candidate(new IrisLootSource(table), loot.getWeight()), state, exact, basic, global);
|
||||
}
|
||||
|
||||
for (IrisObjectVanillaLoot loot : placement.getVanillaLoot()) {
|
||||
if (loot == null) {
|
||||
continue;
|
||||
}
|
||||
Identifier id = loot.getName() == null ? null : Identifier.tryParse(loot.getName());
|
||||
if (id == null) {
|
||||
IrisLogging.warn("Couldn't parse vanilla loot table " + loot.getName());
|
||||
ResourceKey<LootTable> key = resolveNativeKey(loot.getName(), candidate -> hasNativeTable(level, candidate));
|
||||
if (key == null) {
|
||||
IrisLogging.warn("Couldn't find vanilla loot table " + loot.getName());
|
||||
continue;
|
||||
}
|
||||
bucket(engine, loot, new Candidate(null, id, loot.getWeight()), state, exact, basic, global);
|
||||
LootTable table = level.getServer().reloadableRegistries().lookup()
|
||||
.lookupOrThrow(Registries.LOOT_TABLE)
|
||||
.getOrThrow(key)
|
||||
.value();
|
||||
bucket(engine, loot, new Candidate(new NativeLootSource(table), loot.getWeight()), state, exact, basic, global);
|
||||
}
|
||||
|
||||
List<Candidate> pool = !exact.isEmpty() ? exact : !basic.isEmpty() ? basic : global;
|
||||
return pickWeighted(pool, rng);
|
||||
}
|
||||
|
||||
static <T> void injectSources(List<T> sources, List<? extends T> additions, IrisLootMode mode, boolean fallback) {
|
||||
if (mode == IrisLootMode.FALLBACK && !fallback) {
|
||||
return;
|
||||
}
|
||||
if (mode == IrisLootMode.CLEAR || mode == IrisLootMode.REPLACE) {
|
||||
sources.clear();
|
||||
}
|
||||
sources.addAll(additions);
|
||||
}
|
||||
|
||||
static <T> void scaleSources(List<T> sources, double multiplier, RNG rng) {
|
||||
if (sources.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
int target = (int) Math.round(sources.size() * multiplier);
|
||||
while (sources.size() < target && !sources.isEmpty()) {
|
||||
sources.add(sources.get(rng.i(sources.size() - 1)));
|
||||
}
|
||||
while (sources.size() > target && !sources.isEmpty()) {
|
||||
sources.remove(rng.i(sources.size() - 1));
|
||||
}
|
||||
}
|
||||
|
||||
static ResourceKey<LootTable> resolveNativeKey(String name, Predicate<ResourceKey<LootTable>> registryContains) {
|
||||
Identifier id = name == null ? null : Identifier.tryParse(name);
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
ResourceKey<LootTable> key = ResourceKey.create(Registries.LOOT_TABLE, id);
|
||||
return registryContains.test(key) ? key : null;
|
||||
}
|
||||
|
||||
static void fillContainer(Container container, List<ItemStack> items, RNG rng) {
|
||||
for (ItemStack item : items) {
|
||||
addItem(container, item);
|
||||
}
|
||||
scramble(container, rng);
|
||||
container.setChanged();
|
||||
}
|
||||
|
||||
private static void injectReferenceSources(List<LootSource> sources, IrisLootReference reference, Engine engine, boolean fallback) {
|
||||
IrisLootMode mode = reference.getMode();
|
||||
if (mode == IrisLootMode.FALLBACK && !fallback) {
|
||||
return;
|
||||
}
|
||||
injectSources(sources, irisSources(reference, engine), mode, fallback);
|
||||
}
|
||||
|
||||
private static List<LootSource> irisSources(IrisLootReference reference, Engine engine) {
|
||||
KList<IrisLootTable> tables = reference.getLootTables(engine.getComplex());
|
||||
List<LootSource> sources = new ArrayList<>(tables.size());
|
||||
for (IrisLootTable table : tables) {
|
||||
sources.add(new IrisLootSource(table));
|
||||
}
|
||||
return sources;
|
||||
}
|
||||
|
||||
private static boolean hasNativeTable(ServerLevel level, ResourceKey<LootTable> key) {
|
||||
return level.getServer().reloadableRegistries().lookup()
|
||||
.lookup(Registries.LOOT_TABLE)
|
||||
.flatMap(registry -> registry.get(key))
|
||||
.isPresent();
|
||||
}
|
||||
|
||||
private static void bucket(Engine engine, IObjectLoot loot, Candidate candidate, BlockState state, List<Candidate> exact, List<Candidate> basic, List<Candidate> global) {
|
||||
if (loot.getFilter().isEmpty()) {
|
||||
global.add(candidate);
|
||||
@@ -288,4 +332,16 @@ public final class ModdedLootApplier {
|
||||
container.setItem(i, items[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private interface LootSource {
|
||||
}
|
||||
|
||||
private record IrisLootSource(IrisLootTable table) implements LootSource {
|
||||
}
|
||||
|
||||
private record NativeLootSource(LootTable table) implements LootSource {
|
||||
}
|
||||
|
||||
private record Candidate(LootSource source, int weight) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* 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.spi.PlatformBiome;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.PlatformWorld;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
public final class ModdedPlatformWorld implements PlatformWorld {
|
||||
private final ServerLevel level;
|
||||
|
||||
public ModdedPlatformWorld(ServerLevel level) {
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return level.dimension().identifier().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long seed() {
|
||||
return level.getSeed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int minHeight() {
|
||||
return level.getMinY();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int maxHeight() {
|
||||
return exclusiveMaxHeight(level.getMinY(), level.getHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlockState getBlock(int x, int y, int z) {
|
||||
return ModdedBlockState.of(level.getBlockState(new BlockPos(x, y, z)), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBlock(int x, int y, int z, PlatformBlockState block, int flags) {
|
||||
level.setBlock(new BlockPos(x, y, z), (BlockState) block.nativeHandle(), flags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBiome getBiome(int x, int y, int z) {
|
||||
Holder<Biome> biome = level.getBiome(new BlockPos(x, y, z));
|
||||
String key = biome.unwrapKey()
|
||||
.map(resourceKey -> resourceKey.identifier().toString())
|
||||
.orElse("minecraft:plains");
|
||||
return ModdedBiome.of(biome.value(), key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChunkLoaded(int chunkX, int chunkZ) {
|
||||
return level.getChunkSource().getChunkNow(chunkX, chunkZ) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTime() {
|
||||
return level.getDefaultClockTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStorming() {
|
||||
return level.isRaining();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isThundering() {
|
||||
return level.isThundering();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object nativeHandle() {
|
||||
return level;
|
||||
}
|
||||
|
||||
ServerLevel level() {
|
||||
return level;
|
||||
}
|
||||
|
||||
static int exclusiveMaxHeight(int minHeight, int height) {
|
||||
return minHeight + height;
|
||||
}
|
||||
}
|
||||
@@ -30,23 +30,24 @@ public final class ModdedStateMerger implements BlockDataMergeSupport.StateMerge
|
||||
public PlatformBlockState merge(PlatformBlockState base, PlatformBlockState update) {
|
||||
ModdedBlockState fabricBase = (ModdedBlockState) base;
|
||||
ModdedBlockState fabricUpdate = (ModdedBlockState) update;
|
||||
ModdedBlockState metadataSource = fabricUpdate.isCustom() ? fabricUpdate : fabricBase;
|
||||
|
||||
try {
|
||||
return ModdedBlockState.of(mergeStates(fabricBase.handle(), fabricUpdate.handle(), fabricUpdate.parsedProperties()), null);
|
||||
return metadataSource.withHandle(mergeStates(fabricBase.handle(), fabricUpdate.handle(), fabricUpdate.parsedProperties()), null);
|
||||
} catch (IllegalArgumentException e) {
|
||||
ModdedBlockResolution.Parsed normalizedBase = ModdedBlockResolution.resolveGet(ModdedBlockState.serialize(fabricBase.handle()));
|
||||
ModdedBlockResolution.Parsed normalizedUpdate = ModdedBlockResolution.resolveGet(ModdedBlockState.serialize(fabricUpdate.handle()));
|
||||
|
||||
if (normalizedBase != null && normalizedUpdate != null) {
|
||||
try {
|
||||
return ModdedBlockState.of(mergeStates(normalizedBase.state(), normalizedUpdate.state(), normalizedUpdate.properties()), null);
|
||||
return metadataSource.withHandle(mergeStates(normalizedBase.state(), normalizedUpdate.state(), normalizedUpdate.properties()), null);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return ModdedBlockState.of(normalizedUpdate.state(), normalizedUpdate.properties());
|
||||
return metadataSource.withHandle(normalizedUpdate.state(), normalizedUpdate.properties());
|
||||
}
|
||||
}
|
||||
|
||||
if (normalizedUpdate != null) {
|
||||
return ModdedBlockState.of(normalizedUpdate.state(), normalizedUpdate.properties());
|
||||
return metadataSource.withHandle(normalizedUpdate.state(), normalizedUpdate.properties());
|
||||
}
|
||||
|
||||
return update;
|
||||
|
||||
@@ -168,7 +168,7 @@ public final class ModdedStateRotator implements IrisObjectRotation.StateRotator
|
||||
return null;
|
||||
}
|
||||
|
||||
return d == fabric.handle() ? state : ModdedBlockState.of(d, fabric.parsedProperties());
|
||||
return d == fabric.handle() ? state : fabric.withHandle(d, fabric.parsedProperties());
|
||||
}
|
||||
|
||||
private static Property<?> findFacing(BlockState state) {
|
||||
|
||||
+241
-28
@@ -18,18 +18,41 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||
import art.arcane.iris.spi.PlatformWorld;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.Registry;
|
||||
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;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.WorldgenRandom;
|
||||
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
|
||||
import net.minecraft.world.level.levelgen.feature.AbstractHugeMushroomFeature;
|
||||
import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
|
||||
import net.minecraft.world.level.levelgen.feature.FallenTreeFeature;
|
||||
import net.minecraft.world.level.levelgen.feature.Feature;
|
||||
import net.minecraft.world.level.levelgen.feature.TreeFeature;
|
||||
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.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public final class ModdedStructureHooks implements PlatformStructureHooks {
|
||||
@@ -52,64 +75,254 @@ public final class ModdedStructureHooks implements PlatformStructureHooks {
|
||||
@Override
|
||||
public List<String> structureBiomeKeys(String structureKey) {
|
||||
List<String> keys = new ArrayList<>();
|
||||
MinecraftServer instance = server.get();
|
||||
if (instance == null) {
|
||||
return keys;
|
||||
}
|
||||
Identifier identifier = Identifier.tryParse(structureKey);
|
||||
if (identifier == null) {
|
||||
return keys;
|
||||
}
|
||||
Registry<Structure> registry = instance.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
Structure structure = registry.getValue(identifier);
|
||||
if (structure == null) {
|
||||
return keys;
|
||||
}
|
||||
for (Holder<Biome> holder : structure.biomes()) {
|
||||
holder.unwrapKey().ifPresent((net.minecraft.resources.ResourceKey<Biome> key) -> keys.add(key.identifier().toString()));
|
||||
try {
|
||||
MinecraftServer instance = server.get();
|
||||
if (instance == null) {
|
||||
return keys;
|
||||
}
|
||||
Identifier identifier = Identifier.tryParse(structureKey);
|
||||
if (identifier == null) {
|
||||
return keys;
|
||||
}
|
||||
Registry<Structure> registry = instance.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
Structure structure = registry.getValue(identifier);
|
||||
if (structure == null) {
|
||||
return keys;
|
||||
}
|
||||
for (Holder<Biome> holder : structure.biomes()) {
|
||||
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
|
||||
if (key.isPresent()) {
|
||||
keys.add(key.get().identifier().toString());
|
||||
}
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError(error);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> objectFeatureKeys() {
|
||||
return registryKeys(Registries.CONFIGURED_FEATURE);
|
||||
List<String> keys = new ArrayList<>();
|
||||
try {
|
||||
MinecraftServer instance = server.get();
|
||||
if (instance == null) {
|
||||
return keys;
|
||||
}
|
||||
Registry<ConfiguredFeature<?, ?>> registry = instance.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE);
|
||||
for (Identifier identifier : registry.keySet()) {
|
||||
ConfiguredFeature<?, ?> configuredFeature = registry.getValue(identifier);
|
||||
if (configuredFeature == null) {
|
||||
continue;
|
||||
}
|
||||
String group = classifyFeature(configuredFeature.feature());
|
||||
if (group != null) {
|
||||
keys.add(group + "|" + identifier);
|
||||
}
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError(error);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> reachableStructureKeys(PlatformWorld world) {
|
||||
return List.of();
|
||||
List<String> keys = new ArrayList<>();
|
||||
try {
|
||||
ServerLevel level = level(world);
|
||||
if (level == null) {
|
||||
return keys;
|
||||
}
|
||||
BiomeSource source = level.getChunkSource().getGenerator().getBiomeSource();
|
||||
Set<String> possibleBiomes = possibleBiomeKeys(source);
|
||||
if (possibleBiomes.isEmpty()) {
|
||||
return keys;
|
||||
}
|
||||
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
for (Map.Entry<ResourceKey<Structure>, Structure> entry : registry.entrySet()) {
|
||||
if (hasPossibleBiome(entry.getValue(), possibleBiomes)) {
|
||||
keys.add(entry.getKey().identifier().toString());
|
||||
}
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError(error);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> possibleBiomeKeys(PlatformWorld world) {
|
||||
return List.of();
|
||||
List<String> keys = new ArrayList<>();
|
||||
try {
|
||||
ServerLevel level = level(world);
|
||||
if (level == null) {
|
||||
return keys;
|
||||
}
|
||||
keys.addAll(possibleBiomeKeys(level.getChunkSource().getGenerator().getBiomeSource()));
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError(error);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean placeFeature(PlatformWorld world, int x, int y, int z, String featureKey, long seed) {
|
||||
return false;
|
||||
try {
|
||||
ServerLevel level = level(world);
|
||||
Identifier identifier = Identifier.tryParse(featureKey);
|
||||
if (level == null || identifier == null) {
|
||||
return false;
|
||||
}
|
||||
ChunkGenerator generator = level.getChunkSource().getGenerator();
|
||||
Registry<ConfiguredFeature<?, ?>> registry = level.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE);
|
||||
ConfiguredFeature<?, ?> configuredFeature = registry.getValue(identifier);
|
||||
if (configuredFeature == null) {
|
||||
return false;
|
||||
}
|
||||
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(seed));
|
||||
return configuredFeature.place(level, generator, random, new BlockPos(x, y, z));
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] placeStructure(PlatformWorld world, int chunkX, int chunkZ, String structureKey, long seed, int maxSpan) {
|
||||
return null;
|
||||
try {
|
||||
ServerLevel level = level(world);
|
||||
Identifier identifier = Identifier.tryParse(structureKey);
|
||||
if (level == null || identifier == null) {
|
||||
return null;
|
||||
}
|
||||
ChunkGenerator generator = level.getChunkSource().getGenerator();
|
||||
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
Structure structure = registry.getValue(identifier);
|
||||
if (structure == null) {
|
||||
return null;
|
||||
}
|
||||
Holder<Structure> holder = registry.wrapAsHolder(structure);
|
||||
RandomState randomState = level.getChunkSource().randomState();
|
||||
StructureTemplateManager templateManager = level.getStructureManager();
|
||||
StructureManager structureManager = level.structureManager();
|
||||
BiomeSource biomeSource = generator.getBiomeSource();
|
||||
ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
|
||||
StructureStart start = structure.generate(
|
||||
holder,
|
||||
level.dimension(),
|
||||
level.registryAccess(),
|
||||
generator,
|
||||
biomeSource,
|
||||
randomState,
|
||||
templateManager,
|
||||
seed,
|
||||
chunkPos,
|
||||
0,
|
||||
level,
|
||||
biome -> true);
|
||||
if (start == null || !start.isValid()) {
|
||||
return null;
|
||||
}
|
||||
BoundingBox box = start.getBoundingBox();
|
||||
if (!isWithinSpan(box, maxSpan)) {
|
||||
return null;
|
||||
}
|
||||
placeChunks(level, structureManager, generator, start, box, seed);
|
||||
return bounds(box);
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsStructurePlacement() {
|
||||
return true;
|
||||
}
|
||||
|
||||
static String classifyFeature(Feature<?> feature) {
|
||||
if (feature instanceof TreeFeature) {
|
||||
return "trees";
|
||||
}
|
||||
if (feature instanceof FallenTreeFeature) {
|
||||
return "fallen_trees";
|
||||
}
|
||||
if (feature instanceof AbstractHugeMushroomFeature) {
|
||||
return "mushrooms";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static boolean isWithinSpan(BoundingBox box, int maxSpan) {
|
||||
return maxSpan <= 0
|
||||
|| box.getXSpan() <= maxSpan && box.getYSpan() <= maxSpan && box.getZSpan() <= maxSpan;
|
||||
}
|
||||
|
||||
static int[] bounds(BoundingBox box) {
|
||||
return new int[]{box.minX(), box.minY(), box.minZ(), box.maxX(), box.maxY(), box.maxZ()};
|
||||
}
|
||||
|
||||
private static Set<String> possibleBiomeKeys(BiomeSource source) {
|
||||
Set<String> keys = new LinkedHashSet<>();
|
||||
if (source == null) {
|
||||
return keys;
|
||||
}
|
||||
for (Holder<Biome> holder : source.possibleBiomes()) {
|
||||
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
|
||||
if (key.isPresent()) {
|
||||
keys.add(key.get().identifier().toString());
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
private static boolean hasPossibleBiome(Structure structure, Set<String> possibleBiomeKeys) {
|
||||
for (Holder<Biome> holder : structure.biomes()) {
|
||||
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
|
||||
if (key.isPresent() && possibleBiomeKeys.contains(key.get().identifier().toString())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private <T> List<String> registryKeys(net.minecraft.resources.ResourceKey<net.minecraft.core.Registry<T>> registryKey) {
|
||||
List<String> keys = new ArrayList<>();
|
||||
MinecraftServer instance = server.get();
|
||||
if (instance == null) {
|
||||
return keys;
|
||||
private static ServerLevel level(PlatformWorld world) {
|
||||
return world instanceof ModdedPlatformWorld moddedWorld ? moddedWorld.level() : null;
|
||||
}
|
||||
|
||||
private static void placeChunks(ServerLevel level, StructureManager structureManager, ChunkGenerator generator,
|
||||
StructureStart start, BoundingBox box, long seed) {
|
||||
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(seed));
|
||||
int minChunkX = box.minX() >> 4;
|
||||
int maxChunkX = box.maxX() >> 4;
|
||||
int minChunkZ = box.minZ() >> 4;
|
||||
int maxChunkZ = box.maxZ() >> 4;
|
||||
for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) {
|
||||
for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) {
|
||||
level.getChunk(chunkX, chunkZ);
|
||||
ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
|
||||
BoundingBox chunkBox = new BoundingBox(
|
||||
chunkPos.getMinBlockX(), box.minY(), chunkPos.getMinBlockZ(),
|
||||
chunkPos.getMaxBlockX(), box.maxY(), chunkPos.getMaxBlockZ());
|
||||
start.placeInChunk(level, structureManager, generator, random, chunkBox, chunkPos);
|
||||
}
|
||||
}
|
||||
Registry<T> registry = instance.registryAccess().lookupOrThrow(registryKey);
|
||||
for (Identifier identifier : registry.keySet()) {
|
||||
keys.add(identifier.toString());
|
||||
}
|
||||
|
||||
private <T> List<String> registryKeys(ResourceKey<Registry<T>> registryKey) {
|
||||
List<String> keys = new ArrayList<>();
|
||||
try {
|
||||
MinecraftServer instance = server.get();
|
||||
if (instance == null) {
|
||||
return keys;
|
||||
}
|
||||
Registry<T> registry = instance.registryAccess().lookupOrThrow(registryKey);
|
||||
for (Identifier identifier : registry.keySet()) {
|
||||
keys.add(identifier.toString());
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError(error);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
@@ -19,26 +19,58 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.engine.object.TileData;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.Strictness;
|
||||
import net.minecraft.nbt.ByteTag;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.DoubleTag;
|
||||
import net.minecraft.nbt.FloatTag;
|
||||
import net.minecraft.nbt.IntTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.nbt.LongTag;
|
||||
import net.minecraft.nbt.NbtUtils;
|
||||
import net.minecraft.nbt.ShortTag;
|
||||
import net.minecraft.nbt.StringTag;
|
||||
import net.minecraft.nbt.Tag;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BannerBlockEntity;
|
||||
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
|
||||
import net.minecraft.world.level.block.entity.SignBlockEntity;
|
||||
import net.minecraft.world.level.block.entity.SpawnerBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.Property;
|
||||
import net.minecraft.world.level.storage.TagValueInput;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class ModdedTileData extends TileData {
|
||||
public static final String NBT_PROPERTY = "nbt";
|
||||
static final String LEGACY_BANNER_COLOR_PROPERTY = "iris:legacy_banner_color";
|
||||
private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().setStrictness(Strictness.LENIENT).create();
|
||||
|
||||
private final byte[] raw;
|
||||
private final KMap<String, Object> tileProperties;
|
||||
private final String expectedBlockKey;
|
||||
private final int legacyType;
|
||||
|
||||
ModdedTileData(byte[] raw, KMap<String, Object> tileProperties) {
|
||||
ModdedTileData(byte[] raw, KMap<String, Object> tileProperties, String expectedBlockKey, int legacyType) {
|
||||
super();
|
||||
this.raw = raw;
|
||||
this.tileProperties = tileProperties == null ? new KMap<>() : tileProperties;
|
||||
this.expectedBlockKey = expectedBlockKey;
|
||||
this.legacyType = legacyType;
|
||||
}
|
||||
|
||||
public static ModdedTileData capture(String blockKey, String snbt) throws IOException {
|
||||
@@ -49,7 +81,26 @@ public final class ModdedTileData extends TileData {
|
||||
out.writeUTF(blockKey);
|
||||
out.writeUTF(GSON.toJson(properties));
|
||||
}
|
||||
return new ModdedTileData(bytes.toByteArray(), properties);
|
||||
return new ModdedTileData(bytes.toByteArray(), properties, normalizeBlockKey(blockKey), -1);
|
||||
}
|
||||
|
||||
public static ModdedTileData fromProperties(PlatformBlockState state, KMap<String, Object> properties) {
|
||||
String blockKey = state.placementBaseState().key();
|
||||
int bracket = blockKey.indexOf('[');
|
||||
if (bracket >= 0) {
|
||||
blockKey = blockKey.substring(0, bracket);
|
||||
}
|
||||
KMap<String, Object> copied = properties == null ? new KMap<>() : properties.copy();
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
try (DataOutputStream out = new DataOutputStream(bytes)) {
|
||||
out.writeUTF(blockKey);
|
||||
out.writeUTF(GSON.toJson(copied));
|
||||
}
|
||||
return new ModdedTileData(bytes.toByteArray(), copied, normalizeBlockKey(blockKey), -1);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Failed to encode modded tile data for " + blockKey, e);
|
||||
}
|
||||
}
|
||||
|
||||
public String snbt() {
|
||||
@@ -57,6 +108,136 @@ public final class ModdedTileData extends TileData {
|
||||
return value == null ? null : value.toString();
|
||||
}
|
||||
|
||||
public boolean apply(BlockEntity blockEntity, ServerLevel level) throws Exception {
|
||||
CompoundTag payload = payload();
|
||||
if (payload == null) {
|
||||
return false;
|
||||
}
|
||||
CompoundTag merged = blockEntity.saveWithoutMetadata(level.registryAccess());
|
||||
merged.merge(payload);
|
||||
blockEntity.loadWithComponents(TagValueInput.create(ProblemReporter.DISCARDING, level.registryAccess(), merged));
|
||||
blockEntity.setChanged();
|
||||
level.sendBlockUpdated(blockEntity.getBlockPos(), blockEntity.getBlockState(), blockEntity.getBlockState(), Block.UPDATE_CLIENTS);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isApplicable(BlockState state, BlockEntity blockEntity) {
|
||||
if (expectedBlockKey != null) {
|
||||
return expectedBlockKey.equals(normalizeBlockKey(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString()));
|
||||
}
|
||||
return switch (legacyType) {
|
||||
case 0 -> blockEntity instanceof SignBlockEntity;
|
||||
case 1 -> blockEntity instanceof SpawnerBlockEntity;
|
||||
case 2 -> blockEntity instanceof BannerBlockEntity;
|
||||
case 3 -> blockEntity instanceof RandomizableContainerBlockEntity;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
CompoundTag payload() throws Exception {
|
||||
String snbt = snbt();
|
||||
if (snbt != null && !snbt.isBlank()) {
|
||||
return NbtUtils.snbtToStructure(snbt);
|
||||
}
|
||||
Tag converted = toTag(tileProperties);
|
||||
return converted instanceof CompoundTag compound ? compound : null;
|
||||
}
|
||||
|
||||
public BlockState adjustBlockState(BlockState state) {
|
||||
Object colorValue = tileProperties.get(LEGACY_BANNER_COLOR_PROPERTY);
|
||||
if (colorValue == null) {
|
||||
return state;
|
||||
}
|
||||
Identifier currentId = BuiltInRegistries.BLOCK.getKey(state.getBlock());
|
||||
String currentPath = currentId.getPath();
|
||||
if (!currentPath.endsWith("_banner")) {
|
||||
return state;
|
||||
}
|
||||
String suffix = currentPath.endsWith("_wall_banner") ? "_wall_banner" : "_banner";
|
||||
Identifier targetId = Identifier.tryParse("minecraft:" + colorValue + suffix);
|
||||
if (targetId == null || !BuiltInRegistries.BLOCK.containsKey(targetId)) {
|
||||
return state;
|
||||
}
|
||||
BlockState adjusted = BuiltInRegistries.BLOCK.getValue(targetId).defaultBlockState();
|
||||
for (Property<?> property : state.getProperties()) {
|
||||
if (adjusted.hasProperty(property)) {
|
||||
adjusted = copyProperty(adjusted, state, property);
|
||||
}
|
||||
}
|
||||
return adjusted;
|
||||
}
|
||||
|
||||
private static Tag toTag(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Map<?, ?> map) {
|
||||
CompoundTag compound = new CompoundTag();
|
||||
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||
if (LEGACY_BANNER_COLOR_PROPERTY.equals(String.valueOf(entry.getKey()))) {
|
||||
continue;
|
||||
}
|
||||
Tag child = toTag(entry.getValue());
|
||||
if (child != null) {
|
||||
compound.put(String.valueOf(entry.getKey()), child);
|
||||
}
|
||||
}
|
||||
return compound;
|
||||
}
|
||||
if (value instanceof List<?> values) {
|
||||
ListTag list = new ListTag();
|
||||
for (Object entry : values) {
|
||||
Tag child = toTag(entry);
|
||||
if (child != null) {
|
||||
list.add(child);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
if (value instanceof Boolean bool) {
|
||||
return ByteTag.valueOf(bool);
|
||||
}
|
||||
if (value instanceof Byte number) {
|
||||
return ByteTag.valueOf(number);
|
||||
}
|
||||
if (value instanceof Short number) {
|
||||
return ShortTag.valueOf(number);
|
||||
}
|
||||
if (value instanceof Integer number) {
|
||||
return IntTag.valueOf(number);
|
||||
}
|
||||
if (value instanceof Long number) {
|
||||
return LongTag.valueOf(number);
|
||||
}
|
||||
if (value instanceof Float number) {
|
||||
return FloatTag.valueOf(number);
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return DoubleTag.valueOf(number.doubleValue());
|
||||
}
|
||||
return StringTag.valueOf(String.valueOf(value));
|
||||
}
|
||||
|
||||
private static <T extends Comparable<T>> BlockState copyProperty(BlockState target, BlockState source, Property<T> property) {
|
||||
return target.setValue(property, source.getValue(property));
|
||||
}
|
||||
|
||||
static String normalizeBlockKey(String blockKey) {
|
||||
if (blockKey == null || blockKey.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String normalized = blockKey.trim().toLowerCase(java.util.Locale.ROOT);
|
||||
int bracket = normalized.indexOf('[');
|
||||
if (bracket >= 0) {
|
||||
normalized = normalized.substring(0, bracket);
|
||||
}
|
||||
if (!normalized.contains(":")) {
|
||||
normalized = "minecraft:" + normalized.replaceAll("\\s+", "_");
|
||||
}
|
||||
Identifier identifier = Identifier.tryParse(normalized);
|
||||
return identifier == null ? null : identifier.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public KMap<String, Object> getProperties() {
|
||||
return tileProperties;
|
||||
|
||||
+149
-38
@@ -28,18 +28,68 @@ import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.item.DyeColor;
|
||||
import net.minecraft.world.level.block.entity.BannerPattern;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
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 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");
|
||||
private static final List<Identifier> LEGACY_BUKKIT_ENTITY_TYPES = createLegacyBukkitEntityTypes();
|
||||
private static final List<Identifier> PAPER_26_2_BANNER_PATTERNS = List.of(
|
||||
Identifier.parse("minecraft:small_stripes"),
|
||||
Identifier.parse("minecraft:stripe_right"),
|
||||
Identifier.parse("minecraft:diagonal_left"),
|
||||
Identifier.parse("minecraft:stripe_middle"),
|
||||
Identifier.parse("minecraft:square_bottom_right"),
|
||||
Identifier.parse("minecraft:half_horizontal"),
|
||||
Identifier.parse("minecraft:skull"),
|
||||
Identifier.parse("minecraft:flow"),
|
||||
Identifier.parse("minecraft:rhombus"),
|
||||
Identifier.parse("minecraft:border"),
|
||||
Identifier.parse("minecraft:gradient_up"),
|
||||
Identifier.parse("minecraft:guster"),
|
||||
Identifier.parse("minecraft:square_top_left"),
|
||||
Identifier.parse("minecraft:triangle_bottom"),
|
||||
Identifier.parse("minecraft:triangles_bottom"),
|
||||
Identifier.parse("minecraft:half_horizontal_bottom"),
|
||||
Identifier.parse("minecraft:gradient"),
|
||||
Identifier.parse("minecraft:triangle_top"),
|
||||
Identifier.parse("minecraft:piglin"),
|
||||
Identifier.parse("minecraft:stripe_center"),
|
||||
Identifier.parse("minecraft:circle"),
|
||||
Identifier.parse("minecraft:stripe_left"),
|
||||
Identifier.parse("minecraft:stripe_bottom"),
|
||||
Identifier.parse("minecraft:square_top_right"),
|
||||
Identifier.parse("minecraft:curly_border"),
|
||||
Identifier.parse("minecraft:creeper"),
|
||||
Identifier.parse("minecraft:square_bottom_left"),
|
||||
Identifier.parse("minecraft:triangles_top"),
|
||||
Identifier.parse("minecraft:half_vertical"),
|
||||
Identifier.parse("minecraft:mojang"),
|
||||
Identifier.parse("minecraft:diagonal_right"),
|
||||
Identifier.parse("minecraft:cross"),
|
||||
Identifier.parse("minecraft:straight_cross"),
|
||||
Identifier.parse("minecraft:bricks"),
|
||||
Identifier.parse("minecraft:diagonal_up_left"),
|
||||
Identifier.parse("minecraft:base"),
|
||||
Identifier.parse("minecraft:flower"),
|
||||
Identifier.parse("minecraft:stripe_downleft"),
|
||||
Identifier.parse("minecraft:diagonal_up_right"),
|
||||
Identifier.parse("minecraft:stripe_downright"),
|
||||
Identifier.parse("minecraft:stripe_top"),
|
||||
Identifier.parse("minecraft:globe"),
|
||||
Identifier.parse("minecraft:half_vertical_right"));
|
||||
|
||||
private final Supplier<MinecraftServer> server;
|
||||
|
||||
@@ -135,7 +185,8 @@ public final class ModdedTileReader implements TileData.TileReader {
|
||||
if (properties == null) {
|
||||
throw new NullPointerException("properties is marked non-null but is null");
|
||||
}
|
||||
return new ModdedTileData(replay.consumed(), properties);
|
||||
return new ModdedTileData(replay.consumed(), properties,
|
||||
ModdedTileData.normalizeBlockKey(materialKey), -1);
|
||||
} catch (Throwable e) {
|
||||
replay.rewind();
|
||||
return parseLegacy(din, replay);
|
||||
@@ -149,102 +200,162 @@ public final class ModdedTileReader implements TileData.TileReader {
|
||||
|
||||
private TileData parseLegacy(DataInputStream din, ReplayInputStream replay) throws IOException {
|
||||
int id = din.readShort();
|
||||
switch (id) {
|
||||
String expectedBlockKey = null;
|
||||
KMap<String, Object> properties = switch (id) {
|
||||
case 0 -> readSign(din);
|
||||
case 1 -> readSpawner(din, replay);
|
||||
case 2 -> readBanner(din, replay);
|
||||
case 3 -> readLootable(din);
|
||||
case 3 -> {
|
||||
expectedBlockKey = ModdedTileData.normalizeBlockKey(din.readUTF());
|
||||
yield readLootable(din);
|
||||
}
|
||||
default -> throw new IOException("Unknown tile type: " + id);
|
||||
}
|
||||
return new ModdedTileData(replay.consumed(), new KMap<>());
|
||||
};
|
||||
return new ModdedTileData(replay.consumed(), properties, expectedBlockKey, id);
|
||||
}
|
||||
|
||||
private static void readSign(DataInputStream din) throws IOException {
|
||||
din.readUTF();
|
||||
din.readUTF();
|
||||
din.readUTF();
|
||||
din.readUTF();
|
||||
private static KMap<String, Object> readSign(DataInputStream din) throws IOException {
|
||||
List<String> messages = List.of(din.readUTF(), din.readUTF(), din.readUTF(), din.readUTF());
|
||||
byte dye = din.readByte();
|
||||
if (dye < 0 || dye >= DYE_COLOR_COUNT) {
|
||||
throw new ArrayIndexOutOfBoundsException("Index " + dye + " out of bounds for length " + DYE_COLOR_COUNT);
|
||||
}
|
||||
KMap<String, Object> text = new KMap<>();
|
||||
text.put("messages", messages);
|
||||
text.put("color", DyeColor.byId(dye).getName());
|
||||
text.put("has_glowing_text", false);
|
||||
KMap<String, Object> properties = new KMap<>();
|
||||
properties.put("front_text", text);
|
||||
properties.put("back_text", text.copy());
|
||||
return properties;
|
||||
}
|
||||
|
||||
private static void readSpawner(DataInputStream din, ReplayInputStream replay) throws IOException {
|
||||
boolean resolved = false;
|
||||
private static KMap<String, Object> readSpawner(DataInputStream din, ReplayInputStream replay) throws IOException {
|
||||
Identifier entityId = null;
|
||||
replay.mark(Integer.MAX_VALUE);
|
||||
|
||||
try {
|
||||
String keyString = din.readUTF();
|
||||
Identifier key = Identifier.tryParse(keyString);
|
||||
resolved = key != null && BuiltInRegistries.ENTITY_TYPE.containsKey(key);
|
||||
if (!resolved) {
|
||||
if (key != null && BuiltInRegistries.ENTITY_TYPE.containsKey(key)) {
|
||||
entityId = key;
|
||||
} else {
|
||||
replay.reset();
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
replay.reset();
|
||||
}
|
||||
|
||||
if (!resolved) {
|
||||
din.readShort();
|
||||
if (entityId == null) {
|
||||
entityId = legacySpawnerEntityId(din.readShort());
|
||||
}
|
||||
KMap<String, Object> entity = new KMap<>();
|
||||
entity.put("id", entityId.toString());
|
||||
KMap<String, Object> spawnData = new KMap<>();
|
||||
spawnData.put("entity", entity);
|
||||
KMap<String, Object> properties = new KMap<>();
|
||||
properties.put("SpawnData", spawnData);
|
||||
return properties;
|
||||
}
|
||||
|
||||
private void readBanner(DataInputStream din, ReplayInputStream replay) throws IOException {
|
||||
din.readUnsignedByte();
|
||||
static Identifier legacySpawnerEntityId(int legacyOrdinal) {
|
||||
if (legacyOrdinal < 0 || legacyOrdinal >= LEGACY_BUKKIT_ENTITY_TYPES.size()) {
|
||||
return DEFAULT_SPAWNER_ENTITY;
|
||||
}
|
||||
return LEGACY_BUKKIT_ENTITY_TYPES.get(legacyOrdinal);
|
||||
}
|
||||
|
||||
private static List<Identifier> createLegacyBukkitEntityTypes() {
|
||||
List<Identifier> entityTypes = new ArrayList<>();
|
||||
for (Identifier key : BuiltInRegistries.ENTITY_TYPE.keySet()) {
|
||||
if (Identifier.DEFAULT_NAMESPACE.equals(key.getNamespace())) {
|
||||
entityTypes.add(key);
|
||||
}
|
||||
}
|
||||
entityTypes.sort(Identifier::compareTo);
|
||||
return List.copyOf(entityTypes);
|
||||
}
|
||||
|
||||
private KMap<String, Object> readBanner(DataInputStream din, ReplayInputStream replay) throws IOException {
|
||||
int baseColor = din.readUnsignedByte();
|
||||
int listSize = din.readUnsignedByte();
|
||||
replay.mark(Integer.MAX_VALUE);
|
||||
|
||||
boolean parsedKeyed = false;
|
||||
List<Object> layers = new ArrayList<>(listSize);
|
||||
try {
|
||||
for (int i = 0; i < listSize; i++) {
|
||||
din.readUnsignedByte();
|
||||
int color = din.readUnsignedByte();
|
||||
Identifier patternKey = Identifier.tryParse(din.readUTF());
|
||||
if (patternKey == null || !bannerPatternExists(patternKey)) {
|
||||
throw new IOException("Unknown banner pattern key");
|
||||
}
|
||||
layers.add(bannerLayer(patternKey, color));
|
||||
}
|
||||
parsedKeyed = true;
|
||||
} catch (Throwable ignored) {
|
||||
replay.reset();
|
||||
layers.clear();
|
||||
}
|
||||
|
||||
if (parsedKeyed) {
|
||||
return;
|
||||
if (layers.isEmpty() && listSize > 0) {
|
||||
for (int i = 0; i < listSize; i++) {
|
||||
int color = din.readUnsignedByte();
|
||||
int pattern = din.readUnsignedByte();
|
||||
layers.add(bannerLayer(legacyBannerPatternKey(pattern), color));
|
||||
}
|
||||
}
|
||||
KMap<String, Object> properties = new KMap<>();
|
||||
properties.put("patterns", layers);
|
||||
properties.put(ModdedTileData.LEGACY_BANNER_COLOR_PROPERTY, DyeColor.byId(baseColor).getName());
|
||||
return properties;
|
||||
}
|
||||
|
||||
for (int i = 0; i < listSize; i++) {
|
||||
din.readUnsignedByte();
|
||||
din.readUnsignedByte();
|
||||
private static KMap<String, Object> bannerLayer(Identifier pattern, int color) {
|
||||
KMap<String, Object> layer = new KMap<>();
|
||||
layer.put("pattern", pattern.toString());
|
||||
layer.put("color", DyeColor.byId(color).getName());
|
||||
return layer;
|
||||
}
|
||||
|
||||
static Identifier legacyBannerPatternKey(int legacyOrdinal) {
|
||||
if (legacyOrdinal < 0 || legacyOrdinal >= PAPER_26_2_BANNER_PATTERNS.size()) {
|
||||
return DEFAULT_BANNER_PATTERN;
|
||||
}
|
||||
return PAPER_26_2_BANNER_PATTERNS.get(legacyOrdinal);
|
||||
}
|
||||
|
||||
private boolean bannerPatternExists(Identifier key) {
|
||||
MinecraftServer instance = server.get();
|
||||
if (instance == null) {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
Registry<BannerPattern> registry = instance.registryAccess().lookupOrThrow(Registries.BANNER_PATTERN);
|
||||
return registry.containsKey(key);
|
||||
}
|
||||
|
||||
private static void readLootable(DataInputStream din) throws IOException {
|
||||
din.readUTF();
|
||||
din.readUTF();
|
||||
din.readLong();
|
||||
private static KMap<String, Object> readLootable(DataInputStream din) throws IOException {
|
||||
String lootTable = din.readUTF();
|
||||
long seed = din.readLong();
|
||||
KMap<String, Object> properties = new KMap<>();
|
||||
if (!lootTable.isBlank()) {
|
||||
properties.put("LootTable", lootTable);
|
||||
properties.put("LootTableSeed", seed);
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
|
||||
private static boolean matchMaterial(String name) {
|
||||
String filtered = name;
|
||||
if (filtered.startsWith("minecraft:")) {
|
||||
filtered = filtered.substring("minecraft:".length());
|
||||
String filtered = name.trim().toLowerCase(Locale.ROOT);
|
||||
int bracket = filtered.indexOf('[');
|
||||
if (bracket >= 0) {
|
||||
filtered = filtered.substring(0, bracket);
|
||||
}
|
||||
filtered = filtered.toUpperCase(Locale.ROOT);
|
||||
filtered = filtered.replaceAll("\\s+", "_").replaceAll("\\W", "");
|
||||
Identifier identifier = Identifier.tryParse("minecraft:" + filtered.toLowerCase(Locale.ROOT));
|
||||
if (!filtered.contains(":")) {
|
||||
filtered = "minecraft:" + filtered.replaceAll("\\s+", "_");
|
||||
}
|
||||
Identifier identifier = Identifier.tryParse(filtered);
|
||||
if (identifier == null) {
|
||||
return false;
|
||||
}
|
||||
return BuiltInRegistries.ITEM.containsKey(identifier) || BuiltInRegistries.BLOCK.containsKey(identifier);
|
||||
return BuiltInRegistries.BLOCK.containsKey(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
+1006
-19
File diff suppressed because it is too large
Load Diff
+20
-8
@@ -23,6 +23,7 @@ import art.arcane.iris.engine.IrisEngine;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineTarget;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
@@ -82,6 +83,7 @@ public final class ModdedWorldEngines {
|
||||
}
|
||||
|
||||
long seed = seedOverride == Long.MIN_VALUE ? level.getSeed() : seedOverride;
|
||||
validateDimensionContract(dimension, level);
|
||||
File worldFolder = DimensionType.getStorageFolder(level.dimension(), level.getServer().getWorldPath(LevelResource.ROOT)).toFile();
|
||||
IrisWorld world = IrisWorld.builder()
|
||||
.platformIdentity(level.dimension().identifier().toString())
|
||||
@@ -90,21 +92,31 @@ public final class ModdedWorldEngines {
|
||||
.worldFolder(worldFolder)
|
||||
.minHeight(dimension.getMinHeight())
|
||||
.maxHeight(dimension.getMaxHeight())
|
||||
.platformWorld(new ModdedPlatformWorld(level))
|
||||
.build();
|
||||
Engine engine = new IrisEngine(new EngineTarget(world, dimension, data), false);
|
||||
|
||||
int levelMinY = level.getMinY();
|
||||
int levelMaxY = level.getMinY() + level.getHeight();
|
||||
if (dimension.getMinHeight() < levelMinY || dimension.getMaxHeight() > levelMaxY) {
|
||||
LOGGER.warn("Iris pack height range for {} exceeds the level: pack generates {}..{} but the level is {}..{}. Terrain outside the level range will be clipped; restart so the forced datapack registers the pack dimension type.",
|
||||
level.dimension().identifier(), dimension.getMinHeight(), dimension.getMaxHeight(), levelMinY, levelMaxY);
|
||||
}
|
||||
|
||||
LOGGER.info("Iris engine up for {}: pack={} dim={} seed={} height={}..{}",
|
||||
level.dimension().identifier(), packDir.getAbsolutePath(), dimension.getLoadKey(), seed, dimension.getMinHeight(), dimension.getMaxHeight());
|
||||
return engine;
|
||||
}
|
||||
|
||||
private static void validateDimensionContract(IrisDimension dimension, ServerLevel level) {
|
||||
DimensionType actualType = level.dimensionType();
|
||||
String actualTypeKey = level.dimensionTypeRegistration().unwrapKey()
|
||||
.map(key -> key.identifier().toString())
|
||||
.orElse("<unregistered>");
|
||||
IrisDimensionRuntimeContract expected = IrisDimensionRuntimeContract.expected(dimension, "irisworldgen");
|
||||
IrisDimensionRuntimeContract actual = new IrisDimensionRuntimeContract(
|
||||
actualTypeKey,
|
||||
actualType.minY(),
|
||||
actualType.height(),
|
||||
actualType.logicalHeight());
|
||||
String runtimeName = "Modded level '" + level.dimension().identifier() + "'";
|
||||
expected.requireExact(runtimeName, actual);
|
||||
expected.requireHeight(runtimeName, level.getMinY(), level.getHeight());
|
||||
}
|
||||
|
||||
public static File packFolder(String pack) {
|
||||
return ModdedEngineBootstrap.loader().configDir()
|
||||
.resolve("irisworldgen")
|
||||
@@ -113,7 +125,7 @@ public final class ModdedWorldEngines {
|
||||
.toFile();
|
||||
}
|
||||
|
||||
private static File resolvePack(String pack, String dimensionKey) {
|
||||
static File resolvePack(String pack, String dimensionKey) {
|
||||
File packDir = packFolder(pack);
|
||||
if (packDir.isDirectory()) {
|
||||
return packDir;
|
||||
|
||||
+130
-110
@@ -30,7 +30,6 @@ import art.arcane.iris.engine.object.IrisEntitySpawn;
|
||||
import art.arcane.iris.engine.object.IrisMarker;
|
||||
import art.arcane.iris.engine.object.IrisPosition;
|
||||
import art.arcane.iris.engine.object.IrisRange;
|
||||
import art.arcane.iris.engine.object.IrisRate;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisSpawnGroup;
|
||||
import art.arcane.iris.engine.object.IrisSpawner;
|
||||
@@ -43,18 +42,16 @@ import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
import art.arcane.volmlib.util.matter.slices.MarkerMatter;
|
||||
import it.unimi.dsi.fastutil.longs.LongArrayList;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.Mob;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
@@ -66,15 +63,13 @@ import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class ModdedWorldManager implements EngineWorldManager {
|
||||
static final MantleFlag INITIAL_SPAWN_COMPLETION_FLAG = MantleFlag.INITIAL_SPAWNED_MARKER;
|
||||
private static final int MAX_INITIAL_QUEUE = 8192;
|
||||
private static final int MAX_INITIAL_DRAIN_PER_TICK = 8;
|
||||
private static final int MAX_INITIAL_RECOVERY_PER_PASS = 128;
|
||||
private static final int MANTLE_WARMUP_QUEUE_CAPACITY = 256;
|
||||
private static final long INITIAL_RECOVERY_INTERVAL_MS = 1_000L;
|
||||
private static final long COUNT_INTERVAL_MS = 3_000L;
|
||||
private static final int ENTITY_SCAN_RADIUS = 64;
|
||||
private static final int PLAYER_CHUNK_RADIUS = 4;
|
||||
private static final int MIN_TICK_INTERVAL_MS = 1_000;
|
||||
|
||||
private final Engine engine;
|
||||
private final InitialSpawnQueue initialSpawnQueue;
|
||||
@@ -123,6 +118,9 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
if (closed || engine.isClosed() || engine.getMantle().getMantle().isClosed()) {
|
||||
return;
|
||||
}
|
||||
if (!isEntitySpawningEnabledForCurrentWorld()) {
|
||||
return;
|
||||
}
|
||||
if (isPregenActive()) {
|
||||
return;
|
||||
}
|
||||
@@ -132,7 +130,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
}
|
||||
|
||||
private void recoverLoadedInitialSpawns(ServerLevel level) {
|
||||
if (!markerSystemEnabled() && !ambientSystemEnabled()) {
|
||||
if (!markerSystemEnabled()) {
|
||||
return;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
@@ -161,7 +159,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
if (level.getChunkSource().getChunkNow(chunkX, chunkZ) == null) {
|
||||
continue;
|
||||
}
|
||||
if (mantle.isChunkLoaded(chunkX, chunkZ) && mantle.hasFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED)) {
|
||||
if (mantle.isChunkLoaded(chunkX, chunkZ) && mantle.hasFlag(chunkX, chunkZ, INITIAL_SPAWN_COMPLETION_FLAG)) {
|
||||
continue;
|
||||
}
|
||||
if (initialSpawnQueue.offer(key) && ++recovered >= MAX_INITIAL_RECOVERY_PER_PASS) {
|
||||
@@ -174,7 +172,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
if (initialSpawnQueue.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!markerSystemEnabled() && !ambientSystemEnabled()) {
|
||||
if (!markerSystemEnabled()) {
|
||||
initialSpawnQueue.clear();
|
||||
return;
|
||||
}
|
||||
@@ -211,23 +209,22 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
}
|
||||
|
||||
private boolean initialSpawnChunk(ServerLevel level, int chunkX, int chunkZ) {
|
||||
if (!ModdedEntitySpawner.chunksSafe(level, chunkX, chunkZ)) {
|
||||
return false;
|
||||
}
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
if (!mantle.isChunkLoaded(chunkX, chunkZ)) {
|
||||
return false;
|
||||
}
|
||||
if (mantle.hasFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED)) {
|
||||
if (mantle.hasFlag(chunkX, chunkZ, INITIAL_SPAWN_COMPLETION_FLAG)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
MantleChunk<Matter> chunk = mantle.getChunk(chunkX, chunkZ).use();
|
||||
try {
|
||||
chunk.raiseFlagUnchecked(MantleFlag.INITIAL_SPAWNED, () -> {
|
||||
if (markerSystemEnabled()) {
|
||||
spawnMarkerSpawners(level, chunkX, chunkZ, chunk, true);
|
||||
}
|
||||
if (ambientSystemEnabled()) {
|
||||
spawnAmbient(level, chunkX, chunkZ, true);
|
||||
}
|
||||
chunk.raiseFlagUnchecked(INITIAL_SPAWN_COMPLETION_FLAG, () -> {
|
||||
spawnMarkerSpawners(level, chunkX, chunkZ, chunk, true);
|
||||
scheduleInitialFollowUp(level, chunkX, chunkZ);
|
||||
});
|
||||
} finally {
|
||||
chunk.release();
|
||||
@@ -235,6 +232,40 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void scheduleInitialFollowUp(ServerLevel level, int chunkX, int chunkZ) {
|
||||
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
|
||||
if (scheduler == null) {
|
||||
IrisLogging.error("Iris could not schedule the initial entity-spawn follow-up because the modded scheduler is unavailable.");
|
||||
return;
|
||||
}
|
||||
scheduler.laterGlobal(() -> runInitialFollowUp(level, chunkX, chunkZ), RNG.r.i(5, 200));
|
||||
}
|
||||
|
||||
private void runInitialFollowUp(ServerLevel level, int chunkX, int chunkZ) {
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
if (closed || engine.isClosed() || mantle.isClosed() || !isEntitySpawningEnabledForCurrentWorld()) {
|
||||
return;
|
||||
}
|
||||
if (level.getChunkSource().getChunkNow(chunkX, chunkZ) == null || !mantle.isChunkLoaded(chunkX, chunkZ)) {
|
||||
return;
|
||||
}
|
||||
if (engine.getComplex() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
MantleChunk<Matter> chunk = mantle.getChunk(chunkX, chunkZ).use();
|
||||
try {
|
||||
if (markerSystemEnabled()) {
|
||||
spawnMarkerSpawners(level, chunkX, chunkZ, chunk, false);
|
||||
}
|
||||
if (ambientSystemEnabled()) {
|
||||
spawnAmbient(level, chunkX, chunkZ, true);
|
||||
}
|
||||
} finally {
|
||||
chunk.release();
|
||||
}
|
||||
}
|
||||
|
||||
private void warmupMantleChunkAsync(long key, int chunkX, int chunkZ) {
|
||||
if (closed || !mantleWarmups.add(key)) {
|
||||
return;
|
||||
@@ -263,25 +294,25 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
|
||||
private void ambientTick(ServerLevel level) {
|
||||
long now = System.currentTimeMillis();
|
||||
long interval = Math.max((long) MIN_TICK_INTERVAL_MS, IrisSettings.get().getWorld().getAsyncTickIntervalMS());
|
||||
long interval = IrisSettings.get().getWorld().getAsyncTickIntervalMS();
|
||||
if (now - lastAmbientAt < interval) {
|
||||
return;
|
||||
}
|
||||
lastAmbientAt = now;
|
||||
|
||||
if (level.players().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!markerSystemEnabled() && !ambientSystemEnabled()) {
|
||||
return;
|
||||
}
|
||||
if (level.players().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
refreshEntityCount(level, now);
|
||||
long[] candidates = loadedChunkPositionsSnapshot(level);
|
||||
refreshEntityCount(level, now, candidates.length);
|
||||
if (cachedSaturation > IrisSettings.get().getWorld().getTargetSpawnEntitiesPerChunk()) {
|
||||
return;
|
||||
}
|
||||
|
||||
long[] candidates = loadedChunksNearPlayers(level);
|
||||
if (candidates.length == 0) {
|
||||
return;
|
||||
}
|
||||
@@ -298,6 +329,9 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
}
|
||||
|
||||
private void ambientSpawnChunk(ServerLevel level, int chunkX, int chunkZ) {
|
||||
if (!ModdedEntitySpawner.chunksSafe(level, chunkX, chunkZ)) {
|
||||
return;
|
||||
}
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
if (!mantle.isChunkLoaded(chunkX, chunkZ)) {
|
||||
return;
|
||||
@@ -318,6 +352,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
|
||||
private void spawnMarkerSpawners(ServerLevel level, int chunkX, int chunkZ, MantleChunk<Matter> chunk, boolean initial) {
|
||||
int minHeight = engine.getWorld().minHeight();
|
||||
KList<IrisPosition> obstructed = new KList<>();
|
||||
chunk.iterate(MatterMarker.class, (Integer x, Integer yf, Integer z, MatterMarker marker) -> {
|
||||
String tag = marker.getTag();
|
||||
if (tag.equals("cave_floor") || tag.equals("cave_ceiling")) {
|
||||
@@ -333,6 +368,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
int worldY = yf + minHeight;
|
||||
|
||||
if (resolved.isEmptyAbove() && aboveObstructed(level, worldX, worldY, worldZ)) {
|
||||
obstructed.add(new IrisPosition(worldX, yf, worldZ));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -346,6 +382,10 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
}
|
||||
spawnFromSpawner(level, new IrisPosition(worldX, worldY, worldZ), chosen, initial);
|
||||
});
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
for (IrisPosition position : obstructed) {
|
||||
mantle.remove(position.getX(), position.getY(), position.getZ(), MatterMarker.class);
|
||||
}
|
||||
}
|
||||
|
||||
private KList<IrisSpawner> resolveMarkerSpawners(IrisMarker marker) {
|
||||
@@ -397,7 +437,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
int blockZ = chunkZ << 4;
|
||||
IrisBiome biome = engine.getSurfaceBiome(blockX, blockZ);
|
||||
IrisRegion region = engine.getRegion(blockX, blockZ);
|
||||
int chunkMobs = countChunkMobs(level, chunkX, chunkZ);
|
||||
int chunkMobs = countChunkLivingEntities(level, chunkX, chunkZ);
|
||||
|
||||
KList<IrisEntitySpawn> pool = new KList<>();
|
||||
collectSpawns(pool, engine.getData().getSpawnerLoader().loadAll(engine.getDimension().getEntitySpawners()), biome, chunkX, chunkZ, chunkMobs, initial);
|
||||
@@ -457,19 +497,33 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
return 0;
|
||||
}
|
||||
|
||||
RNG entityRng = new RNG(engine.getSeedManager().getEntity());
|
||||
RNG entityRng = entry.getRng().aquire(() -> new RNG(engine.getSeedManager().getEntity()));
|
||||
IrisSpawnGroup group = spawner.getGroup();
|
||||
KList<IrisPosition> caveFloors = group == IrisSpawnGroup.CAVE
|
||||
? engine.getMantle().findMarkers(chunkX, chunkZ, MarkerMatter.CAVE_FLOOR)
|
||||
: new KList<>();
|
||||
int spawned = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
int worldX = (chunkX << 4) + RNG.r.i(15);
|
||||
int worldZ = (chunkZ << 4) + RNG.r.i(15);
|
||||
int surfaceY = level.getMinY() + engine.getHeight(worldX, worldZ, false);
|
||||
int solidY = level.getMinY() + engine.getHeight(worldX, worldZ, true);
|
||||
int worldY = switch (group) {
|
||||
case NORMAL -> surfaceY + 1;
|
||||
case CAVE -> solidY + 1;
|
||||
case UNDERWATER, BEACH -> surfaceY > solidY + 1 ? RNG.r.i(solidY + 1, surfaceY) : surfaceY;
|
||||
};
|
||||
int worldX;
|
||||
int worldY;
|
||||
int worldZ;
|
||||
if (group == IrisSpawnGroup.CAVE) {
|
||||
if (caveFloors.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
IrisPosition caveFloor = caveFloors.getRandom(RNG.r);
|
||||
worldX = caveFloor.getX();
|
||||
worldY = caveFloor.getY() + 1;
|
||||
worldZ = caveFloor.getZ();
|
||||
} else {
|
||||
worldX = (chunkX << 4) + RNG.r.i(15);
|
||||
worldZ = (chunkZ << 4) + RNG.r.i(15);
|
||||
int surfaceY = level.getMinY() + engine.getHeight(worldX, worldZ, false);
|
||||
int solidY = level.getMinY() + engine.getHeight(worldX, worldZ, true);
|
||||
worldY = group == IrisSpawnGroup.NORMAL
|
||||
? surfaceY + 1
|
||||
: RNG.r.i(solidY + 1, surfaceY);
|
||||
}
|
||||
if (worldY <= level.getMinY() || worldY >= level.getMaxY()) {
|
||||
continue;
|
||||
}
|
||||
@@ -479,7 +533,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
if (!surfaceMatches(irisEntity.getSurface(), level, worldX, worldY, worldZ)) {
|
||||
continue;
|
||||
}
|
||||
if (!clearForSpawn(level, worldX, worldY, worldZ)) {
|
||||
if (!ModdedEntitySpawner.isAreaClearForSpawn(level, irisEntity, worldX, worldY, worldZ)) {
|
||||
continue;
|
||||
}
|
||||
if (ModdedEntitySpawner.spawn(engine, irisEntity, level, worldX, worldY, worldZ, entityRng) != null) {
|
||||
@@ -504,7 +558,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
|
||||
exhaustMarker(spawner, position);
|
||||
|
||||
RNG entityRng = new RNG(engine.getSeedManager().getEntity());
|
||||
RNG entityRng = entry.getRng().aquire(() -> new RNG(engine.getSeedManager().getEntity()));
|
||||
int worldX = position.getX();
|
||||
int worldY = position.getY() + 1;
|
||||
int worldZ = position.getZ();
|
||||
@@ -529,12 +583,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
}
|
||||
|
||||
private boolean canSpawn(IrisSpawner spawner, int chunkX, int chunkZ) {
|
||||
IrisRate rate = spawner.getMaximumRate();
|
||||
if (!rate.isInfinite() && !engine.getEngineData().getCooldown(spawner).canSpawn(rate)) {
|
||||
return false;
|
||||
}
|
||||
IrisRate chunkRate = spawner.getMaximumRatePerChunk();
|
||||
return chunkRate.isInfinite() || engine.getEngineData().getChunk(chunkX, chunkZ).getCooldown(spawner).canSpawn(chunkRate);
|
||||
return spawner.canSpawn(engine, chunkX, chunkZ);
|
||||
}
|
||||
|
||||
private boolean lightAllowed(IrisSpawner spawner, ServerLevel level, int worldX, int worldY, int worldZ) {
|
||||
@@ -575,68 +624,51 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
|| state.is(Blocks.KELP) || state.is(Blocks.KELP_PLANT);
|
||||
}
|
||||
|
||||
private boolean clearForSpawn(ServerLevel level, int worldX, int worldY, int worldZ) {
|
||||
BlockState at = level.getBlockState(new BlockPos(worldX, worldY, worldZ));
|
||||
BlockState above = level.getBlockState(new BlockPos(worldX, worldY + 1, worldZ));
|
||||
return !ModdedBlockResolution.isSolid(at) && !ModdedBlockResolution.isSolid(above);
|
||||
}
|
||||
|
||||
private boolean aboveObstructed(ServerLevel level, int worldX, int worldY, int worldZ) {
|
||||
return ModdedBlockResolution.isSolid(level.getBlockState(new BlockPos(worldX, worldY + 1, worldZ)))
|
||||
return worldY + 2 >= level.getMaxY()
|
||||
|| ModdedBlockResolution.isSolid(level.getBlockState(new BlockPos(worldX, worldY + 1, worldZ)))
|
||||
|| ModdedBlockResolution.isSolid(level.getBlockState(new BlockPos(worldX, worldY + 2, worldZ)));
|
||||
}
|
||||
|
||||
private int countChunkMobs(ServerLevel level, int chunkX, int chunkZ) {
|
||||
private int countChunkLivingEntities(ServerLevel level, int chunkX, int chunkZ) {
|
||||
int baseX = chunkX << 4;
|
||||
int baseZ = chunkZ << 4;
|
||||
AABB box = new AABB(baseX, level.getMinY(), baseZ, baseX + 16, level.getMaxY(), baseZ + 16);
|
||||
return level.getEntities((Entity) null, box, (Entity e) -> e instanceof Mob && e.isAlive()).size();
|
||||
return level.getEntities((Entity) null, box,
|
||||
(Entity entity) -> isLivingEntityInChunk(entity, chunkX, chunkZ)).size();
|
||||
}
|
||||
|
||||
private void refreshEntityCount(ServerLevel level, long now) {
|
||||
private static boolean isLivingEntityInChunk(Entity entity, int chunkX, int chunkZ) {
|
||||
if (!(entity instanceof LivingEntity)) {
|
||||
return false;
|
||||
}
|
||||
BlockPos position = entity.blockPosition();
|
||||
return (position.getX() >> 4) == chunkX && (position.getZ() >> 4) == chunkZ;
|
||||
}
|
||||
|
||||
private void refreshEntityCount(ServerLevel level, long now, int loadedChunks) {
|
||||
cachedConsideredChunks = loadedChunks;
|
||||
cachedSaturation = cachedEntityCount / (loadedChunks + 1.0) * 1.28;
|
||||
if (now - lastCountAt < COUNT_INTERVAL_MS) {
|
||||
return;
|
||||
}
|
||||
lastCountAt = now;
|
||||
|
||||
int mobs = 0;
|
||||
int chunks = 0;
|
||||
for (ServerPlayer player : level.players()) {
|
||||
int px = player.blockPosition().getX();
|
||||
int pz = player.blockPosition().getZ();
|
||||
AABB box = new AABB(px - ENTITY_SCAN_RADIUS, level.getMinY(), pz - ENTITY_SCAN_RADIUS,
|
||||
px + ENTITY_SCAN_RADIUS, level.getMaxY(), pz + ENTITY_SCAN_RADIUS);
|
||||
mobs += level.getEntities((Entity) null, box, (Entity e) -> e instanceof Mob && e.isAlive()).size();
|
||||
chunks += (PLAYER_CHUNK_RADIUS * 2 + 1) * (PLAYER_CHUNK_RADIUS * 2 + 1);
|
||||
}
|
||||
|
||||
cachedEntityCount = mobs;
|
||||
cachedConsideredChunks = chunks;
|
||||
cachedSaturation = mobs / (chunks + 1.0) * 1.28;
|
||||
}
|
||||
|
||||
private long[] loadedChunksNearPlayers(ServerLevel level) {
|
||||
Set<Long> keys = new HashSet<>();
|
||||
for (ServerPlayer player : level.players()) {
|
||||
int centerX = player.blockPosition().getX() >> 4;
|
||||
int centerZ = player.blockPosition().getZ() >> 4;
|
||||
for (int dx = -PLAYER_CHUNK_RADIUS; dx <= PLAYER_CHUNK_RADIUS; dx++) {
|
||||
for (int dz = -PLAYER_CHUNK_RADIUS; dz <= PLAYER_CHUNK_RADIUS; dz++) {
|
||||
int chunkX = centerX + dx;
|
||||
int chunkZ = centerZ + dz;
|
||||
if (level.getChunkSource().getChunkNow(chunkX, chunkZ) != null) {
|
||||
keys.add(pack(chunkX, chunkZ));
|
||||
}
|
||||
}
|
||||
int livingEntities = 0;
|
||||
for (Entity entity : level.getAllEntities()) {
|
||||
if (entity instanceof LivingEntity && entity.isAlive()) {
|
||||
livingEntities++;
|
||||
}
|
||||
}
|
||||
|
||||
long[] result = new long[keys.size()];
|
||||
int index = 0;
|
||||
for (long key : keys) {
|
||||
result[index++] = key;
|
||||
}
|
||||
return result;
|
||||
cachedEntityCount = livingEntities;
|
||||
cachedSaturation = livingEntities / (loadedChunks + 1.0) * 1.28;
|
||||
}
|
||||
|
||||
private long[] loadedChunkPositionsSnapshot(ServerLevel level) {
|
||||
LongArrayList positions = new LongArrayList(level.getChunkSource().getLoadedChunksCount());
|
||||
level.getChunkSource().chunkMap.forEachReadyToSendChunk(chunk -> positions.add(chunk.getPos().pack()));
|
||||
return positions.toLongArray();
|
||||
}
|
||||
|
||||
private boolean isPregenActive() {
|
||||
@@ -652,6 +684,14 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
return IrisSettings.get().getWorld().isAmbientEntitySpawningSystem();
|
||||
}
|
||||
|
||||
private boolean isEntitySpawningEnabledForCurrentWorld() {
|
||||
return entitySpawningEnabled(engine.isStudio(), IrisSettings.get().getStudio().isEnableEntitySpawning());
|
||||
}
|
||||
|
||||
static boolean entitySpawningEnabled(boolean studio, boolean studioSetting) {
|
||||
return !studio || studioSetting;
|
||||
}
|
||||
|
||||
private IrisEntitySpawn rarityPick(KList<IrisEntitySpawn> entries) {
|
||||
int totalRarity = 0;
|
||||
for (IrisEntitySpawn entry : entries) {
|
||||
@@ -710,24 +750,4 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
public void onSave() {
|
||||
engine.getMantle().save();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlockBreak(BlockBreakEvent e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlockPlace(BlockPlaceEvent e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChunkLoad(Chunk e, boolean generated) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChunkUnload(Chunk e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void teleportAsync(PlayerTeleportEvent e) {
|
||||
}
|
||||
}
|
||||
|
||||
+15
-13
@@ -16,20 +16,22 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.fabric.mixin;
|
||||
package art.arcane.iris.modded.api;
|
||||
|
||||
import art.arcane.iris.modded.ModdedDeathLoot;
|
||||
import net.minecraft.world.damagesource.DamageSource;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
@Mixin(LivingEntity.class)
|
||||
public class LivingEntityMixin {
|
||||
@Inject(method = "die", at = @At("TAIL"))
|
||||
private void iris$dropIrisDeathLoot(DamageSource damageSource, CallbackInfo info) {
|
||||
ModdedDeathLoot.handle((LivingEntity) (Object) this);
|
||||
import java.util.Objects;
|
||||
|
||||
public record ModdedBlockData(BlockState state, boolean deferredPlacement) {
|
||||
public ModdedBlockData {
|
||||
Objects.requireNonNull(state);
|
||||
}
|
||||
|
||||
public static ModdedBlockData direct(BlockState state) {
|
||||
return new ModdedBlockData(state, false);
|
||||
}
|
||||
|
||||
public static ModdedBlockData deferred(BlockState state) {
|
||||
return new ModdedBlockData(state, true);
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public record ModdedBlockPlacementContext(
|
||||
Engine engine,
|
||||
ServerLevel level,
|
||||
BlockPos position,
|
||||
Identifier blockId,
|
||||
Map<String, String> state,
|
||||
BlockState blockState) {
|
||||
public ModdedBlockPlacementContext {
|
||||
Objects.requireNonNull(engine);
|
||||
Objects.requireNonNull(level);
|
||||
Objects.requireNonNull(position);
|
||||
Objects.requireNonNull(blockId);
|
||||
state = Map.copyOf(state);
|
||||
Objects.requireNonNull(blockState);
|
||||
}
|
||||
}
|
||||
+27
-3
@@ -18,7 +18,9 @@
|
||||
|
||||
package art.arcane.iris.modded.api;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.modded.ModdedBlockResolution;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
@@ -96,7 +98,7 @@ public final class ModdedCustomContentRegistry {
|
||||
return !PROVIDERS.isEmpty() || !CUSTOM_BLOCKS.isEmpty();
|
||||
}
|
||||
|
||||
public static BlockState resolveBlock(String key) {
|
||||
public static ModdedBlockData resolveBlock(String key) {
|
||||
if (key == null || (PROVIDERS.isEmpty() && CUSTOM_BLOCKS.isEmpty())) {
|
||||
return null;
|
||||
}
|
||||
@@ -106,7 +108,7 @@ public final class ModdedCustomContentRegistry {
|
||||
}
|
||||
BlockState custom = CUSTOM_BLOCKS.get(base.toString());
|
||||
if (custom != null) {
|
||||
return custom;
|
||||
return ModdedBlockData.direct(custom);
|
||||
}
|
||||
if (PROVIDERS.isEmpty()) {
|
||||
return null;
|
||||
@@ -117,7 +119,7 @@ public final class ModdedCustomContentRegistry {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
BlockState resolved = provider.getBlockData(base, state);
|
||||
ModdedBlockData resolved = provider.getBlockData(base, state);
|
||||
if (resolved != null) {
|
||||
return resolved;
|
||||
}
|
||||
@@ -128,6 +130,28 @@ public final class ModdedCustomContentRegistry {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void processBlockPlacement(Engine engine, ServerLevel level, BlockPos position, String key) {
|
||||
Identifier base = parseIdentifier(key);
|
||||
if (base == null) {
|
||||
LOGGER.warn("Iris deferred custom block placement rejected invalid id {}", key);
|
||||
return;
|
||||
}
|
||||
Map<String, String> state = parseState(key);
|
||||
for (ModdedDataProvider provider : PROVIDERS) {
|
||||
if (!provider.isReady() || !provider.isValidProvider(base, ModdedDataType.BLOCK)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
provider.processBlockPlacement(new ModdedBlockPlacementContext(
|
||||
engine, level, position.immutable(), base, state, level.getBlockState(position)));
|
||||
} catch (Throwable error) {
|
||||
LOGGER.error("Iris custom content provider '{}' failed post-placement for {} at {}", provider.modId(), key, position, error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
LOGGER.warn("Iris deferred custom block placement has no provider for {}", key);
|
||||
}
|
||||
|
||||
public static Entity spawnMob(ServerLevel level, double x, double y, double z, String key) {
|
||||
if (PROVIDERS.isEmpty() || level == null || key == null) {
|
||||
return null;
|
||||
|
||||
+4
-2
@@ -21,7 +21,6 @@ package art.arcane.iris.modded.api;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
@@ -37,10 +36,13 @@ public interface ModdedDataProvider {
|
||||
|
||||
boolean isValidProvider(Identifier id, ModdedDataType type);
|
||||
|
||||
default BlockState getBlockData(Identifier blockId, Map<String, String> state) {
|
||||
default ModdedBlockData getBlockData(Identifier blockId, Map<String, String> state) {
|
||||
return null;
|
||||
}
|
||||
|
||||
default void processBlockPlacement(ModdedBlockPlacementContext context) {
|
||||
}
|
||||
|
||||
default Entity spawnMob(ServerLevel level, double x, double y, double z, Identifier entityId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
+266
-22
@@ -27,6 +27,7 @@ import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.framework.Locator;
|
||||
import art.arcane.iris.engine.framework.WrongEngineBroException;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisImportedStructureControl;
|
||||
import art.arcane.iris.engine.object.IrisPosition;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
@@ -40,6 +41,7 @@ import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.math.Position2;
|
||||
import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||
import com.mojang.brigadier.arguments.LongArgumentType;
|
||||
@@ -58,10 +60,14 @@ import net.minecraft.commands.Commands;
|
||||
import net.minecraft.commands.SharedSuggestionProvider;
|
||||
import net.minecraft.commands.arguments.DimensionArgument;
|
||||
import net.minecraft.commands.arguments.EntityArgument;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.particles.DustParticleOptions;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
@@ -71,6 +77,7 @@ import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
import org.slf4j.Logger;
|
||||
@@ -79,10 +86,13 @@ import org.slf4j.LoggerFactory;
|
||||
import java.awt.Desktop;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.function.Predicate;
|
||||
@@ -91,6 +101,7 @@ public final class IrisModdedCommands {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final Predicate<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
|
||||
private static final long LOCATE_TIMEOUT_MS = 120000L;
|
||||
private static final int NATIVE_STRUCTURE_LOCATE_RADIUS = 100;
|
||||
|
||||
private static final SuggestionProvider<CommandSourceStack> BIOME_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestBiomeKeys(context, builder);
|
||||
private static final SuggestionProvider<CommandSourceStack> REGION_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestRegionKeys(context, builder);
|
||||
@@ -869,36 +880,58 @@ public final class IrisModdedCommands {
|
||||
return 0;
|
||||
}
|
||||
String key = keyRaw.trim();
|
||||
Set<String> placed = IrisStructureLocator.placedKeys(engine);
|
||||
if (!IrisStructureLocator.isPlaced(engine, key)) {
|
||||
fail(source, "Structure " + key + " is not placed by this pack. Placed keys (" + placed.size() + "): "
|
||||
+ String.join(", ", placed.stream().limit(8).toList()) + (placed.size() > 8 ? ", ..." : ""));
|
||||
if (key.isEmpty()) {
|
||||
fail(source, "Name an Iris or native structure to locate.");
|
||||
return 0;
|
||||
}
|
||||
ServerPlayer player = source.getPlayer();
|
||||
if (player == null) {
|
||||
fail(source, "This command can only be used by players. (structure key '" + key + "' resolved; "
|
||||
+ placed.size() + " placed structure keys loaded)");
|
||||
fail(source, "This command can only be used by players.");
|
||||
return 0;
|
||||
}
|
||||
if (IrisStructureLocator.isPlaced(engine, key)) {
|
||||
locateIrisStructure(source, level, engine, player, key);
|
||||
return 1;
|
||||
}
|
||||
Optional<NativeStructureTarget> resolved = resolveNativeStructure(source, level, engine, key);
|
||||
if (resolved.isEmpty()) {
|
||||
fail(source, "Unknown structure '" + key + "'. Use tab completion to choose an Iris placement or a registered native/datapack structure.");
|
||||
return 0;
|
||||
}
|
||||
NativeStructureTarget target = resolved.get();
|
||||
if (target.availability() != NativeStructureAvailability.AVAILABLE) {
|
||||
fail(source, nativeUnavailableMessage(target.key(), target.availability()));
|
||||
return 0;
|
||||
}
|
||||
ok(source, "Searching for native structure " + target.key() + " within " + NATIVE_STRUCTURE_LOCATE_RADIUS + " chunks...");
|
||||
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();
|
||||
ok(source, "Searching for structure " + key + "...");
|
||||
ok(source, "Searching for Iris-placed structure " + key + "...");
|
||||
Thread thread = new Thread(() -> {
|
||||
try {
|
||||
int[] at = IrisStructureLocator.locate(engine, key, blockX, blockZ, 1024);
|
||||
if (at == null) {
|
||||
server.execute(() -> fail(source, "Could not find structure " + key + " within 1024 chunks."));
|
||||
IrisStructureLocator.LocateResult result =
|
||||
IrisStructureLocator.locate(engine, key, blockX, blockZ, 1024);
|
||||
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
|
||||
server.execute(() -> fail(source, "Unable to locate Iris-placed structure " + key
|
||||
+ ": the density search safety limit was reached before the full 1024-chunk radius was searched."));
|
||||
return;
|
||||
}
|
||||
int targetX = at[0] + 8;
|
||||
int targetY = at[1] + 2;
|
||||
int targetZ = at[2] + 8;
|
||||
server.execute(() -> {
|
||||
player.teleportTo(level, targetX + 0.5D, targetY, targetZ + 0.5D, Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
|
||||
ok(source, "Teleported to structure " + key + " at " + targetX + " " + targetY + " " + targetZ);
|
||||
});
|
||||
if (!result.found()) {
|
||||
server.execute(() -> fail(source, "Could not find Iris-placed structure " + key + " within 1024 chunks."));
|
||||
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(() -> fail(source, "Search failed: " + e.getClass().getSimpleName()));
|
||||
@@ -906,9 +939,196 @@ public final class IrisModdedCommands {
|
||||
}, "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) {
|
||||
fail(source, "Could not find native structure " + target.key() + " within "
|
||||
+ NATIVE_STRUCTURE_LOCATE_RADIUS + " chunks.");
|
||||
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);
|
||||
fail(source, "Search for native structure " + target.key() + " failed: " + 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()) {
|
||||
fail(source, "The player disconnected before the structure search completed.");
|
||||
return;
|
||||
}
|
||||
if (player.level() != level) {
|
||||
fail(source, "You changed dimensions before the structure search completed; run the 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) {
|
||||
fail(source, "Found " + label + " at " + targetX + " " + clampedY + " " + targetZ + ", but teleportation failed.");
|
||||
return;
|
||||
}
|
||||
ok(source, "Teleported to " + label + " at " + targetX + " " + clampedY + " " + targetZ);
|
||||
}
|
||||
|
||||
static int verifyStructures(CommandSourceStack source, String keyRaw) {
|
||||
ServerLevel level = source.getLevel();
|
||||
Engine engine = engineFor(level);
|
||||
if (engine == null) {
|
||||
fail(source, "This dimension is not generated by Iris.");
|
||||
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();
|
||||
ok(source, "Structure reachability: " + available + " native locatable, " + irisPlaced
|
||||
+ " Iris-placed, " + disabled + " native disabled, " + suppressed
|
||||
+ " native replaced by Iris placements, " + unreachableBiomes
|
||||
+ " native excluded by this pack's biomes, and " + unsupported
|
||||
+ " registered native structures unsupported in this dimension. Use /iris structure verify <key> for one structure.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int verifyStructure(CommandSourceStack source, ServerLevel level, Engine engine, String key) {
|
||||
if (IrisStructureLocator.isPlaced(engine, key)) {
|
||||
ok(source, "Structure " + key + " is Iris-placed and locatable with /iris goto structure " + key + ".");
|
||||
return 1;
|
||||
}
|
||||
Optional<NativeStructureTarget> target = resolveNativeStructure(source, level, engine, key);
|
||||
if (target.isEmpty()) {
|
||||
fail(source, "Unknown structure '" + key + "'. It is neither Iris-placed nor registered by vanilla or a datapack.");
|
||||
return 0;
|
||||
}
|
||||
NativeStructureTarget resolved = target.get();
|
||||
if (resolved.availability() != NativeStructureAvailability.AVAILABLE) {
|
||||
fail(source, nativeUnavailableMessage(resolved.key(), resolved.availability()));
|
||||
return 0;
|
||||
}
|
||||
ok(source, "Native structure " + resolved.key() + " is enabled, supported by this dimension's generator state, and locatable with /iris goto structure " + 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();
|
||||
IrisImportedStructureControl control = engine.getDimension().getImportedStructures();
|
||||
boolean selected = control != null && control.active() && control.shouldGenerate(key);
|
||||
boolean suppressed = IrisStructureLocator.suppressesVanilla(engine, key);
|
||||
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 -> "Native structure " + key + " is disabled by this dimension's importedStructures settings.";
|
||||
case IRIS_SUPPRESSED -> "Native structure " + key + " is replaced by an Iris placement in this pack; locate the Iris key instead.";
|
||||
case 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.";
|
||||
};
|
||||
}
|
||||
|
||||
private static int gotoPoi(CommandSourceStack source, String typeRaw) {
|
||||
ServerLevel level = source.getLevel();
|
||||
Engine engine = engineFor(level);
|
||||
@@ -991,7 +1211,7 @@ public final class IrisModdedCommands {
|
||||
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), pack, branch,
|
||||
(String message) -> server.execute(() -> ok(source, message)));
|
||||
if (installed) {
|
||||
server.execute(() -> ok(source, "Pack '" + pack + "' installed. Its dimension types and custom biomes join the forced Iris datapack on the next server restart; worlds created before restarting run with fallback heights until then."));
|
||||
server.execute(() -> ok(source, "Pack '" + pack + "' installed. Its exact dimension types and custom biomes join the forced Iris datapack on the next server restart; restart before creating worlds from this pack."));
|
||||
} else {
|
||||
server.execute(() -> fail(source, "Pack download failed for " + pack + " (" + downloadSource + "; see console)."));
|
||||
}
|
||||
@@ -1079,18 +1299,29 @@ public final class IrisModdedCommands {
|
||||
return builder.buildFuture();
|
||||
}
|
||||
|
||||
private static CompletableFuture<Suggestions> suggestStructureKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
|
||||
static CompletableFuture<Suggestions> suggestStructureKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
|
||||
ModdedCommandFeedback.tab(context.getSource());
|
||||
try {
|
||||
Engine engine = engineFor(context.getSource().getLevel());
|
||||
if (engine != null) {
|
||||
return SharedSuggestionProvider.suggest(IrisStructureLocator.placedKeys(engine), builder);
|
||||
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 ignored) {
|
||||
}
|
||||
return builder.buildFuture();
|
||||
}
|
||||
|
||||
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());
|
||||
List<String> names = new ArrayList<>();
|
||||
@@ -1132,4 +1363,17 @@ public final class IrisModdedCommands {
|
||||
static void fail(CommandSourceStack source, String message) {
|
||||
ModdedCommandFeedback.fail(source, message);
|
||||
}
|
||||
|
||||
enum NativeStructureAvailability {
|
||||
AVAILABLE,
|
||||
WORLD_DISABLED,
|
||||
FILTERED,
|
||||
IRIS_SUPPRESSED,
|
||||
BIOME_UNREACHABLE,
|
||||
NO_PLACEMENT
|
||||
}
|
||||
|
||||
private record NativeStructureTarget(String key, Holder.Reference<Structure> holder,
|
||||
NativeStructureAvailability availability) {
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -53,7 +53,7 @@ final class ModdedCommandHelp {
|
||||
Entry.command("version", "", "Print version information"),
|
||||
Entry.command("info", "[dimension]", "List loaded Iris dimensions and pack details"),
|
||||
Entry.command("what", "[block|hand|markers]", "Inspect the Iris biome, region, cave biome, surface and chunk at your position, the block you look at, your held item, or nearby markers"),
|
||||
Entry.group("find", "Find and teleport to Iris biomes, regions, objects, structures and points of interest", "goto"),
|
||||
Entry.group("find", "Find and teleport to Iris biomes, regions, objects, Iris structures, native structures and points of interest", "goto"),
|
||||
Entry.command("tp", "<dimension> [player]", "Teleport yourself or a named player into a loaded Iris dimension"),
|
||||
Entry.command("evacuate", "[dimension]", "Teleport every player out of an Iris dimension to the primary world spawn"),
|
||||
Entry.command("seed", "", "Print world and engine seed information"),
|
||||
@@ -79,7 +79,7 @@ final class ModdedCommandHelp {
|
||||
Entry.command("biome", "<key>", "Find an Iris biome"),
|
||||
Entry.command("region", "<key>", "Find an Iris region"),
|
||||
Entry.command("object", "<key>", "Find an object placement"),
|
||||
Entry.command("structure", "<key>", "Find an Iris-placed structure"),
|
||||
Entry.command("structure", "<key>", "Find an Iris-placed or native/datapack structure"),
|
||||
Entry.command("poi", "<type>", "Find a supported point of interest")
|
||||
));
|
||||
SECTIONS.put("goto", SECTIONS.get("find"));
|
||||
@@ -160,7 +160,7 @@ final class ModdedCommandHelp {
|
||||
Entry.command("place", "<key>", "Assemble and place an Iris structure at your location", "p"),
|
||||
Entry.command("import", "", "Explain Bukkit structure import workflow", "import-all", "reimport", "imp", "all"),
|
||||
Entry.command("capture", "", "Explain Bukkit structure capture workflow", "cap"),
|
||||
Entry.command("verify", "", "Explain modded structure locate behavior", "locateall")
|
||||
Entry.command("verify", "[key]", "Report native and Iris structure reachability in the current dimension", "locateall")
|
||||
));
|
||||
SECTIONS.put("struct", SECTIONS.get("structure"));
|
||||
SECTIONS.put("str", SECTIONS.get("structure"));
|
||||
|
||||
+3
-4
@@ -69,8 +69,8 @@ public final class ModdedDatapackCommands {
|
||||
root.then(Commands.literal("ls")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> list(context.getSource())));
|
||||
|
||||
root.then(message("ingest", "Modrinth datapack ingest requires the Bukkit plugin: its post-restart structure import into editable Iris resources uses Bukkit registries, and Iris modded dimensions do not run vanilla structure placement, so ingested structure datapacks would not generate. Drop datapacks into world/datapacks manually for non-structure content."));
|
||||
root.then(message("pull", "Modrinth datapack ingest requires the Bukkit plugin: its post-restart structure import into editable Iris resources uses Bukkit registries, and Iris modded dimensions do not run vanilla structure placement, so ingested structure datapacks would not generate. Drop datapacks into world/datapacks manually for non-structure content."));
|
||||
root.then(message("ingest", "Modrinth datapack ingest requires the Bukkit plugin because its editable-resource import and manifest workflow use Bukkit tooling. Iris modded dimensions do run native vanilla and datapack structure placement; install the datapack in world/datapacks and restart to generate its registered structures."));
|
||||
root.then(message("pull", "Modrinth datapack ingest requires the Bukkit plugin because its editable-resource import and manifest workflow use Bukkit tooling. Iris modded dimensions do run native vanilla and datapack structure placement; install the datapack in world/datapacks and restart to generate its registered structures."));
|
||||
|
||||
root.then(message("remove", "Datapack removal manages the Bukkit ingest manifest. On modded servers delete the datapack folder from world/datapacks and restart."));
|
||||
root.then(message("rm", "Datapack removal manages the Bukkit ingest manifest. On modded servers delete the datapack folder from world/datapacks and restart."));
|
||||
@@ -135,8 +135,7 @@ public final class ModdedDatapackCommands {
|
||||
+ (override.isFile() ? " (world datapack override installed)" : ""));
|
||||
if (!matches) {
|
||||
mismatches++;
|
||||
IrisModdedCommands.fail(source, " WARNING: the active dimension type does not match the pack. Terrain outside "
|
||||
+ activeMin + ".." + activeMax + " will be clipped. Run /iris world enable <dimension> <pack> for new worlds, or /iris datapack install for already-loaded Iris dimensions, then restart.");
|
||||
IrisModdedCommands.fail(source, " WARNING: the active dimension type does not match the pack. Iris will refuse to start this world engine instead of clipping terrain. Run /iris world enable <dimension> <pack> for new worlds, or /iris datapack install for already-loaded Iris dimensions, then restart.");
|
||||
}
|
||||
}
|
||||
if (irisLevels == 0) {
|
||||
|
||||
+14
-10
@@ -27,11 +27,10 @@ import art.arcane.iris.modded.ModdedBlockState;
|
||||
import art.arcane.iris.modded.ModdedTileData;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.NbtUtils;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.EntityBlock;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
@@ -148,26 +147,31 @@ final class ModdedObjectPlacer implements IObjectPlacer {
|
||||
skippedTiles++;
|
||||
return;
|
||||
}
|
||||
String snbt = moddedTile.snbt();
|
||||
if (snbt == null || snbt.isBlank()) {
|
||||
skippedTiles++;
|
||||
return;
|
||||
}
|
||||
BlockPos pos = new BlockPos(xx, yy, zz);
|
||||
BlockState state = level.getBlockState(pos);
|
||||
BlockState adjusted = moddedTile.adjustBlockState(state);
|
||||
if (adjusted != state) {
|
||||
level.setBlock(pos, adjusted, Block.UPDATE_CLIENTS | Block.UPDATE_KNOWN_SHAPE);
|
||||
state = adjusted;
|
||||
}
|
||||
if (!state.hasBlockEntity()) {
|
||||
skippedTiles++;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
CompoundTag tag = NbtUtils.snbtToStructure(snbt);
|
||||
BlockEntity restored = BlockEntity.loadStatic(pos, state, tag, level.registryAccess());
|
||||
BlockEntity restored = level.getBlockEntity(pos);
|
||||
if (restored == null && state.getBlock() instanceof EntityBlock entityBlock) {
|
||||
restored = entityBlock.newBlockEntity(pos, state);
|
||||
}
|
||||
if (restored == null) {
|
||||
skippedTiles++;
|
||||
return;
|
||||
}
|
||||
level.setBlockEntity(restored);
|
||||
restored.setChanged();
|
||||
if (!moddedTile.apply(restored, level)) {
|
||||
skippedTiles++;
|
||||
return;
|
||||
}
|
||||
restoredTiles++;
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris tile restore failed at {} {} {}", xx, yy, zz, e);
|
||||
|
||||
+8
-5
@@ -23,6 +23,8 @@ import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.ModdedBlockBuffer;
|
||||
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
||||
import art.arcane.iris.modded.service.ModdedChunkUpdateService;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
@@ -33,16 +35,13 @@ import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.server.level.ThreadedLevelLightEngine;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.EntityBlock;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
@@ -240,9 +239,13 @@ public final class ModdedRegen {
|
||||
lightEngine.updateSectionStatus(SectionPos.of(pos, chunk.getSectionYFromSectionIndex(i)), section.hasOnlyAir());
|
||||
}
|
||||
|
||||
ModdedChunkUpdateService updateService = ModdedEngineBootstrap.services().service(ModdedChunkUpdateService.class);
|
||||
if (updateService == null) {
|
||||
throw new IllegalStateException("Iris chunk update service is unavailable during regeneration");
|
||||
}
|
||||
updateService.updateRegeneratedChunk(engine, level, chunkX, chunkZ);
|
||||
Heightmap.primeHeightmaps(chunk, ChunkStatus.FULL.heightmapsAfter());
|
||||
Registry<Biome> biomeRegistry = level.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
chunk.fillBiomesFromNoise(generator.regenBiomeResolver(biomeRegistry, biomes, pos), level.getChunkSource().randomState().sampler());
|
||||
chunk.fillBiomesFromNoise(generator.regenBiomeResolver(), level.getChunkSource().randomState().sampler());
|
||||
chunk.markUnsaved();
|
||||
|
||||
for (BlockPos check : lightChecks) {
|
||||
|
||||
+11
-2
@@ -52,6 +52,7 @@ public final class ModdedStructureCommands {
|
||||
private static final Predicate<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
|
||||
|
||||
private static final SuggestionProvider<CommandSourceStack> IRIS_STRUCTURE_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestIrisStructureKeys(context, builder);
|
||||
private static final SuggestionProvider<CommandSourceStack> ALL_STRUCTURE_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> IrisModdedCommands.suggestStructureKeys(context, builder);
|
||||
|
||||
private ModdedStructureCommands() {
|
||||
}
|
||||
@@ -84,12 +85,20 @@ public final class ModdedStructureCommands {
|
||||
root.then(message("all", "Structure import rebuilds vanilla & datapack structures as editable Iris resources through Bukkit/NMS template managers; run /iris structure import on a Bukkit server against this pack, then copy the pack folder over."));
|
||||
root.then(message("capture", "Structure capture generates each structure in a throwaway Bukkit scratch world to read its blocks; it requires the Bukkit plugin (v26 NMS binding)."));
|
||||
root.then(message("cap", "Structure capture generates each structure in a throwaway Bukkit scratch world to read its blocks; it requires the Bukkit plugin (v26 NMS binding)."));
|
||||
root.then(message("verify", "Vanilla structure locate is meaningless here: Iris modded dimensions do not run vanilla structure placement (Iris places structures itself). Use /iris goto structure <key> to locate Iris-placed structures."));
|
||||
root.then(message("locateall", "Vanilla structure locate is meaningless here: Iris modded dimensions do not run vanilla structure placement (Iris places structures itself). Use /iris goto structure <key> to locate Iris-placed structures."));
|
||||
root.then(verifyTree("verify"));
|
||||
root.then(verifyTree("locateall"));
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> verifyTree(String name) {
|
||||
return Commands.literal(name)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.verifyStructures(context.getSource(), null))
|
||||
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ALL_STRUCTURE_KEYS)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.verifyStructures(
|
||||
context.getSource(), StringArgumentType.getString(context, "key"))));
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> message(String name, String text) {
|
||||
return Commands.literal(name)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> {
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package art.arcane.iris.modded.mixin;
|
||||
|
||||
import art.arcane.iris.modded.ModdedEntityPersistence;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@Mixin(Entity.class)
|
||||
public abstract class EntityPersistenceMixin {
|
||||
@Inject(method = "shouldBeSaved", at = @At("RETURN"), cancellable = true)
|
||||
private void iris$applyGeneratedPersistence(CallbackInfoReturnable<Boolean> info) {
|
||||
Entity entity = (Entity) (Object) this;
|
||||
info.setReturnValue(ModdedEntityPersistence.shouldSave(entity, info.getReturnValue()));
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package art.arcane.iris.modded.mixin;
|
||||
|
||||
import art.arcane.iris.modded.ModdedDeathLoot;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.damagesource.DamageSource;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(LivingEntity.class)
|
||||
public abstract class LivingEntityLootMixin {
|
||||
@Inject(method = "dropFromLootTable(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/damagesource/DamageSource;Z)V", at = @At("HEAD"), cancellable = true)
|
||||
private void iris$replaceBaseLoot(ServerLevel level, DamageSource damageSource, boolean playerKilled, CallbackInfo info) {
|
||||
if (ModdedDeathLoot.replaceBaseLoot((LivingEntity) (Object) this)) {
|
||||
info.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package art.arcane.iris.modded.mixin;
|
||||
|
||||
import art.arcane.iris.modded.ModdedEntityAwareness;
|
||||
import net.minecraft.world.entity.Mob;
|
||||
import net.minecraft.world.entity.ai.goal.FloatGoal;
|
||||
import net.minecraft.world.entity.ai.goal.WrappedGoal;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(Mob.class)
|
||||
public abstract class MobAwarenessMixin {
|
||||
@Inject(
|
||||
method = "serverAiStep",
|
||||
at = @At(
|
||||
value = "FIELD",
|
||||
target = "Lnet/minecraft/world/entity/Mob;noActionTime:I",
|
||||
opcode = Opcodes.PUTFIELD,
|
||||
shift = At.Shift.AFTER),
|
||||
cancellable = true)
|
||||
private void iris$tickUnawareMob(CallbackInfo info) {
|
||||
Mob mob = (Mob) (Object) this;
|
||||
if (ModdedEntityAwareness.isAware(mob)) {
|
||||
return;
|
||||
}
|
||||
for (WrappedGoal wrappedGoal : mob.getGoalSelector().getAvailableGoals()) {
|
||||
if (wrappedGoal.getGoal() instanceof FloatGoal floatGoal) {
|
||||
if (floatGoal.canUse()) {
|
||||
floatGoal.tick();
|
||||
}
|
||||
mob.getJumpControl().tick();
|
||||
break;
|
||||
}
|
||||
}
|
||||
info.cancel();
|
||||
}
|
||||
}
|
||||
+54
-19
@@ -20,6 +20,7 @@ package art.arcane.iris.modded.service;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.engine.data.cache.Cache;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.TileData;
|
||||
@@ -27,6 +28,7 @@ import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.ModdedBlockResolution;
|
||||
import art.arcane.iris.modded.ModdedLootApplier;
|
||||
import art.arcane.iris.modded.ModdedTileData;
|
||||
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.util.project.matter.TileWrapper;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
@@ -39,14 +41,13 @@ import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import art.arcane.volmlib.util.matter.MatterUpdate;
|
||||
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.NbtUtils;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.EntityBlock;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
@@ -148,11 +149,21 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||
}
|
||||
}
|
||||
|
||||
public void updateRegeneratedChunk(Engine engine, ServerLevel level, int chunkX, int chunkZ) {
|
||||
updateChunk(engine, level, chunkX, chunkZ, false);
|
||||
}
|
||||
|
||||
private void updateChunk(Engine engine, ServerLevel level, int chunkX, int chunkZ) {
|
||||
for (int x = -1; x <= 1; x++) {
|
||||
for (int z = -1; z <= 1; z++) {
|
||||
if (level.getChunkSource().getChunkNow(chunkX + x, chunkZ + z) == null) {
|
||||
return;
|
||||
updateChunk(engine, level, chunkX, chunkZ, true);
|
||||
}
|
||||
|
||||
private void updateChunk(Engine engine, ServerLevel level, int chunkX, int chunkZ, boolean requireNeighbors) {
|
||||
if (requireNeighbors) {
|
||||
for (int x = -1; x <= 1; x++) {
|
||||
for (int z = -1; z <= 1; z++) {
|
||||
if (level.getChunkSource().getChunkNow(chunkX + x, chunkZ + z) == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,6 +181,7 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||
try {
|
||||
chunk.raiseFlagUnchecked(MantleFlag.ETCHED, () -> {
|
||||
chunk.raiseFlagUnchecked(MantleFlag.TILE, () -> runTilePass(engine, level, chunkX, chunkZ, chunk));
|
||||
chunk.raiseFlagUnchecked(MantleFlag.CUSTOM, () -> runCustomPass(engine, level, chunkX, chunkZ, chunk));
|
||||
chunk.raiseFlagUnchecked(MantleFlag.UPDATE, () -> runUpdatePass(engine, level, chunkX, chunkZ, chunk));
|
||||
});
|
||||
} finally {
|
||||
@@ -183,34 +195,57 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||
int baseZ = chunkZ << 4;
|
||||
chunk.iterate(TileWrapper.class, (Integer x, Integer yf, Integer z, TileWrapper v) -> {
|
||||
int y = yf + minHeight;
|
||||
if (y < level.getMinY() || y > level.getMaxY()) {
|
||||
if (y < level.getMinY() || y >= level.getMaxY()) {
|
||||
return;
|
||||
}
|
||||
applyTile(level, baseX + (x & 15), y, baseZ + (z & 15), v.getData());
|
||||
});
|
||||
}
|
||||
|
||||
private void runCustomPass(Engine engine, ServerLevel level, int chunkX, int chunkZ, MantleChunk<Matter> chunk) {
|
||||
int minHeight = engine.getWorld().minHeight();
|
||||
int baseX = chunkX << 4;
|
||||
int baseZ = chunkZ << 4;
|
||||
chunk.iterate(Identifier.class, (Integer x, Integer yf, Integer z, Identifier identifier) -> {
|
||||
int y = yf + minHeight;
|
||||
if (y < level.getMinY() || y >= level.getMaxY()) {
|
||||
return;
|
||||
}
|
||||
BlockPos position = new BlockPos(baseX + (x & 15), y, baseZ + (z & 15));
|
||||
ModdedCustomContentRegistry.processBlockPlacement(engine, level, position, identifier.toString());
|
||||
});
|
||||
}
|
||||
|
||||
private void applyTile(ServerLevel level, int x, int y, int z, TileData tile) {
|
||||
if (!(tile instanceof ModdedTileData moddedTile)) {
|
||||
return;
|
||||
}
|
||||
String snbt = moddedTile.snbt();
|
||||
if (snbt == null || snbt.isBlank()) {
|
||||
return;
|
||||
}
|
||||
BlockPos pos = new BlockPos(x, y, z);
|
||||
BlockState state = level.getBlockState(pos);
|
||||
if (!state.hasBlockEntity()) {
|
||||
BlockState adjusted = moddedTile.adjustBlockState(state);
|
||||
if (adjusted != state) {
|
||||
level.setBlock(pos, adjusted, SILENT_FLAGS);
|
||||
state = adjusted;
|
||||
}
|
||||
if (!state.hasBlockEntity() || !(state.getBlock() instanceof EntityBlock entityBlock)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
CompoundTag tag = NbtUtils.snbtToStructure(snbt);
|
||||
BlockEntity restored = BlockEntity.loadStatic(pos, state, tag, level.registryAccess());
|
||||
if (restored == null) {
|
||||
BlockEntity blockEntity = level.getBlockEntity(pos);
|
||||
if (blockEntity == null) {
|
||||
blockEntity = entityBlock.newBlockEntity(pos, state);
|
||||
}
|
||||
if (blockEntity == null) {
|
||||
IrisLogging.warn("Iris could not create block entity at " + pos + " for " + state);
|
||||
return;
|
||||
}
|
||||
level.setBlockEntity(restored);
|
||||
restored.setChanged();
|
||||
if (!moddedTile.isApplicable(state, blockEntity)) {
|
||||
return;
|
||||
}
|
||||
level.setBlockEntity(blockEntity);
|
||||
if (!moddedTile.apply(blockEntity, level)) {
|
||||
IrisLogging.warn("Iris tile payload was empty at " + pos + " for " + state);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
@@ -255,7 +290,7 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||
RNG rng = new RNG(Cache.key(chunkX, chunkZ));
|
||||
chunk.iterate(MatterCavern.class, (Integer x, Integer yf, Integer z, MatterCavern v) -> {
|
||||
int y = yf + minHeight;
|
||||
if (y < level.getMinY() || y > level.getMaxY()) {
|
||||
if (y < level.getMinY() || y >= level.getMaxY()) {
|
||||
return;
|
||||
}
|
||||
int lx = x & 15;
|
||||
@@ -294,7 +329,7 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||
}
|
||||
|
||||
private void update(Engine engine, ServerLevel level, int x, int y, int z, int baseX, int baseZ, MantleChunk<Matter> chunk, RNG rng) {
|
||||
if (y < level.getMinY() || y > level.getMaxY()) {
|
||||
if (y < level.getMinY() || y >= level.getMaxY()) {
|
||||
return;
|
||||
}
|
||||
BlockPos pos = new BlockPos(baseX + (x & 15), y, baseZ + (z & 15));
|
||||
|
||||
+3
-2
@@ -31,9 +31,10 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
@@ -47,7 +48,7 @@ public final class ModdedEngineMaintenanceService implements ModdedTickableServi
|
||||
private static final long SAVE_PERIOD_MILLIS = 60_000L;
|
||||
|
||||
private final AtomicInteger tectonicLimit = new AtomicInteger(30);
|
||||
private final Set<Engine> inFlight = ConcurrentHashMap.newKeySet();
|
||||
private final Set<Engine> inFlight = Collections.synchronizedSet(Collections.newSetFromMap(new IdentityHashMap<>()));
|
||||
private volatile ExecutorService service;
|
||||
private long lastMaintenanceAt;
|
||||
private long lastSaveAt;
|
||||
|
||||
+75
-6
@@ -20,9 +20,15 @@ package art.arcane.iris.modded.service;
|
||||
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.tools.WorldMaintenance;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EnginePlatformHooks;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.ModdedDimensionManager;
|
||||
import art.arcane.iris.modded.ModdedForcedDatapack;
|
||||
import art.arcane.iris.modded.ModdedWorkspaceGenerator;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.io.ReactiveFolder;
|
||||
@@ -41,7 +47,7 @@ import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public final class ModdedStudioHotloadService implements ModdedTickableService {
|
||||
public final class ModdedStudioHotloadService implements ModdedTickableService, EnginePlatformHooks {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final String STUDIO_DIMENSION_PREFIX = "irisworldgen:studio_";
|
||||
private static final long POLL_MILLIS = 250L;
|
||||
@@ -76,6 +82,34 @@ public final class ModdedStudioHotloadService implements ModdedTickableService {
|
||||
watches.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshWorkspace(Engine engine) {
|
||||
writeWorkspace(engine, "workspace refresh");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshDatapackWorkspace(Engine engine) {
|
||||
IrisData data = engine.getData();
|
||||
KList<IrisDimension> dimensions = data.getDimensionLoader().loadAll(data.getDimensionLoader().getPossibleKeys());
|
||||
if (hasDatapackImports(dimensions)) {
|
||||
writeWorkspace(engine, "datapack workspace refresh");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reloadDatapacks(Engine engine) {
|
||||
ModdedForcedDatapack.regenerate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateDimensionHotload(Engine engine, IrisDimension replacement) {
|
||||
IrisDimensionRuntimeContract.requireHotloadCompatible(
|
||||
"Modded Studio level '" + engine.getWorld().identity() + "'",
|
||||
engine.getDimension(),
|
||||
replacement,
|
||||
"irisworldgen");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServerTick(MinecraftServer server) {
|
||||
ExecutorService active = executor;
|
||||
@@ -134,12 +168,34 @@ public final class ModdedStudioHotloadService implements ModdedTickableService {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPregeneratorActive(Engine engine) {
|
||||
IrisWorld world = engine.getWorld();
|
||||
if (world == null) {
|
||||
return false;
|
||||
}
|
||||
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
|
||||
return pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(world.identity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdownPregenerator(Engine engine) {
|
||||
PregeneratorJob.shutdownInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldSkipMantleCleanup(Engine engine) {
|
||||
IrisWorld world = engine.getWorld();
|
||||
return world != null
|
||||
&& WorldMaintenance.isWorldMaintenanceActive(world.identity())
|
||||
&& !isPregeneratorActive(engine);
|
||||
}
|
||||
|
||||
private boolean throttled(IrisModdedChunkGenerator generator, Engine engine, long now) {
|
||||
if (now - generator.lastChunkGenAt() < RECENT_GENERATION_HOLDOFF_MILLIS) {
|
||||
return true;
|
||||
}
|
||||
PregeneratorJob job = PregeneratorJob.getInstance();
|
||||
return job != null && job.targetsWorldIdentity(engine.getWorld().identity());
|
||||
return isPregeneratorActive(engine);
|
||||
}
|
||||
|
||||
private void poll(String dimensionId, Watch watch, IrisModdedChunkGenerator generator, Engine engine) {
|
||||
@@ -171,19 +227,32 @@ public final class ModdedStudioHotloadService implements ModdedTickableService {
|
||||
try {
|
||||
engine.hotloadSilently();
|
||||
generator.onHotload();
|
||||
regenerateSchemas(dimensionId, engine);
|
||||
LOGGER.info("Iris studio hotload {} pack={} {}ms", dimensionId, engine.getDimension().getLoadKey(), System.currentTimeMillis() - start);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris studio hotload failed for {}", dimensionId, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void regenerateSchemas(String dimensionId, Engine engine) {
|
||||
static boolean hasDatapackImports(Iterable<IrisDimension> dimensions) {
|
||||
if (dimensions == null) {
|
||||
return false;
|
||||
}
|
||||
for (IrisDimension dimension : dimensions) {
|
||||
if (dimension != null
|
||||
&& dimension.getDatapackImports() != null
|
||||
&& !dimension.getDatapackImports().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void writeWorkspace(Engine engine, String operation) {
|
||||
try {
|
||||
IrisData data = engine.getData();
|
||||
ModdedWorkspaceGenerator.writeWorkspace(data, data.getDataFolder());
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris studio schema regeneration failed for {}", dimensionId, e);
|
||||
LOGGER.error("Iris {} failed for {}", operation, engine.getDimension().getLoadKey(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
{
|
||||
"ambient_light": 0.0,
|
||||
"attributes": {
|
||||
"minecraft:audio/ambient_sounds": {
|
||||
"mood": {
|
||||
"block_search_extent": 8,
|
||||
"offset": 2.0,
|
||||
"sound": "minecraft:ambient.cave",
|
||||
"tick_delay": 6000
|
||||
}
|
||||
},
|
||||
"minecraft:audio/background_music": {
|
||||
"creative": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.creative"
|
||||
},
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.game"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/bed_rule": {
|
||||
"can_set_spawn": "always",
|
||||
"can_sleep": "when_dark",
|
||||
"error_message": {
|
||||
"translate": "block.minecraft.bed.no_sleep"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/nether_portal_spawns_piglin": true,
|
||||
"minecraft:gameplay/respawn_anchor_works": false,
|
||||
"minecraft:visual/ambient_light_color": "#0a0a0a",
|
||||
"minecraft:visual/cloud_color": "#ccffffff",
|
||||
"minecraft:visual/cloud_height": 192.33,
|
||||
"minecraft:visual/fog_color": "#c0d8ff",
|
||||
"minecraft:visual/sky_color": "#78a7ff"
|
||||
},
|
||||
"coordinate_scale": 1.0,
|
||||
"default_clock": "minecraft:overworld",
|
||||
"has_ceiling": false,
|
||||
"has_ender_dragon_fight": false,
|
||||
"has_skylight": true,
|
||||
"height": 768,
|
||||
"infiniburn": "#minecraft:infiniburn_overworld",
|
||||
"logical_height": 512,
|
||||
"min_y": -256,
|
||||
"monster_spawn_block_light_limit": 0,
|
||||
"monster_spawn_light_level": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 7,
|
||||
"min_inclusive": 0
|
||||
},
|
||||
"timelines": "#minecraft:in_overworld"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"required": true,
|
||||
"minVersion": "0.8",
|
||||
"package": "art.arcane.iris.modded.mixin",
|
||||
"compatibilityLevel": "JAVA_21",
|
||||
"mixins": [
|
||||
"EntityPersistenceMixin",
|
||||
"LivingEntityLootMixin",
|
||||
"MobAwarenessMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
}
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import com.mojang.serialization.Codec;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.IdMapper;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.component.DataComponentMap;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeGenerationSettings;
|
||||
import net.minecraft.world.level.biome.BiomeSpecialEffects;
|
||||
import net.minecraft.world.level.biome.MobSpawnSettings;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.entity.BannerBlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.ChestBlockEntity;
|
||||
import net.minecraft.world.level.block.entity.SignBlockEntity;
|
||||
import net.minecraft.world.level.block.entity.SpawnerBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.PalettedContainerFactory;
|
||||
import net.minecraft.world.level.chunk.PalettedContainer;
|
||||
import net.minecraft.world.level.chunk.PalettedContainerRO;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.Strategy;
|
||||
import net.minecraft.world.level.chunk.UpgradeData;
|
||||
import net.minecraft.world.level.storage.TagValueInput;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisModdedBlockEntityParityTest {
|
||||
private static RegistryAccess registries;
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
registries = RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY);
|
||||
if (!Items.DIAMOND.builtInRegistryHolder().areComponentsBound()) {
|
||||
Items.DIAMOND.builtInRegistryHolder().bindComponents(DataComponentMap.EMPTY);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generatedContainerGetsBlockEntityAndPersistsFilledLoot() {
|
||||
ProtoChunk chunk = newChunk();
|
||||
BlockPos position = new BlockPos(3, 70, 5);
|
||||
chunk.setBlockState(position, Blocks.CHEST.defaultBlockState(), 0);
|
||||
|
||||
IrisModdedChunkGenerator.createDefaultBlockEntity(chunk, position, Blocks.CHEST.defaultBlockState());
|
||||
|
||||
BlockEntity blockEntity = chunk.getBlockEntity(position);
|
||||
assertNotNull(blockEntity);
|
||||
assertTrue(blockEntity instanceof Container);
|
||||
Container container = (Container) blockEntity;
|
||||
ModdedLootApplier.fillContainer(container, List.of(new ItemStack(Items.DIAMOND)), new RNG(17L));
|
||||
assertEquals(1, countItem(container, Items.DIAMOND));
|
||||
CompoundTag saved = blockEntity.saveWithoutMetadata(registries);
|
||||
ChestBlockEntity restored = new ChestBlockEntity(position, Blocks.CHEST.defaultBlockState());
|
||||
restored.loadWithComponents(TagValueInput.create(ProblemReporter.DISCARDING, registries, saved));
|
||||
assertEquals(1, countItem(restored, Items.DIAMOND));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generatedSignSpawnerAndBannerGetNativeBlockEntities() {
|
||||
ProtoChunk chunk = newChunk();
|
||||
BlockState signState = Blocks.OAK_SIGN.defaultBlockState();
|
||||
BlockState spawnerState = Blocks.SPAWNER.defaultBlockState();
|
||||
BlockState bannerState = BuiltInRegistries.BLOCK.getValue(
|
||||
Identifier.parse("minecraft:white_banner")).defaultBlockState();
|
||||
BlockPos signPosition = new BlockPos(1, 70, 1);
|
||||
BlockPos spawnerPosition = new BlockPos(2, 70, 1);
|
||||
BlockPos bannerPosition = new BlockPos(3, 70, 1);
|
||||
|
||||
chunk.setBlockState(signPosition, signState, 0);
|
||||
chunk.setBlockState(spawnerPosition, spawnerState, 0);
|
||||
chunk.setBlockState(bannerPosition, bannerState, 0);
|
||||
IrisModdedChunkGenerator.createDefaultBlockEntity(chunk, signPosition, signState);
|
||||
IrisModdedChunkGenerator.createDefaultBlockEntity(chunk, spawnerPosition, spawnerState);
|
||||
IrisModdedChunkGenerator.createDefaultBlockEntity(chunk, bannerPosition, bannerState);
|
||||
|
||||
assertTrue(chunk.getBlockEntity(signPosition) instanceof SignBlockEntity);
|
||||
assertTrue(chunk.getBlockEntity(spawnerPosition) instanceof SpawnerBlockEntity);
|
||||
assertTrue(chunk.getBlockEntity(bannerPosition) instanceof BannerBlockEntity);
|
||||
}
|
||||
|
||||
private static ProtoChunk newChunk() {
|
||||
return new ProtoChunk(
|
||||
new ChunkPos(0, 0),
|
||||
UpgradeData.EMPTY,
|
||||
LevelHeightAccessor.create(-64, 384),
|
||||
palettedContainerFactory(),
|
||||
null);
|
||||
}
|
||||
|
||||
private static PalettedContainerFactory palettedContainerFactory() {
|
||||
Strategy<BlockState> blockStrategy = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY);
|
||||
Codec<PalettedContainer<BlockState>> blockCodec = PalettedContainer.codecRW(
|
||||
BlockState.CODEC, blockStrategy, Blocks.AIR.defaultBlockState());
|
||||
Biome biome = new Biome.BiomeBuilder()
|
||||
.hasPrecipitation(false)
|
||||
.temperature(0.8F)
|
||||
.downfall(0.4F)
|
||||
.specialEffects(new BiomeSpecialEffects.Builder().waterColor(0x3F76E4).build())
|
||||
.mobSpawnSettings(MobSpawnSettings.EMPTY)
|
||||
.generationSettings(BiomeGenerationSettings.EMPTY)
|
||||
.build();
|
||||
Holder<Biome> biomeHolder = Holder.direct(biome);
|
||||
IdMapper<Holder<Biome>> biomeIds = new IdMapper<>(1);
|
||||
biomeIds.add(biomeHolder);
|
||||
Strategy<Holder<Biome>> biomeStrategy = Strategy.createForBiomes(biomeIds);
|
||||
Codec<PalettedContainerRO<Holder<Biome>>> biomeCodec = PalettedContainer.codecRO(
|
||||
Biome.CODEC, biomeStrategy, biomeHolder);
|
||||
return new PalettedContainerFactory(
|
||||
blockStrategy,
|
||||
Blocks.AIR.defaultBlockState(),
|
||||
blockCodec,
|
||||
biomeStrategy,
|
||||
biomeHolder,
|
||||
biomeCodec);
|
||||
}
|
||||
|
||||
private static int countItem(Container container, Item item) {
|
||||
int count = 0;
|
||||
for (int slot = 0; slot < container.getContainerSize(); slot++) {
|
||||
ItemStack stack = container.getItem(slot);
|
||||
if (stack.getItem() == item) {
|
||||
count += stack.getCount();
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user