mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 04:37:47 +00:00
ADA
This commit is contained in:
+20
@@ -23,6 +23,7 @@ import art.arcane.iris.nativegen.NativeStructureTerrainIntegrator;
|
|||||||
import art.arcane.iris.nativegen.NativeStructureVegetationClearer;
|
import art.arcane.iris.nativegen.NativeStructureVegetationClearer;
|
||||||
import art.arcane.iris.nativegen.NativeStructureVerticalPlacer;
|
import art.arcane.iris.nativegen.NativeStructureVerticalPlacer;
|
||||||
import art.arcane.iris.nativegen.NativeStructureVanillaLocator;
|
import art.arcane.iris.nativegen.NativeStructureVanillaLocator;
|
||||||
|
import art.arcane.iris.nativegen.NativeStructureVolumeIndex;
|
||||||
import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;
|
import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;
|
||||||
import art.arcane.iris.spi.PlatformBlockState;
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||||
@@ -87,6 +88,7 @@ import org.bukkit.block.data.BlockData;
|
|||||||
import org.spigotmc.SpigotWorldConfig;
|
import org.spigotmc.SpigotWorldConfig;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -130,6 +132,24 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
this.runtimeMinY = level.getMinY();
|
this.runtimeMinY = level.getMinY();
|
||||||
this.runtimeHeight = level.getHeight();
|
this.runtimeHeight = level.getHeight();
|
||||||
this.runtimeSeaLevel = runtimeMinY + engine.getDimension().getFluidHeight();
|
this.runtimeSeaLevel = runtimeMinY + engine.getDimension().getFluidHeight();
|
||||||
|
installNativeStructureVolumeIndex(level);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void installNativeStructureVolumeIndex(ServerLevel level) {
|
||||||
|
WeakReference<ServerLevel> levelReference = new WeakReference<>(level);
|
||||||
|
WeakReference<ChunkGenerator> generatorReference = new WeakReference<>(this);
|
||||||
|
WeakReference<BiomeSource> biomeSourceReference = new WeakReference<>(customBiomeSource);
|
||||||
|
NativeStructureVolumeIndex.install(engine, new NativeStructureVolumeIndex.Context(
|
||||||
|
level.registryAccess(),
|
||||||
|
level.getServer().getStructureManager(),
|
||||||
|
level.dimension(),
|
||||||
|
LevelHeightAccessor.create(runtimeMinY, runtimeHeight),
|
||||||
|
generatorReference::get,
|
||||||
|
biomeSourceReference::get,
|
||||||
|
() -> {
|
||||||
|
ServerLevel active = levelReference.get();
|
||||||
|
return active == null ? null : active.getChunkSource().getGeneratorState();
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+7
@@ -13,6 +13,8 @@ import art.arcane.iris.core.nms.datapack.DataVersion;
|
|||||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||||
import art.arcane.iris.engine.data.chunk.TerrainChunk;
|
import art.arcane.iris.engine.data.chunk.TerrainChunk;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.framework.NativeStructureVolume;
|
||||||
|
import art.arcane.iris.nativegen.NativeStructureVolumeIndex;
|
||||||
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
|
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
|
||||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||||
import art.arcane.iris.nativegen.NativeStructureFactory;
|
import art.arcane.iris.nativegen.NativeStructureFactory;
|
||||||
@@ -794,6 +796,11 @@ public class NMSBinding implements INMSBinding {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KList<NativeStructureVolume> nativeStructureVolumes(Engine engine, int minX, int minZ, int maxX, int maxZ) {
|
||||||
|
return NativeStructureVolumeIndex.volumes(engine, minX, minZ, maxX, maxZ);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getBiomeId(Biome biome) {
|
public int getBiomeId(Biome biome) {
|
||||||
for (World i : Bukkit.getWorlds()) {
|
for (World i : Bukkit.getWorlds()) {
|
||||||
|
|||||||
+233
@@ -0,0 +1,233 @@
|
|||||||
|
package art.arcane.iris.nativegen;
|
||||||
|
|
||||||
|
import com.mojang.datafixers.util.Either;
|
||||||
|
import com.mojang.serialization.MapCodec;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.util.valueproviders.ConstantInt;
|
||||||
|
import net.minecraft.world.level.LevelReader;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.Rotation;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.BlackstoneReplaceProcessor;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.BlockAgeProcessor;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.BlockIgnoreProcessor;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.CappedProcessor;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.GravityProcessor;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.JigsawReplacementProcessor;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.LavaSubmergedBlockProcessor;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.NopProcessor;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.ProtectedBlockProcessor;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.RuleProcessor;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.InvocationHandler;
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class NativeStructureTemplateOccupancyTest {
|
||||||
|
private static final BoundingBox PROCESSING_AREA = new BoundingBox(0, 56, 0, 15, 80, 0);
|
||||||
|
private static final int TEMPLATE_WIDTH = 32;
|
||||||
|
private static final int TERRAIN_HEIGHT = 64;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraft() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void straddlingGravityPieceReadsHeightsOnlyInsideTheProcessingArea() throws Exception {
|
||||||
|
ColumnRecorder recorder = new ColumnRecorder();
|
||||||
|
|
||||||
|
resolve(straddlingPiece(
|
||||||
|
List.of(new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, 0))), recorder);
|
||||||
|
|
||||||
|
assertEquals(columns(0, 15), recorder.heightColumns);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void straddlingPieceRunsProcessorsOnlyInsideTheProcessingArea() throws Exception {
|
||||||
|
CountingProcessor counter = new CountingProcessor();
|
||||||
|
|
||||||
|
resolve(straddlingPiece(List.of(counter)), new ColumnRecorder());
|
||||||
|
|
||||||
|
assertEquals(columns(0, 15), counter.columns);
|
||||||
|
assertTrue(counter.columns.size() < TEMPLATE_WIDTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cappedStraddlingPieceRunsProcessorsAcrossTheWholePiece() throws Exception {
|
||||||
|
CountingProcessor counter = new CountingProcessor();
|
||||||
|
|
||||||
|
resolve(straddlingPiece(List.of(counter,
|
||||||
|
new CappedProcessor(NopProcessor.INSTANCE, ConstantInt.of(0)))),
|
||||||
|
new ColumnRecorder());
|
||||||
|
|
||||||
|
assertEquals(columns(0, TEMPLATE_WIDTH - 1), counter.columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cappedProcessorIsTheOnlyProcessorThatDisablesTheProcessingAreaClip() {
|
||||||
|
for (StructureProcessor processor : List.of(
|
||||||
|
BlockIgnoreProcessor.STRUCTURE_BLOCK,
|
||||||
|
JigsawReplacementProcessor.INSTANCE,
|
||||||
|
LavaSubmergedBlockProcessor.INSTANCE,
|
||||||
|
NopProcessor.INSTANCE,
|
||||||
|
BlackstoneReplaceProcessor.INSTANCE,
|
||||||
|
new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, 0),
|
||||||
|
new RuleProcessor(List.of()),
|
||||||
|
new BlockAgeProcessor(0.5F),
|
||||||
|
new ProtectedBlockProcessor(HolderSet.empty()))) {
|
||||||
|
assertFalse(processor.getClass().getName(),
|
||||||
|
processor.evaluatesEntirePieceState());
|
||||||
|
}
|
||||||
|
assertTrue(new CappedProcessor(NopProcessor.INSTANCE, ConstantInt.of(4))
|
||||||
|
.evaluatesEntirePieceState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void placementSettingsCarryTheProcessingAreaAsTheirClipBox() throws Exception {
|
||||||
|
PoolElementStructurePiece piece = straddlingPiece(List.of());
|
||||||
|
|
||||||
|
StructurePlaceSettings settings = NativeStructureReflection.resolvePlacementSettings(
|
||||||
|
(SinglePoolElement) piece.getElement(), piece, PROCESSING_AREA);
|
||||||
|
|
||||||
|
assertEquals(PROCESSING_AREA, settings.getBoundingBox());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void resolve(PoolElementStructurePiece piece, ColumnRecorder recorder) {
|
||||||
|
NativeStructureTemplateOccupancy.resolve(
|
||||||
|
world(recorder), piece, piece.getPosition(), PROCESSING_AREA,
|
||||||
|
NativeStructureTemplateOccupancyTest::forbiddenTemplateManager,
|
||||||
|
PROCESSING_AREA::isInside, amount -> {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PoolElementStructurePiece straddlingPiece(
|
||||||
|
List<StructureProcessor> processors) throws Exception {
|
||||||
|
BoundingBox bounds = new BoundingBox(0, 70, 0, TEMPLATE_WIDTH - 1, 76, 0);
|
||||||
|
return new PoolElementStructurePiece(
|
||||||
|
null, new InlineSinglePoolElement(template(), processors),
|
||||||
|
new BlockPos(bounds.minX(), bounds.minY(), bounds.minZ()),
|
||||||
|
1, Rotation.NONE, bounds, LiquidSettings.APPLY_WATERLOGGING);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<Integer> columns(int minimumX, int maximumX) {
|
||||||
|
Set<Integer> columns = new TreeSet<>();
|
||||||
|
for (int x = minimumX; x <= maximumX; x++) {
|
||||||
|
columns.add(x);
|
||||||
|
}
|
||||||
|
return columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureTemplateManager forbiddenTemplateManager() {
|
||||||
|
throw new AssertionError("Inline templates must not resolve a template manager");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureTemplate template() throws Exception {
|
||||||
|
List<StructureTemplate.StructureBlockInfo> blocks = new ArrayList<>();
|
||||||
|
for (int x = 0; x < TEMPLATE_WIDTH; x++) {
|
||||||
|
blocks.add(new StructureTemplate.StructureBlockInfo(
|
||||||
|
new BlockPos(x, 0, 0), Blocks.COBBLESTONE.defaultBlockState(), null));
|
||||||
|
}
|
||||||
|
Constructor<StructureTemplate.Palette> constructor =
|
||||||
|
StructureTemplate.Palette.class.getDeclaredConstructor(List.class);
|
||||||
|
assertTrue(constructor.trySetAccessible());
|
||||||
|
StructureTemplate template = new StructureTemplate();
|
||||||
|
template.palettes.add(constructor.newInstance(blocks));
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorldGenLevel world(ColumnRecorder recorder) {
|
||||||
|
InvocationHandler handler = (proxy, method, arguments) -> {
|
||||||
|
String methodName = method.getName();
|
||||||
|
if (methodName.equals("getHeight") && arguments.length == 3) {
|
||||||
|
recorder.heightColumns.add((Integer) arguments[1]);
|
||||||
|
return TERRAIN_HEIGHT;
|
||||||
|
}
|
||||||
|
if (methodName.equals("getBlockState")) {
|
||||||
|
recorder.stateColumns.add(((BlockPos) arguments[0]).getX());
|
||||||
|
return Blocks.STONE.defaultBlockState();
|
||||||
|
}
|
||||||
|
if (methodName.equals("getLevel")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (methodName.equals("holderLookup")) {
|
||||||
|
return BuiltInRegistries.BLOCK;
|
||||||
|
}
|
||||||
|
if (methodName.equals("hashCode")) {
|
||||||
|
return System.identityHashCode(proxy);
|
||||||
|
}
|
||||||
|
if (methodName.equals("equals")) {
|
||||||
|
return proxy == arguments[0];
|
||||||
|
}
|
||||||
|
if (methodName.equals("toString")) {
|
||||||
|
return "occupancy-test-world";
|
||||||
|
}
|
||||||
|
throw new UnsupportedOperationException(method.toString());
|
||||||
|
};
|
||||||
|
return (WorldGenLevel) Proxy.newProxyInstance(
|
||||||
|
WorldGenLevel.class.getClassLoader(),
|
||||||
|
new Class<?>[]{WorldGenLevel.class}, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ColumnRecorder {
|
||||||
|
private final Set<Integer> heightColumns = new TreeSet<>();
|
||||||
|
private final Set<Integer> stateColumns = new TreeSet<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class CountingProcessor implements StructureProcessor {
|
||||||
|
private final Set<Integer> columns = new TreeSet<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StructureTemplate.StructureBlockInfo processBlock(
|
||||||
|
LevelReader level, BlockPos targetPosition, BlockPos referencePos,
|
||||||
|
BlockPos templateRelativePos,
|
||||||
|
StructureTemplate.StructureBlockInfo processedBlockInfo,
|
||||||
|
StructurePlaceSettings settings) {
|
||||||
|
columns.add(processedBlockInfo.pos().getX());
|
||||||
|
return processedBlockInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MapCodec<? extends StructureProcessor> codec() {
|
||||||
|
return BlockIgnoreProcessor.STRUCTURE_BLOCK.codec();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class InlineSinglePoolElement extends SinglePoolElement {
|
||||||
|
private InlineSinglePoolElement(
|
||||||
|
StructureTemplate template, List<StructureProcessor> processors) {
|
||||||
|
super(Either.right(template),
|
||||||
|
Holder.direct(new StructureProcessorList(processors)),
|
||||||
|
StructureTemplatePool.Projection.RIGID,
|
||||||
|
Optional.<LiquidSettings>empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+160
@@ -0,0 +1,160 @@
|
|||||||
|
package art.arcane.iris.nativegen;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.framework.NativeStructureVolume;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
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.pieces.PiecesContainer;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class NativeStructureVolumeIndexTest {
|
||||||
|
private static final String STRUCTURE_KEY = "minecraft:swamp_hut";
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraftRegistries() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void assembledPiecesBecomeWorldSpacePieceVolumes() {
|
||||||
|
StructureStart start = swampHut(0, 0, 4, 6);
|
||||||
|
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
|
||||||
|
|
||||||
|
KList<NativeStructureVolume> volumes = NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, start);
|
||||||
|
|
||||||
|
assertEquals(1, volumes.size());
|
||||||
|
NativeStructureVolume volume = volumes.getFirst();
|
||||||
|
assertEquals(STRUCTURE_KEY, volume.structure());
|
||||||
|
assertEquals(bounds.minX(), volume.minX());
|
||||||
|
assertEquals(bounds.minY(), volume.minY());
|
||||||
|
assertEquals(bounds.minZ(), volume.minZ());
|
||||||
|
assertEquals(bounds.maxX(), volume.maxX());
|
||||||
|
assertEquals(bounds.maxY(), volume.maxY());
|
||||||
|
assertEquals(bounds.maxZ(), volume.maxZ());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void invalidStartsContributeNoVolumes() {
|
||||||
|
assertNull(NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, StructureStart.INVALID_START));
|
||||||
|
assertNull(NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void coldAndCachedQueriesResolveIdenticalVolumes() {
|
||||||
|
CountingResolver resolver = new CountingResolver(0, 0);
|
||||||
|
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
|
||||||
|
|
||||||
|
KList<NativeStructureVolume> cold = index.resolve(null, 0, 0, 15, 15);
|
||||||
|
int coldResolutions = resolver.resolutions();
|
||||||
|
KList<NativeStructureVolume> cached = index.resolve(null, 0, 0, 15, 15);
|
||||||
|
|
||||||
|
assertTrue(coldResolutions > 0);
|
||||||
|
assertFalse(cold.isEmpty());
|
||||||
|
assertEquals(cold, cached);
|
||||||
|
assertEquals(coldResolutions, resolver.resolutions());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void independentIndexesResolveIdenticalVolumesFromTheSameSeed() {
|
||||||
|
NativeStructureVolumeIndex first = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
|
||||||
|
NativeStructureVolumeIndex second = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
|
||||||
|
|
||||||
|
assertEquals(first.resolve(null, 0, 0, 15, 15), second.resolve(null, 0, 0, 15, 15));
|
||||||
|
assertEquals(first.resolve(null, -32, -32, 47, 47), second.resolve(null, -32, -32, 47, 47));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void volumesOutsideTheQueryRectAreExcluded() {
|
||||||
|
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
|
||||||
|
BoundingBox bounds = swampHut(0, 0, 4, 6).getPieces().getFirst().getBoundingBox();
|
||||||
|
|
||||||
|
KList<NativeStructureVolume> hit = index.resolve(null, bounds.minX(), bounds.minZ(), bounds.maxX(), bounds.maxZ());
|
||||||
|
KList<NativeStructureVolume> miss = index.resolve(null,
|
||||||
|
bounds.maxX() + 1, bounds.minZ(), bounds.maxX() + 8, bounds.maxZ());
|
||||||
|
|
||||||
|
assertEquals(1, hit.size());
|
||||||
|
assertTrue(miss.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void startsWithinTheOriginReachContributeAndBeyondItDoNot() {
|
||||||
|
int reach = NativeStructureVolumeIndex.originReachChunks();
|
||||||
|
NativeStructureVolumeIndex reachable = NativeStructureVolumeIndex.forTesting(new CountingResolver(reach, 0));
|
||||||
|
NativeStructureVolumeIndex unreachable = NativeStructureVolumeIndex.forTesting(new CountingResolver(reach + 1, 0));
|
||||||
|
|
||||||
|
assertFalse(reachable.resolve(null, 0, 0, 15, 15).isEmpty());
|
||||||
|
assertTrue(unreachable.resolve(null, 0, 0, 15, 15).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void volumeResolutionNeverReadsChunkOrOwnershipState() throws Exception {
|
||||||
|
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructureVolumeIndexSource")));
|
||||||
|
|
||||||
|
assertFalse(source.contains("StructureManager"));
|
||||||
|
assertFalse(source.contains("getStartForStructure"));
|
||||||
|
assertFalse(source.contains("getAllStarts"));
|
||||||
|
assertFalse(source.contains("ChunkAccess"));
|
||||||
|
assertFalse(source.contains("NativeStructureOwnershipStore"));
|
||||||
|
assertFalse(source.contains("NativeStructureOwnershipRecovery"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void vanillaVolumesAreGatedOnPackPolicy() throws Exception {
|
||||||
|
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructureVolumeIndexSource")));
|
||||||
|
|
||||||
|
assertTrue(source.contains("NativeStructureGenerationPolicy.resolve("));
|
||||||
|
assertTrue(source.contains("if (!decision.generate())"));
|
||||||
|
assertTrue(source.contains("isStructureChunk("));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureStart swampHut(int chunkX, int chunkZ, int x, int z) {
|
||||||
|
Structure source = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty()));
|
||||||
|
SwampHutPiece piece = new SwampHutPiece(RandomSource.create(17L), x, z);
|
||||||
|
return new StructureStart(source, new ChunkPos(chunkX, chunkZ), 0, new PiecesContainer(List.of(piece)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class CountingResolver implements NativeStructureVolumeIndex.OriginResolver {
|
||||||
|
private final AtomicInteger resolutions = new AtomicInteger();
|
||||||
|
private final int originChunkX;
|
||||||
|
private final int originChunkZ;
|
||||||
|
|
||||||
|
private CountingResolver(int originChunkX, int originChunkZ) {
|
||||||
|
this.originChunkX = originChunkX;
|
||||||
|
this.originChunkZ = originChunkZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int resolutions() {
|
||||||
|
return resolutions.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KList<NativeStructureVolume> volumesAt(Engine engine, int chunkX, int chunkZ) {
|
||||||
|
resolutions.incrementAndGet();
|
||||||
|
if (chunkX != originChunkX || chunkZ != originChunkZ) {
|
||||||
|
return NativeStructureVolume.NONE;
|
||||||
|
}
|
||||||
|
return NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, swampHut(chunkX, chunkZ, 4, 6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
@@ -23,6 +23,7 @@ import art.arcane.iris.core.ServerConfigurator;
|
|||||||
import art.arcane.iris.core.datapack.DatapackIngestService;
|
import art.arcane.iris.core.datapack.DatapackIngestService;
|
||||||
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
|
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
|
||||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||||
|
import art.arcane.iris.core.nms.INMS;
|
||||||
import art.arcane.iris.core.project.IrisProject;
|
import art.arcane.iris.core.project.IrisProject;
|
||||||
import art.arcane.iris.core.project.IrisCodeWorkspace;
|
import art.arcane.iris.core.project.IrisCodeWorkspace;
|
||||||
import art.arcane.iris.core.service.IrisApiEventSVC;
|
import art.arcane.iris.core.service.IrisApiEventSVC;
|
||||||
@@ -31,15 +32,22 @@ import art.arcane.iris.core.tools.WorldMaintenance;
|
|||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.framework.EngineMode;
|
import art.arcane.iris.engine.framework.EngineMode;
|
||||||
import art.arcane.iris.engine.framework.EnginePlatformHooks;
|
import art.arcane.iris.engine.framework.EnginePlatformHooks;
|
||||||
|
import art.arcane.iris.engine.framework.NativeStructureVolume;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
|
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
|
||||||
import art.arcane.iris.engine.object.IrisWorld;
|
import art.arcane.iris.engine.object.IrisWorld;
|
||||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||||
import art.arcane.iris.spi.IrisPlatforms;
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
import art.arcane.iris.util.common.scheduling.J;
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
|
|
||||||
public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
|
public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
|
||||||
|
@Override
|
||||||
|
public KList<NativeStructureVolume> nativeStructureVolumes(Engine engine, int minX, int minZ, int maxX, int maxZ) {
|
||||||
|
return INMS.get().nativeStructureVolumes(engine, minX, minZ, maxX, maxZ);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void refreshWorkspace(Engine engine) {
|
public void refreshWorkspace(Engine engine) {
|
||||||
new IrisCodeWorkspace(new IrisProject(engine.getData().getDataFolder())).updateWorkspace();
|
new IrisCodeWorkspace(new IrisProject(engine.getData().getDataFolder())).updateWorkspace();
|
||||||
|
|||||||
+385
@@ -0,0 +1,385 @@
|
|||||||
|
package art.arcane.iris.nativegen;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||||
|
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
|
||||||
|
import art.arcane.iris.engine.framework.NativeStructurePlacementPlanner;
|
||||||
|
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
|
||||||
|
import art.arcane.iris.engine.framework.NativeStructureVolume;
|
||||||
|
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.Registry;
|
||||||
|
import net.minecraft.core.RegistryAccess;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
|
import net.minecraft.world.level.biome.BiomeSource;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.WeakHashMap;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.function.IntBinaryOperator;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the world-space piece bounds of every native structure that will generate around a query rect.
|
||||||
|
*
|
||||||
|
* <p>The answer is derived from the seed, the registries and this pack's structure policy only: candidate origins
|
||||||
|
* come from the placement grid and from vanilla structure placements, and each candidate is assembled through the
|
||||||
|
* same {@link NativeStructureFactory} path chunk generation uses. Nothing here reads chunk state, ownership records
|
||||||
|
* or generation progress, so the same query answers identically no matter which chunks already exist.
|
||||||
|
*/
|
||||||
|
public final class NativeStructureVolumeIndex {
|
||||||
|
private static final int ORIGIN_REACH_CHUNKS = NativeStructureOwnershipRecord.MAX_REFERENCE_DISTANCE_CHUNKS;
|
||||||
|
private static final int MAX_CACHED_ORIGIN_CHUNKS = 16_384;
|
||||||
|
private static final int MAX_CACHED_QUERY_CHUNKS = 4_096;
|
||||||
|
private static final Map<Engine, NativeStructureVolumeIndex> INDEXES =
|
||||||
|
Collections.synchronizedMap(new WeakHashMap<>());
|
||||||
|
private static final Set<String> WARNED_RESOLUTION_FAILURES = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final OriginResolver origins;
|
||||||
|
private final Map<Long, KList<NativeStructureVolume>> originCache = lru(MAX_CACHED_ORIGIN_CHUNKS);
|
||||||
|
private final Map<Long, KList<NativeStructureVolume>> queryCache = lru(MAX_CACHED_QUERY_CHUNKS);
|
||||||
|
private final ConcurrentHashMap<Long, CompletableFuture<KList<NativeStructureVolume>>> originBuilds =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
|
private final ConcurrentHashMap<Long, CompletableFuture<KList<NativeStructureVolume>>> queryBuilds =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private NativeStructureVolumeIndex(Context context) {
|
||||||
|
this.context = Objects.requireNonNull(context, "Native structure volume context must not be null");
|
||||||
|
this.origins = this::buildOriginVolumes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private NativeStructureVolumeIndex(OriginResolver origins) {
|
||||||
|
this.context = null;
|
||||||
|
this.origins = Objects.requireNonNull(origins, "Native structure origin resolver must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void install(Engine engine, Context context) {
|
||||||
|
Objects.requireNonNull(engine, "Native structure volume index requires an engine");
|
||||||
|
INDEXES.put(engine, new NativeStructureVolumeIndex(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void uninstall(Engine engine) {
|
||||||
|
if (engine != null) {
|
||||||
|
INDEXES.remove(engine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static KList<NativeStructureVolume> volumes(Engine engine, int minX, int minZ, int maxX, int maxZ) {
|
||||||
|
NativeStructureVolumeIndex index = engine == null ? null : INDEXES.get(engine);
|
||||||
|
return index == null ? NativeStructureVolume.NONE : index.resolve(engine, minX, minZ, maxX, maxZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
static NativeStructureVolumeIndex forTesting(OriginResolver origins) {
|
||||||
|
return new NativeStructureVolumeIndex(origins);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int originReachChunks() {
|
||||||
|
return ORIGIN_REACH_CHUNKS;
|
||||||
|
}
|
||||||
|
|
||||||
|
KList<NativeStructureVolume> resolve(Engine engine, int minX, int minZ, int maxX, int maxZ) {
|
||||||
|
int fromChunkX = Math.min(minX, maxX) >> 4;
|
||||||
|
int toChunkX = Math.max(minX, maxX) >> 4;
|
||||||
|
int fromChunkZ = Math.min(minZ, maxZ) >> 4;
|
||||||
|
int toChunkZ = Math.max(minZ, maxZ) >> 4;
|
||||||
|
KList<NativeStructureVolume> matches = null;
|
||||||
|
for (int chunkX = fromChunkX; chunkX <= toChunkX; chunkX++) {
|
||||||
|
for (int chunkZ = fromChunkZ; chunkZ <= toChunkZ; chunkZ++) {
|
||||||
|
for (NativeStructureVolume volume : chunkVolumes(engine, chunkX, chunkZ)) {
|
||||||
|
if (!volume.intersectsRect(minX, minZ, maxX, maxZ)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (matches == null) {
|
||||||
|
matches = new KList<>();
|
||||||
|
}
|
||||||
|
if (!matches.contains(volume)) {
|
||||||
|
matches.add(volume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matches == null ? NativeStructureVolume.NONE : matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
private KList<NativeStructureVolume> chunkVolumes(Engine engine, int chunkX, int chunkZ) {
|
||||||
|
return cached(queryCache, queryBuilds, chunkKey(chunkX, chunkZ),
|
||||||
|
() -> buildChunkVolumes(engine, chunkX, chunkZ));
|
||||||
|
}
|
||||||
|
|
||||||
|
private KList<NativeStructureVolume> buildChunkVolumes(Engine engine, int chunkX, int chunkZ) {
|
||||||
|
int minX = chunkX << 4;
|
||||||
|
int minZ = chunkZ << 4;
|
||||||
|
int maxX = minX + 15;
|
||||||
|
int maxZ = minZ + 15;
|
||||||
|
KList<NativeStructureVolume> volumes = null;
|
||||||
|
for (int originX = chunkX - ORIGIN_REACH_CHUNKS; originX <= chunkX + ORIGIN_REACH_CHUNKS; originX++) {
|
||||||
|
for (int originZ = chunkZ - ORIGIN_REACH_CHUNKS; originZ <= chunkZ + ORIGIN_REACH_CHUNKS; originZ++) {
|
||||||
|
for (NativeStructureVolume volume : originVolumes(engine, originX, originZ)) {
|
||||||
|
if (!volume.intersectsRect(minX, minZ, maxX, maxZ)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (volumes == null) {
|
||||||
|
volumes = new KList<>();
|
||||||
|
}
|
||||||
|
volumes.add(volume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return volumes == null ? NativeStructureVolume.NONE : volumes;
|
||||||
|
}
|
||||||
|
|
||||||
|
KList<NativeStructureVolume> originVolumes(Engine engine, int chunkX, int chunkZ) {
|
||||||
|
return cached(originCache, originBuilds, chunkKey(chunkX, chunkZ),
|
||||||
|
() -> origins.volumesAt(engine, chunkX, chunkZ));
|
||||||
|
}
|
||||||
|
|
||||||
|
private KList<NativeStructureVolume> buildOriginVolumes(Engine engine, int chunkX, int chunkZ) {
|
||||||
|
ChunkGeneratorStructureState state = context.structureState().get();
|
||||||
|
ChunkGenerator generator = context.generator().get();
|
||||||
|
BiomeSource biomeSource = context.biomeSource().get();
|
||||||
|
if (state == null || generator == null || biomeSource == null) {
|
||||||
|
return NativeStructureVolume.NONE;
|
||||||
|
}
|
||||||
|
Registry<Structure> registry = context.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||||
|
Bindings bindings = new Bindings(state, generator, biomeSource);
|
||||||
|
KList<NativeStructureVolume> volumes = appendPlannedVolumes(engine, bindings, registry, chunkX, chunkZ, null);
|
||||||
|
volumes = appendVanillaVolumes(engine, bindings, registry, chunkX, chunkZ, volumes);
|
||||||
|
return volumes == null ? NativeStructureVolume.NONE : volumes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private KList<NativeStructureVolume> appendPlannedVolumes(Engine engine, Bindings bindings,
|
||||||
|
Registry<Structure> registry, int chunkX, int chunkZ,
|
||||||
|
KList<NativeStructureVolume> volumes) {
|
||||||
|
KList<NativeStructureStartPlan> plans;
|
||||||
|
try {
|
||||||
|
plans = NativeStructurePlacementPlanner.plansAt(engine, chunkX, chunkZ);
|
||||||
|
} catch (RuntimeException | Error error) {
|
||||||
|
warnOnce("iris-placements", error);
|
||||||
|
return volumes;
|
||||||
|
}
|
||||||
|
|
||||||
|
KList<NativeStructureVolume> target = volumes;
|
||||||
|
for (NativeStructureStartPlan plan : plans) {
|
||||||
|
IrisNativeStructureDecision decision = NativeStructurePlacementPlanner.decisionFor(plan);
|
||||||
|
if (!decision.generate()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Identifier identifier = Identifier.tryParse(plan.source().getStructure());
|
||||||
|
if (identifier == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Structure structure = registry.getValue(identifier);
|
||||||
|
if (structure == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String structureId = identifier.toString();
|
||||||
|
try {
|
||||||
|
StructureStart generated = NativeStructureFactory.generate(
|
||||||
|
generationContext(engine, bindings), registry.wrapAsHolder(structure), plan, 0);
|
||||||
|
target = appendPieces(target, structureId, generated);
|
||||||
|
} catch (RuntimeException | Error error) {
|
||||||
|
warnOnce(structureId, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
private KList<NativeStructureVolume> appendVanillaVolumes(Engine engine, Bindings bindings,
|
||||||
|
Registry<Structure> registry, int chunkX, int chunkZ,
|
||||||
|
KList<NativeStructureVolume> volumes) {
|
||||||
|
ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
|
||||||
|
IntBinaryOperator surfaceHeight = surfaceHeight(engine);
|
||||||
|
int worldMinY = context.heightAccessor().getMinY();
|
||||||
|
int worldMaxYExclusive = worldMinY + context.heightAccessor().getHeight();
|
||||||
|
KList<NativeStructureVolume> target = volumes;
|
||||||
|
for (Holder<StructureSet> setHolder : bindings.state().possibleStructureSets()) {
|
||||||
|
StructureSet set = setHolder.value();
|
||||||
|
if (!set.placement().isStructureChunk(bindings.state(), chunkX, chunkZ)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (StructureSet.StructureSelectionEntry entry : set.structures()) {
|
||||||
|
Holder<Structure> holder = entry.structure();
|
||||||
|
Structure structure = holder.value();
|
||||||
|
Identifier identifier = registry.getKey(structure);
|
||||||
|
if (identifier == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String structureId = identifier.toString();
|
||||||
|
boolean undergroundStep = NativeStructureVegetationClearer.isUndergroundStep(structure.step());
|
||||||
|
try {
|
||||||
|
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(
|
||||||
|
engine, structureId, undergroundStep);
|
||||||
|
if (!decision.generate()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
StructureStart generated = structure.generate(
|
||||||
|
holder,
|
||||||
|
context.levelKey(),
|
||||||
|
context.registryAccess(),
|
||||||
|
bindings.generator(),
|
||||||
|
bindings.biomeSource(),
|
||||||
|
bindings.state().randomState(),
|
||||||
|
context.templateManager(),
|
||||||
|
bindings.state().getLevelSeed(),
|
||||||
|
chunkPos,
|
||||||
|
0,
|
||||||
|
context.heightAccessor(),
|
||||||
|
structure.biomes()::contains
|
||||||
|
);
|
||||||
|
if (generated == null || !generated.isValid()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||||
|
generated,
|
||||||
|
structureId,
|
||||||
|
decision.yShift(),
|
||||||
|
bindings.generator().getSeaLevel(),
|
||||||
|
worldMinY,
|
||||||
|
worldMaxYExclusive,
|
||||||
|
undergroundStep,
|
||||||
|
decision.preserveSourceY(),
|
||||||
|
decision.yBand(),
|
||||||
|
surfaceHeight);
|
||||||
|
target = appendPieces(target, structureId, generated);
|
||||||
|
} catch (RuntimeException | Error error) {
|
||||||
|
warnOnce(structureId, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
private NativeStructureFactory.GenerationContext generationContext(Engine engine, Bindings bindings) {
|
||||||
|
return new NativeStructureFactory.GenerationContext(
|
||||||
|
context.registryAccess(),
|
||||||
|
bindings.generator(),
|
||||||
|
bindings.biomeSource(),
|
||||||
|
bindings.state().randomState(),
|
||||||
|
context.templateManager(),
|
||||||
|
bindings.state().getLevelSeed(),
|
||||||
|
context.levelKey(),
|
||||||
|
context.heightAccessor(),
|
||||||
|
biome -> true,
|
||||||
|
bindings.generator().getSeaLevel(),
|
||||||
|
surfaceHeight(engine)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Bindings(ChunkGeneratorStructureState state, ChunkGenerator generator, BiomeSource biomeSource) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
interface OriginResolver {
|
||||||
|
KList<NativeStructureVolume> volumesAt(Engine engine, int chunkX, int chunkZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IntBinaryOperator surfaceHeight(Engine engine) {
|
||||||
|
return (x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
static KList<NativeStructureVolume> appendPieces(KList<NativeStructureVolume> volumes,
|
||||||
|
String structureId, StructureStart start) {
|
||||||
|
if (start == null || !start.isValid()) {
|
||||||
|
return volumes;
|
||||||
|
}
|
||||||
|
KList<NativeStructureVolume> target = volumes;
|
||||||
|
for (StructurePiece piece : start.getPieces()) {
|
||||||
|
BoundingBox bounds = piece.getBoundingBox();
|
||||||
|
if (target == null) {
|
||||||
|
target = new KList<>();
|
||||||
|
}
|
||||||
|
target.add(new NativeStructureVolume(structureId,
|
||||||
|
bounds.minX(), bounds.minY(), bounds.minZ(),
|
||||||
|
bounds.maxX(), bounds.maxY(), bounds.maxZ()));
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
private KList<NativeStructureVolume> cached(Map<Long, KList<NativeStructureVolume>> cache,
|
||||||
|
ConcurrentHashMap<Long, CompletableFuture<KList<NativeStructureVolume>>> builds,
|
||||||
|
long key, Supplier<KList<NativeStructureVolume>> loader) {
|
||||||
|
synchronized (cache) {
|
||||||
|
KList<NativeStructureVolume> hit = cache.get(key);
|
||||||
|
if (hit != null) {
|
||||||
|
return hit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompletableFuture<KList<NativeStructureVolume>> future = new CompletableFuture<>();
|
||||||
|
CompletableFuture<KList<NativeStructureVolume>> existing = builds.putIfAbsent(key, future);
|
||||||
|
if (existing != null) {
|
||||||
|
return existing.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
KList<NativeStructureVolume> built = loader.get();
|
||||||
|
synchronized (cache) {
|
||||||
|
cache.put(key, built);
|
||||||
|
}
|
||||||
|
future.complete(built);
|
||||||
|
return built;
|
||||||
|
} catch (RuntimeException | Error error) {
|
||||||
|
future.completeExceptionally(error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
builds.remove(key, future);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void warnOnce(String structureId, Throwable error) {
|
||||||
|
if (WARNED_RESOLUTION_FAILURES.add(structureId)) {
|
||||||
|
IrisLogging.warn("Native structure volume resolution failed for '" + structureId
|
||||||
|
+ "'; objects will not be vetoed against it: "
|
||||||
|
+ error.getClass().getSimpleName() + ":" + error.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long chunkKey(int chunkX, int chunkZ) {
|
||||||
|
return ((long) chunkX << 32) ^ (chunkZ & 0xffffffffL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<Long, KList<NativeStructureVolume>> lru(int capacity) {
|
||||||
|
return new LinkedHashMap<>(16, 0.75F, true) {
|
||||||
|
@Override
|
||||||
|
protected boolean removeEldestEntry(Map.Entry<Long, KList<NativeStructureVolume>> eldest) {
|
||||||
|
return size() > capacity;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The generator, biome source and structure state arrive as suppliers so an installed index never holds the
|
||||||
|
* level (and through it the engine) strongly. That keeps the weakly keyed registry collectable on world unload.
|
||||||
|
*/
|
||||||
|
public record Context(
|
||||||
|
RegistryAccess registryAccess,
|
||||||
|
StructureTemplateManager templateManager,
|
||||||
|
ResourceKey<Level> levelKey,
|
||||||
|
LevelHeightAccessor heightAccessor,
|
||||||
|
Supplier<ChunkGenerator> generator,
|
||||||
|
Supplier<BiomeSource> biomeSource,
|
||||||
|
Supplier<ChunkGeneratorStructureState> structureState
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -350,6 +350,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
requireCompletedShutdown(engine);
|
requireCompletedShutdown(engine);
|
||||||
unloading = false;
|
unloading = false;
|
||||||
Engine bound = bindEngine(level);
|
Engine bound = bindEngine(level);
|
||||||
|
nativeStructures.installVolumeIndex(level, bound);
|
||||||
// Bind time: a feature-order cycle is reported here, once, and degrades to features-off. Non-waiting for
|
// Bind time: a feature-order cycle is reported here, once, and degrades to features-off. Non-waiting for
|
||||||
// the same reason as repointAndBind: this method owns the generator monitor.
|
// the same reason as repointAndBind: this method owns the generator monitor.
|
||||||
importedFeatures.prepareWithoutWaiting(bound);
|
importedFeatures.prepareWithoutWaiting(bound);
|
||||||
|
|||||||
+22
@@ -36,6 +36,7 @@ import art.arcane.iris.nativegen.NativeStructureTerrainIntegrator;
|
|||||||
import art.arcane.iris.nativegen.NativeStructureVegetationClearer;
|
import art.arcane.iris.nativegen.NativeStructureVegetationClearer;
|
||||||
import art.arcane.iris.nativegen.NativeStructureVerticalPlacer;
|
import art.arcane.iris.nativegen.NativeStructureVerticalPlacer;
|
||||||
import art.arcane.iris.nativegen.NativeStructureVanillaLocator;
|
import art.arcane.iris.nativegen.NativeStructureVanillaLocator;
|
||||||
|
import art.arcane.iris.nativegen.NativeStructureVolumeIndex;
|
||||||
import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;
|
import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;
|
||||||
import art.arcane.iris.spi.PlatformBlockState;
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
import art.arcane.volmlib.util.math.RNG;
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
@@ -50,10 +51,13 @@ import net.minecraft.core.registries.Registries;
|
|||||||
import net.minecraft.resources.Identifier;
|
import net.minecraft.resources.Identifier;
|
||||||
import net.minecraft.server.level.ServerLevel;
|
import net.minecraft.server.level.ServerLevel;
|
||||||
import net.minecraft.world.level.ChunkPos;
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
import net.minecraft.world.level.StructureManager;
|
import net.minecraft.world.level.StructureManager;
|
||||||
import net.minecraft.world.level.WorldGenLevel;
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
import net.minecraft.world.level.block.state.BlockState;
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.biome.BiomeSource;
|
||||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
import net.minecraft.world.level.levelgen.GenerationStep;
|
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||||
import net.minecraft.world.level.levelgen.RandomSupport;
|
import net.minecraft.world.level.levelgen.RandomSupport;
|
||||||
import net.minecraft.world.level.levelgen.WorldgenRandom;
|
import net.minecraft.world.level.levelgen.WorldgenRandom;
|
||||||
@@ -63,6 +67,7 @@ import net.minecraft.world.level.levelgen.structure.Structure;
|
|||||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -87,6 +92,23 @@ final class ModdedNativeStructureStage {
|
|||||||
this.generator = generator;
|
this.generator = generator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void installVolumeIndex(ServerLevel level, Engine engine) {
|
||||||
|
WeakReference<ServerLevel> levelReference = new WeakReference<>(level);
|
||||||
|
WeakReference<ChunkGenerator> generatorReference = new WeakReference<>(generator);
|
||||||
|
WeakReference<BiomeSource> biomeSourceReference = new WeakReference<>(generator.structureBiomeSource);
|
||||||
|
NativeStructureVolumeIndex.install(engine, new NativeStructureVolumeIndex.Context(
|
||||||
|
level.registryAccess(),
|
||||||
|
level.getServer().getStructureManager(),
|
||||||
|
level.dimension(),
|
||||||
|
LevelHeightAccessor.create(level.getMinY(), level.getHeight()),
|
||||||
|
generatorReference::get,
|
||||||
|
biomeSourceReference::get,
|
||||||
|
() -> {
|
||||||
|
ServerLevel active = levelReference.get();
|
||||||
|
return active == null ? null : active.getChunkSource().getGeneratorState();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(ServerLevel level,
|
Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(ServerLevel level,
|
||||||
HolderSet<Structure> holders,
|
HolderSet<Structure> holders,
|
||||||
BlockPos pos, int radius, boolean findUnexplored,
|
BlockPos pos, int radius, boolean findUnexplored,
|
||||||
|
|||||||
+7
@@ -23,6 +23,7 @@ import art.arcane.iris.core.loader.IrisData;
|
|||||||
import art.arcane.iris.core.tools.WorldMaintenance;
|
import art.arcane.iris.core.tools.WorldMaintenance;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.framework.EnginePlatformHooks;
|
import art.arcane.iris.engine.framework.EnginePlatformHooks;
|
||||||
|
import art.arcane.iris.engine.framework.NativeStructureVolume;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
|
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
|
||||||
import art.arcane.iris.engine.object.IrisWorld;
|
import art.arcane.iris.engine.object.IrisWorld;
|
||||||
@@ -30,6 +31,7 @@ import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
|||||||
import art.arcane.iris.modded.ModdedDimensionManager;
|
import art.arcane.iris.modded.ModdedDimensionManager;
|
||||||
import art.arcane.iris.modded.ModdedForcedDatapack;
|
import art.arcane.iris.modded.ModdedForcedDatapack;
|
||||||
import art.arcane.iris.modded.ModdedWorkspaceGenerator;
|
import art.arcane.iris.modded.ModdedWorkspaceGenerator;
|
||||||
|
import art.arcane.iris.nativegen.NativeStructureVolumeIndex;
|
||||||
import art.arcane.iris.modded.command.ModdedPregenJob;
|
import art.arcane.iris.modded.command.ModdedPregenJob;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import art.arcane.volmlib.util.io.ReactiveFolder;
|
import art.arcane.volmlib.util.io.ReactiveFolder;
|
||||||
@@ -88,6 +90,11 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
|||||||
writeWorkspace(engine, "workspace refresh");
|
writeWorkspace(engine, "workspace refresh");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KList<NativeStructureVolume> nativeStructureVolumes(Engine engine, int minX, int minZ, int maxX, int maxZ) {
|
||||||
|
return NativeStructureVolumeIndex.volumes(engine, minX, minZ, maxX, maxZ);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void refreshDatapackWorkspace(Engine engine) {
|
public void refreshDatapackWorkspace(Engine engine) {
|
||||||
IrisData data = engine.getData();
|
IrisData data = engine.getData();
|
||||||
|
|||||||
@@ -127,6 +127,8 @@ nmsBindings.each { key, value ->
|
|||||||
rootProject.file('adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureStartInjector.java').absolutePath)
|
rootProject.file('adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureStartInjector.java').absolutePath)
|
||||||
systemProperty('iris.nativeStructureGenerationKeysSource',
|
systemProperty('iris.nativeStructureGenerationKeysSource',
|
||||||
rootProject.file('adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureGenerationKeys.java').absolutePath)
|
rootProject.file('adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureGenerationKeys.java').absolutePath)
|
||||||
|
systemProperty('iris.nativeStructureVolumeIndexSource',
|
||||||
|
rootProject.file('adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVolumeIndex.java').absolutePath)
|
||||||
systemProperty('iris.customBiomeSource',
|
systemProperty('iris.customBiomeSource',
|
||||||
rootProject.file("adapters/bukkit/nms/${key}/src/main/java/art/arcane/iris/core/nms/${key}/CustomBiomeSource.java").absolutePath)
|
rootProject.file("adapters/bukkit/nms/${key}/src/main/java/art/arcane/iris/core/nms/${key}/CustomBiomeSource.java").absolutePath)
|
||||||
systemProperty('iris.vanillaStructureBiomesSource',
|
systemProperty('iris.vanillaStructureBiomesSource',
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ import art.arcane.iris.core.localization.IrisLanguage;
|
|||||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||||
public class ServerConfigurator {
|
public class ServerConfigurator {
|
||||||
private static final Object DATAPACK_INSTALL_LOCK = new Object();
|
private static final Object DATAPACK_INSTALL_LOCK = new Object();
|
||||||
|
private static final String CODE_WORKSPACE_SUFFIX = ".code-workspace";
|
||||||
|
|
||||||
public static void configure() {
|
public static void configure() {
|
||||||
IrisSettings.IrisSettingsAutoconfiguration s = IrisSettings.get().getAutoConfiguration();
|
IrisSettings.IrisSettingsAutoconfiguration s = IrisSettings.get().getAutoConfiguration();
|
||||||
@@ -266,50 +267,71 @@ public class ServerConfigurator {
|
|||||||
public static DatapackInstallResult installDataPacksIfChanged(boolean fullInstall) {
|
public static DatapackInstallResult installDataPacksIfChanged(boolean fullInstall) {
|
||||||
synchronized (DATAPACK_INSTALL_LOCK) {
|
synchronized (DATAPACK_INSTALL_LOCK) {
|
||||||
File packsDir = IrisPlatforms.get().dataFolder("packs");
|
File packsDir = IrisPlatforms.get().dataFolder("packs");
|
||||||
String current;
|
File cacheFile = new File(IrisPlatforms.get().dataFolder("cache"), "datapack-fingerprint");
|
||||||
|
FingerprintCache cached = readFingerprintCache(cacheFile.toPath());
|
||||||
|
PackFingerprint fingerprint;
|
||||||
try {
|
try {
|
||||||
current = computePackFingerprint(packsDir);
|
fingerprint = resolvePackFingerprint(packsDir, cached.metadata(), cached.content());
|
||||||
} catch (RuntimeException exception) {
|
} catch (RuntimeException exception) {
|
||||||
IrisLogging.reportError("Unable to fingerprint Iris packs safely", exception);
|
IrisLogging.reportError("Unable to fingerprint Iris packs safely", exception);
|
||||||
return DatapackInstallResult.failedResult();
|
return DatapackInstallResult.failedResult();
|
||||||
}
|
}
|
||||||
File cacheFile = new File(IrisPlatforms.get().dataFolder("cache"), "datapack-fingerprint");
|
String current = fingerprint.content();
|
||||||
String cached = "";
|
if (!current.isEmpty() && current.equals(cached.content())) {
|
||||||
if (cacheFile.exists()) {
|
if (!fingerprint.metadata().equals(cached.metadata())) {
|
||||||
try {
|
writeFingerprintCache(cacheFile.toPath(), fingerprint);
|
||||||
cached = Files.readString(cacheFile.toPath(), StandardCharsets.UTF_8).trim();
|
|
||||||
} catch (IOException e) {
|
|
||||||
cached = "";
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (!current.isEmpty() && current.equals(cached)) {
|
|
||||||
IrisLogging.debug("Data packs unchanged, skipping install.");
|
IrisLogging.debug("Data packs unchanged, skipping install.");
|
||||||
return DatapackInstallResult.unchangedResult();
|
return DatapackInstallResult.unchangedResult();
|
||||||
}
|
}
|
||||||
DatapackInstallResult result = installDataPacksLocked(resolveDataFixer(), fullInstall);
|
DatapackInstallResult result = installDataPacksLocked(resolveDataFixer(), fullInstall);
|
||||||
if (result.succeeded()) {
|
if (result.succeeded()) {
|
||||||
try {
|
writeFingerprintCache(cacheFile.toPath(), fingerprint);
|
||||||
writeFingerprintAtomic(cacheFile.toPath(), current);
|
|
||||||
} catch (IOException e) {
|
|
||||||
IrisLogging.warn("Failed to write datapack fingerprint cache: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static PackFingerprint resolvePackFingerprint(File packsDir, String cachedMetadata, String cachedContent) {
|
||||||
|
String metadata = computePackMetadataDigest(packsDir);
|
||||||
|
if (!metadata.isEmpty()
|
||||||
|
&& metadata.equals(cachedMetadata)
|
||||||
|
&& cachedContent != null
|
||||||
|
&& !cachedContent.isEmpty()) {
|
||||||
|
return new PackFingerprint(metadata, cachedContent);
|
||||||
|
}
|
||||||
|
return new PackFingerprint(metadata, computePackFingerprint(packsDir));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String computePackMetadataDigest(File packsDir) {
|
||||||
|
Path root = resolveFingerprintRoot(packsDir);
|
||||||
|
if (root == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||||
|
List<FingerprintEntry> entries = collectFingerprintEntries(root.toRealPath());
|
||||||
|
entries.sort(Comparator.comparing(FingerprintEntry::relativePath));
|
||||||
|
for (FingerprintEntry entry : entries) {
|
||||||
|
BasicFileAttributes attributes = Files.readAttributes(
|
||||||
|
entry.source(), BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
|
||||||
|
byte[] relativePath = entry.relativePath().getBytes(StandardCharsets.UTF_8);
|
||||||
|
updateDigestInt(digest, relativePath.length);
|
||||||
|
digest.update(relativePath);
|
||||||
|
updateDigestLong(digest, attributes.size());
|
||||||
|
updateDigestLong(digest, attributes.lastModifiedTime().toMillis());
|
||||||
|
}
|
||||||
|
return HexFormat.of().formatHex(digest.digest());
|
||||||
|
} catch (IOException exception) {
|
||||||
|
throw new UncheckedIOException("Unable to fingerprint Iris packs at " + root, exception);
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new IllegalStateException("SHA-256 not available", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static String computePackFingerprint(File packsDir) {
|
public static String computePackFingerprint(File packsDir) {
|
||||||
if (packsDir == null) {
|
Path root = resolveFingerprintRoot(packsDir);
|
||||||
return "";
|
if (root == null) {
|
||||||
}
|
|
||||||
Path root = packsDir.toPath().toAbsolutePath().normalize();
|
|
||||||
if (!Files.exists(root, LinkOption.NOFOLLOW_LINKS)) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (!Files.isDirectory(root)) {
|
|
||||||
if (Files.isSymbolicLink(root)) {
|
|
||||||
throw new IllegalArgumentException("Iris packs root target is missing or unsafe: " + root);
|
|
||||||
}
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -340,6 +362,45 @@ public class ServerConfigurator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Path resolveFingerprintRoot(File packsDir) {
|
||||||
|
if (packsDir == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Path root = packsDir.toPath().toAbsolutePath().normalize();
|
||||||
|
if (!Files.exists(root, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!Files.isDirectory(root)) {
|
||||||
|
if (Files.isSymbolicLink(root)) {
|
||||||
|
throw new IllegalArgumentException("Iris packs root target is missing or unsafe: " + root);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FingerprintCache readFingerprintCache(Path cacheFile) {
|
||||||
|
if (!Files.isRegularFile(cacheFile)) {
|
||||||
|
return new FingerprintCache("", "");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
List<String> lines = Files.readAllLines(cacheFile, StandardCharsets.UTF_8);
|
||||||
|
String content = lines.isEmpty() ? "" : lines.getFirst().trim();
|
||||||
|
String metadata = lines.size() > 1 ? lines.get(1).trim() : "";
|
||||||
|
return new FingerprintCache(content, metadata);
|
||||||
|
} catch (IOException e) {
|
||||||
|
return new FingerprintCache("", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeFingerprintCache(Path cacheFile, PackFingerprint fingerprint) {
|
||||||
|
try {
|
||||||
|
writeFingerprintAtomic(cacheFile, fingerprint.content() + "\n" + fingerprint.metadata());
|
||||||
|
} catch (IOException e) {
|
||||||
|
IrisLogging.warn("Failed to write datapack fingerprint cache: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void writeFingerprintAtomic(Path target, String fingerprint) throws IOException {
|
private static void writeFingerprintAtomic(Path target, String fingerprint) throws IOException {
|
||||||
Path absoluteTarget = target.toAbsolutePath().normalize();
|
Path absoluteTarget = target.toAbsolutePath().normalize();
|
||||||
Path parent = absoluteTarget.getParent();
|
Path parent = absoluteTarget.getParent();
|
||||||
@@ -368,7 +429,7 @@ public class ServerConfigurator {
|
|||||||
try (Stream<Path> children = Files.list(root)) {
|
try (Stream<Path> children = Files.list(root)) {
|
||||||
for (Path child : children.toList()) {
|
for (Path child : children.toList()) {
|
||||||
String childName = child.getFileName().toString();
|
String childName = child.getFileName().toString();
|
||||||
if (PackDirectoryResolver.isHiddenName(childName)) {
|
if (PackDirectoryResolver.isHiddenName(childName) || isGeneratedPackFile(childName)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (Files.isSymbolicLink(child)) {
|
if (Files.isSymbolicLink(child)) {
|
||||||
@@ -406,7 +467,8 @@ public class ServerConfigurator {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException {
|
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException {
|
||||||
if (PackDirectoryResolver.isHiddenName(file.getFileName().toString())) {
|
String fileName = file.getFileName().toString();
|
||||||
|
if (PackDirectoryResolver.isHiddenName(fileName) || isGeneratedPackFile(fileName)) {
|
||||||
return FileVisitResult.CONTINUE;
|
return FileVisitResult.CONTINUE;
|
||||||
}
|
}
|
||||||
if (attributes.isSymbolicLink() || Files.isSymbolicLink(file)) {
|
if (attributes.isSymbolicLink() || Files.isSymbolicLink(file)) {
|
||||||
@@ -427,9 +489,19 @@ public class ServerConfigurator {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isGeneratedPackFile(String name) {
|
||||||
|
return name != null && name.endsWith(CODE_WORKSPACE_SUFFIX);
|
||||||
|
}
|
||||||
|
|
||||||
private record FingerprintEntry(Path source, String relativePath) {
|
private record FingerprintEntry(Path source, String relativePath) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
record PackFingerprint(String metadata, String content) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record FingerprintCache(String content, String metadata) {
|
||||||
|
}
|
||||||
|
|
||||||
private static void updateDigestInt(MessageDigest digest, int value) {
|
private static void updateDigestInt(MessageDigest digest, int value) {
|
||||||
for (int shift = Integer.SIZE - Byte.SIZE; shift >= 0; shift -= Byte.SIZE) {
|
for (int shift = Integer.SIZE - Byte.SIZE; shift >= 0; shift -= Byte.SIZE) {
|
||||||
digest.update((byte) (value >>> shift));
|
digest.update((byte) (value >>> shift));
|
||||||
|
|||||||
@@ -352,9 +352,13 @@ public final class DatapackIngestService {
|
|||||||
boolean successful = true;
|
boolean successful = true;
|
||||||
for (Entry entry : manifest.entries) {
|
for (Entry entry : manifest.entries) {
|
||||||
File stagedDir = new File(stagingDir, entry.id);
|
File stagedDir = new File(stagingDir, entry.id);
|
||||||
|
if (isRecordedUnchangedInstall(stagedDir, worldFolders, entry, stripOverrides)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!isUsableStaging(stagedDir, entry)) {
|
if (!isUsableStaging(stagedDir, entry)) {
|
||||||
IrisLogging.error("Managed datapack staging is unusable for '" + entry.id
|
IrisLogging.error("Managed datapack staging is unusable for '" + entry.id
|
||||||
+ "' at " + stagedDir.getPath());
|
+ "' at " + stagedDir.getPath());
|
||||||
|
forgetInstallMetadata(entry);
|
||||||
successful = false;
|
successful = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -364,8 +368,10 @@ public final class DatapackIngestService {
|
|||||||
IrisLogging.warn("Repaired installed datapack '" + entry.id
|
IrisLogging.warn("Repaired installed datapack '" + entry.id
|
||||||
+ "' from Iris staging before datapack compilation.");
|
+ "' from Iris staging before datapack compilation.");
|
||||||
}
|
}
|
||||||
|
recordInstallMetadata(stagedDir, worldFolders, entry);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
IrisLogging.reportError(e);
|
IrisLogging.reportError(e);
|
||||||
|
forgetInstallMetadata(entry);
|
||||||
successful = false;
|
successful = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -373,6 +379,127 @@ public final class DatapackIngestService {
|
|||||||
return successful;
|
return successful;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isRecordedUnchangedInstall(
|
||||||
|
File stagedDir,
|
||||||
|
KList<File> worldFolders,
|
||||||
|
Entry entry,
|
||||||
|
boolean stripOverrides
|
||||||
|
) {
|
||||||
|
if (entry.stagingMetadata == null || entry.stagingMetadata.isBlank()
|
||||||
|
|| entry.installMetadata == null || entry.installMetadata.size() != worldFolders.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (!isRecordedManagedDirectory(stagedDir, entry)
|
||||||
|
|| !entry.stagingMetadata.equals(metadataDigest(stagedDir))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (File worldFolder : worldFolders) {
|
||||||
|
File target = new File(worldFolder, entry.id);
|
||||||
|
if (!isRecordedManagedDirectory(target, entry)
|
||||||
|
|| new File(target, OVERRIDES_STRIPPED_MARKER).isFile() != stripOverrides) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String recorded = entry.installMetadata.get(installMetadataKey(target));
|
||||||
|
if (recorded == null || !recorded.equals(metadataDigest(target))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (IOException e) {
|
||||||
|
IrisLogging.debug("Managed datapack '" + entry.id
|
||||||
|
+ "' requires full verification: " + e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isRecordedManagedDirectory(File directory, Entry entry) throws IOException {
|
||||||
|
Path path = directory.toPath();
|
||||||
|
if (!Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(path)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!new File(directory, "pack.mcmeta").isFile()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Ownership ownership = readOwnershipOrNull(directory);
|
||||||
|
return ownership != null
|
||||||
|
&& ownershipSourceMatches(ownership, entry)
|
||||||
|
&& Objects.equals(ownership.versionId, entry.versionId)
|
||||||
|
&& Objects.equals(ownership.versionNumber, entry.versionNumber)
|
||||||
|
&& Objects.equals(ownership.sha1, entry.sha1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void recordInstallMetadata(File stagedDir, KList<File> worldFolders, Entry entry) {
|
||||||
|
try {
|
||||||
|
Map<String, String> recorded = new HashMap<>();
|
||||||
|
for (File worldFolder : worldFolders) {
|
||||||
|
File target = new File(worldFolder, entry.id);
|
||||||
|
recorded.put(installMetadataKey(target), metadataDigest(target));
|
||||||
|
}
|
||||||
|
entry.stagingMetadata = metadataDigest(stagedDir);
|
||||||
|
entry.installMetadata = recorded;
|
||||||
|
} catch (IOException e) {
|
||||||
|
IrisLogging.debug("Unable to record managed datapack metadata for '" + entry.id
|
||||||
|
+ "': " + e.getMessage());
|
||||||
|
forgetInstallMetadata(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void forgetInstallMetadata(Entry entry) {
|
||||||
|
entry.stagingMetadata = "";
|
||||||
|
entry.installMetadata = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String installMetadataKey(File target) {
|
||||||
|
return target.toPath().toAbsolutePath().normalize().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String metadataDigest(File root) throws IOException {
|
||||||
|
try {
|
||||||
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||||
|
Path rootPath = root.toPath().toAbsolutePath().normalize();
|
||||||
|
List<Path> entries = new ArrayList<>();
|
||||||
|
try (Stream<Path> paths = Files.walk(rootPath)) {
|
||||||
|
Iterator<Path> iterator = paths.iterator();
|
||||||
|
int pathCount = 0;
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
Path path = iterator.next();
|
||||||
|
if (path.equals(rootPath) || isFinderMetadata(path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pathCount++;
|
||||||
|
if (pathCount > MAX_MANAGED_PATHS) {
|
||||||
|
throw new IOException("Datapack contains more than " + MAX_MANAGED_PATHS + " paths");
|
||||||
|
}
|
||||||
|
if (Files.isSymbolicLink(path)) {
|
||||||
|
throw new IOException("Datapack contains a symbolic link: " + path);
|
||||||
|
}
|
||||||
|
entries.add(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entries.sort(Comparator.comparing(path -> rootPath.relativize(path).toString()));
|
||||||
|
for (Path entry : entries) {
|
||||||
|
String relative = rootPath.relativize(entry).toString().replace(File.separatorChar, '/');
|
||||||
|
byte[] relativeBytes = relative.getBytes(StandardCharsets.UTF_8);
|
||||||
|
BasicFileAttributes attributes = Files.readAttributes(
|
||||||
|
entry, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
|
||||||
|
if (!attributes.isDirectory() && !attributes.isRegularFile()) {
|
||||||
|
throw new IOException("Datapack contains an unsupported filesystem entry: " + entry);
|
||||||
|
}
|
||||||
|
digest.update((byte) (attributes.isDirectory() ? 1 : 2));
|
||||||
|
updateDigestInt(digest, relativeBytes.length);
|
||||||
|
digest.update(relativeBytes);
|
||||||
|
if (!attributes.isDirectory()) {
|
||||||
|
updateDigestLong(digest, attributes.size());
|
||||||
|
updateDigestLong(digest, attributes.lastModifiedTime().toMillis());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hex(digest.digest());
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new IOException("SHA-256 algorithm unavailable", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static boolean recoverBeforeReapply(File root, List<File> worldFolders) {
|
static boolean recoverBeforeReapply(File root, List<File> worldFolders) {
|
||||||
try {
|
try {
|
||||||
recoverTransactions(root, worldFolders);
|
recoverTransactions(root, worldFolders);
|
||||||
@@ -1166,6 +1293,7 @@ public final class DatapackIngestService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void recordInstallResult(VolmitSender sender, Report report, Entry entry, InstallResult result, String versionNumber) {
|
private static void recordInstallResult(VolmitSender sender, Report report, Entry entry, InstallResult result, String versionNumber) {
|
||||||
|
forgetInstallMetadata(entry);
|
||||||
if (result.changed()) {
|
if (result.changed()) {
|
||||||
report.updated.add(entry.id + " (" + safe(versionNumber) + ")");
|
report.updated.add(entry.id + " (" + safe(versionNumber) + ")");
|
||||||
report.requiresRestart = true;
|
report.requiresRestart = true;
|
||||||
@@ -2805,8 +2933,11 @@ public final class DatapackIngestService {
|
|||||||
copy.lastModified = resolved.lastModified;
|
copy.lastModified = resolved.lastModified;
|
||||||
copy.installedEpoch = resolved.installedEpoch;
|
copy.installedEpoch = resolved.installedEpoch;
|
||||||
copy.structuresImported = resolved.structuresImported;
|
copy.structuresImported = resolved.structuresImported;
|
||||||
|
copy.stagingMetadata = resolved.stagingMetadata;
|
||||||
copy.structureKeys = new ArrayList<>(copyList(resolved.structureKeys));
|
copy.structureKeys = new ArrayList<>(copyList(resolved.structureKeys));
|
||||||
copy.templateKeys = new ArrayList<>(copyList(resolved.templateKeys));
|
copy.templateKeys = new ArrayList<>(copyList(resolved.templateKeys));
|
||||||
|
copy.installMetadata = new HashMap<>(Objects.requireNonNullElseGet(
|
||||||
|
resolved.installMetadata, Map::of));
|
||||||
copy.importedTargets = new HashMap<>(Objects.requireNonNullElseGet(
|
copy.importedTargets = new HashMap<>(Objects.requireNonNullElseGet(
|
||||||
resolved.importedTargets, Map::of));
|
resolved.importedTargets, Map::of));
|
||||||
copy.importedBundles = new HashMap<>();
|
copy.importedBundles = new HashMap<>();
|
||||||
@@ -2915,6 +3046,8 @@ public final class DatapackIngestService {
|
|||||||
}
|
}
|
||||||
entry.structureKeys = normalizeKeys(entry.structureKeys);
|
entry.structureKeys = normalizeKeys(entry.structureKeys);
|
||||||
entry.templateKeys = normalizeKeys(entry.templateKeys);
|
entry.templateKeys = normalizeKeys(entry.templateKeys);
|
||||||
|
entry.stagingMetadata = entry.stagingMetadata == null ? "" : entry.stagingMetadata.trim();
|
||||||
|
entry.installMetadata = normalizeImportedTargets(entry.installMetadata);
|
||||||
entry.importedTargets = normalizeImportedTargets(entry.importedTargets);
|
entry.importedTargets = normalizeImportedTargets(entry.importedTargets);
|
||||||
entry.importedBundles = normalizeImportedBundles(entry.importedBundles);
|
entry.importedBundles = normalizeImportedBundles(entry.importedBundles);
|
||||||
if (!urls.add(entry.url) || !ids.add(entry.id)) {
|
if (!urls.add(entry.url) || !ids.add(entry.id)) {
|
||||||
@@ -4472,8 +4605,10 @@ public final class DatapackIngestService {
|
|||||||
public String lastModified;
|
public String lastModified;
|
||||||
public long installedEpoch;
|
public long installedEpoch;
|
||||||
public boolean structuresImported;
|
public boolean structuresImported;
|
||||||
|
public String stagingMetadata = "";
|
||||||
public List<String> structureKeys = new ArrayList<>();
|
public List<String> structureKeys = new ArrayList<>();
|
||||||
public List<String> templateKeys = new ArrayList<>();
|
public List<String> templateKeys = new ArrayList<>();
|
||||||
|
public Map<String, String> installMetadata = new HashMap<>();
|
||||||
public Map<String, String> importedTargets = new HashMap<>();
|
public Map<String, String> importedTargets = new HashMap<>();
|
||||||
public Map<String, Map<String, String>> importedBundles = new HashMap<>();
|
public Map<String, Map<String, String>> importedBundles = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import art.arcane.iris.core.nms.container.BlockProperty;
|
|||||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||||
import art.arcane.iris.engine.data.chunk.TerrainChunk;
|
import art.arcane.iris.engine.data.chunk.TerrainChunk;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.framework.NativeStructureVolume;
|
||||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||||
import art.arcane.iris.spi.PlatformBlockState;
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
import art.arcane.iris.spi.PlatformStructureHooks.JigsawSourceMetadata;
|
import art.arcane.iris.spi.PlatformStructureHooks.JigsawSourceMetadata;
|
||||||
@@ -143,6 +144,14 @@ public interface INMSBinding {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* World-space piece bounds of every native structure that will generate inside the given XZ rect. Bindings
|
||||||
|
* without native structure support answer with no volumes, which leaves the object veto inert.
|
||||||
|
*/
|
||||||
|
default KList<NativeStructureVolume> nativeStructureVolumes(Engine engine, int minX, int minZ, int maxX, int maxZ) {
|
||||||
|
return NativeStructureVolume.NONE;
|
||||||
|
}
|
||||||
|
|
||||||
int getBiomeId(Biome biome);
|
int getBiomeId(Biome biome);
|
||||||
|
|
||||||
MCABiomeContainer newBiomeContainer(int min, int max, int[] data);
|
MCABiomeContainer newBiomeContainer(int min, int max, int[] data);
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import art.arcane.volmlib.util.io.IO;
|
|||||||
import art.arcane.volmlib.util.json.JSONArray;
|
import art.arcane.volmlib.util.json.JSONArray;
|
||||||
import art.arcane.volmlib.util.json.JSONObject;
|
import art.arcane.volmlib.util.json.JSONObject;
|
||||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||||
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
|
||||||
import org.dom4j.Document;
|
import org.dom4j.Document;
|
||||||
import org.dom4j.Element;
|
import org.dom4j.Element;
|
||||||
|
|
||||||
@@ -43,7 +42,11 @@ import java.awt.Desktop;
|
|||||||
import java.awt.GraphicsEnvironment;
|
import java.awt.GraphicsEnvironment;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@SuppressWarnings("ALL")
|
@SuppressWarnings("ALL")
|
||||||
@@ -129,10 +132,7 @@ public class IrisCodeWorkspace {
|
|||||||
File ws = getCodeWorkspaceFile();
|
File ws = getCodeWorkspaceFile();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
PrecisionStopwatch p = PrecisionStopwatch.start();
|
writeIfChanged(ws, createCodeWorkspaceConfig().toString(4));
|
||||||
JSONObject j = createCodeWorkspaceConfig();
|
|
||||||
IO.writeAll(ws, j.toString(4));
|
|
||||||
p.end();
|
|
||||||
return true;
|
return true;
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
IrisLogging.reportError(e);
|
IrisLogging.reportError(e);
|
||||||
@@ -153,6 +153,13 @@ public class IrisCodeWorkspace {
|
|||||||
return createCodeWorkspaceConfig(true);
|
return createCodeWorkspaceConfig(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void writeIfChanged(File target, String rendered) throws IOException {
|
||||||
|
if (target.isFile() && (rendered + "\n").equals(IO.readAll(target))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IO.writeAll(target, rendered);
|
||||||
|
}
|
||||||
|
|
||||||
private JSONObject createCodeWorkspaceConfig(boolean includeSchemas) {
|
private JSONObject createCodeWorkspaceConfig(boolean includeSchemas) {
|
||||||
JSONObject ws = new JSONObject();
|
JSONObject ws = new JSONObject();
|
||||||
JSONArray folders = new JSONArray();
|
JSONArray folders = new JSONArray();
|
||||||
@@ -184,16 +191,17 @@ public class IrisCodeWorkspace {
|
|||||||
settings.put("[json]", jc);
|
settings.put("[json]", jc);
|
||||||
settings.put("json.maxItemsComputed", 30000);
|
settings.put("json.maxItemsComputed", 30000);
|
||||||
JSONArray schemas = new JSONArray();
|
JSONArray schemas = new JSONArray();
|
||||||
|
List<JSONObject> schemaEntries = new ArrayList<>();
|
||||||
IrisData dm = null;
|
IrisData dm = null;
|
||||||
if (includeSchemas) {
|
if (includeSchemas) {
|
||||||
dm = IrisData.get(project.getPath());
|
dm = IrisData.get(project.getPath());
|
||||||
for (ResourceLoader<?> r : dm.getLoaders().v()) {
|
for (ResourceLoader<?> r : dm.getLoaders().v()) {
|
||||||
if (r.supportsSchemas()) {
|
if (r.supportsSchemas()) {
|
||||||
schemas.put(r.buildSchema());
|
schemaEntries.add(r.buildSchema());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Class<?> i : dm.resolveSnippets()) {
|
for (Class<?> i : sortedSnippets(dm.resolveSnippets())) {
|
||||||
try {
|
try {
|
||||||
String snipType = i.getDeclaredAnnotation(Snippet.class).value();
|
String snipType = i.getDeclaredAnnotation(Snippet.class).value();
|
||||||
JSONObject o = new JSONObject();
|
JSONObject o = new JSONObject();
|
||||||
@@ -205,7 +213,7 @@ public class IrisCodeWorkspace {
|
|||||||
|
|
||||||
o.put("fileMatch", new JSONArray(fm.toArray()));
|
o.put("fileMatch", new JSONArray(fm.toArray()));
|
||||||
o.put("url", "./.iris/schema/snippet/" + snipType + "-schema.json");
|
o.put("url", "./.iris/schema/snippet/" + snipType + "-schema.json");
|
||||||
schemas.put(o);
|
schemaEntries.add(o);
|
||||||
IrisData snippetData = dm;
|
IrisData snippetData = dm;
|
||||||
File a = new File(snippetData.getDataFolder(), ".iris/schema/snippet/" + snipType + "-schema.json");
|
File a = new File(snippetData.getDataFolder(), ".iris/schema/snippet/" + snipType + "-schema.json");
|
||||||
J.attemptAsync(() -> {
|
J.attemptAsync(() -> {
|
||||||
@@ -219,6 +227,11 @@ public class IrisCodeWorkspace {
|
|||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
schemaEntries.sort(Comparator.comparing(entry -> entry.getString("url")));
|
||||||
|
for (JSONObject entry : schemaEntries) {
|
||||||
|
schemas.put(entry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.put("json.schemas", schemas);
|
settings.put("json.schemas", schemas);
|
||||||
@@ -295,4 +308,10 @@ public class IrisCodeWorkspace {
|
|||||||
}
|
}
|
||||||
return ws;
|
return ws;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<Class<?>> sortedSnippets(Set<Class<?>> snippets) {
|
||||||
|
List<Class<?>> sorted = new ArrayList<>(snippets);
|
||||||
|
sorted.sort(Comparator.comparing(Class::getName));
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ import art.arcane.iris.engine.framework.EngineEffects;
|
|||||||
import art.arcane.iris.engine.framework.EngineMetrics;
|
import art.arcane.iris.engine.framework.EngineMetrics;
|
||||||
import art.arcane.iris.engine.framework.EngineMode;
|
import art.arcane.iris.engine.framework.EngineMode;
|
||||||
import art.arcane.iris.engine.framework.EnginePlatformHooks;
|
import art.arcane.iris.engine.framework.EnginePlatformHooks;
|
||||||
|
import art.arcane.iris.engine.framework.NativeStructureVolume;
|
||||||
|
import art.arcane.iris.engine.framework.NativeStructureVolumeMemo;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import art.arcane.iris.engine.framework.EngineTarget;
|
import art.arcane.iris.engine.framework.EngineTarget;
|
||||||
import art.arcane.iris.engine.framework.EngineWorldManager;
|
import art.arcane.iris.engine.framework.EngineWorldManager;
|
||||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||||
@@ -116,6 +119,9 @@ public class IrisEngine implements Engine {
|
|||||||
private final SeedManager seedManager;
|
private final SeedManager seedManager;
|
||||||
private final GenerationSessionManager generationSessions;
|
private final GenerationSessionManager generationSessions;
|
||||||
private final EnginePlatformHooks platformHooks;
|
private final EnginePlatformHooks platformHooks;
|
||||||
|
@Getter(AccessLevel.NONE)
|
||||||
|
@Setter(AccessLevel.NONE)
|
||||||
|
private final NativeStructureVolumeMemo nativeStructureVolumeMemo = new NativeStructureVolumeMemo();
|
||||||
private final AtomicBoolean closing;
|
private final AtomicBoolean closing;
|
||||||
@Setter(AccessLevel.NONE)
|
@Setter(AccessLevel.NONE)
|
||||||
volatile IrisEngineData engineData;
|
volatile IrisEngineData engineData;
|
||||||
@@ -316,9 +322,15 @@ public class IrisEngine implements Engine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void hotloadSilently() {
|
public void hotloadSilently() {
|
||||||
|
nativeStructureVolumeMemo.clear();
|
||||||
hotloader.hotloadSilently();
|
hotloader.hotloadSilently();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KList<NativeStructureVolume> getNativeStructureVolumes(int minX, int minZ, int maxX, int maxZ) {
|
||||||
|
return nativeStructureVolumeMemo.volumes(this, platformHooks, minX, minZ, maxX, maxZ);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IrisEngineData getEngineData() {
|
public IrisEngineData getEngineData() {
|
||||||
return engineDataStore.getEngineData();
|
return engineDataStore.getEngineData();
|
||||||
|
|||||||
@@ -92,6 +92,15 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
|||||||
|
|
||||||
EnginePlatformHooks getPlatformHooks();
|
EnginePlatformHooks getPlatformHooks();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* World-space native structure piece bounds overlapping the given XZ rect. The answer is a pure function of the
|
||||||
|
* seed, the registries and this pack's structure policy, so it never depends on generation order.
|
||||||
|
*/
|
||||||
|
default KList<NativeStructureVolume> getNativeStructureVolumes(int minX, int minZ, int maxX, int maxZ) {
|
||||||
|
EnginePlatformHooks hooks = getPlatformHooks();
|
||||||
|
return hooks == null ? NativeStructureVolume.NONE : hooks.nativeStructureVolumes(this, minX, minZ, maxX, maxZ);
|
||||||
|
}
|
||||||
|
|
||||||
int getBlockUpdatesPerSecond();
|
int getBlockUpdatesPerSecond();
|
||||||
|
|
||||||
void printMetrics(VolmitSender sender);
|
void printMetrics(VolmitSender sender);
|
||||||
|
|||||||
@@ -19,8 +19,17 @@
|
|||||||
package art.arcane.iris.engine.framework;
|
package art.arcane.iris.engine.framework;
|
||||||
|
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
|
||||||
public interface EnginePlatformHooks {
|
public interface EnginePlatformHooks {
|
||||||
|
/**
|
||||||
|
* World-space piece bounds of every native structure that will generate inside the given XZ rect. Platforms
|
||||||
|
* without native structures return no volumes, which keeps the object veto free on those platforms.
|
||||||
|
*/
|
||||||
|
default KList<NativeStructureVolume> nativeStructureVolumes(Engine engine, int minX, int minZ, int maxX, int maxZ) {
|
||||||
|
return NativeStructureVolume.NONE;
|
||||||
|
}
|
||||||
|
|
||||||
default void refreshWorkspace(Engine engine) {
|
default void refreshWorkspace(Engine engine) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Servers
|
||||||
|
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package art.arcane.iris.engine.framework;
|
||||||
|
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* World-space axis-aligned bounds of one native structure piece. Volumes are resolved from seed, registry and
|
||||||
|
* pack policy alone so the same query answers identically regardless of which chunks exist.
|
||||||
|
*/
|
||||||
|
public record NativeStructureVolume(
|
||||||
|
String structure,
|
||||||
|
int minX,
|
||||||
|
int minY,
|
||||||
|
int minZ,
|
||||||
|
int maxX,
|
||||||
|
int maxY,
|
||||||
|
int maxZ
|
||||||
|
) {
|
||||||
|
public static final KList<NativeStructureVolume> NONE = new KList<>();
|
||||||
|
|
||||||
|
public static NativeStructureVolume of(String structure, int aX, int aY, int aZ, int bX, int bY, int bZ) {
|
||||||
|
return new NativeStructureVolume(
|
||||||
|
structure,
|
||||||
|
Math.min(aX, bX),
|
||||||
|
Math.min(aY, bY),
|
||||||
|
Math.min(aZ, bZ),
|
||||||
|
Math.max(aX, bX),
|
||||||
|
Math.max(aY, bY),
|
||||||
|
Math.max(aZ, bZ));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean intersectsRect(int rectMinX, int rectMinZ, int rectMaxX, int rectMaxZ) {
|
||||||
|
return maxX >= rectMinX && minX <= rectMaxX && maxZ >= rectMinZ && minZ <= rectMaxZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean intersects(int boxMinX, int boxMinY, int boxMinZ, int boxMaxX, int boxMaxY, int boxMaxZ) {
|
||||||
|
return maxX >= boxMinX && minX <= boxMaxX
|
||||||
|
&& maxY >= boxMinY && minY <= boxMaxY
|
||||||
|
&& maxZ >= boxMinZ && minZ <= boxMaxZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean contains(int x, int y, int z) {
|
||||||
|
return x >= minX && x <= maxX && y >= minY && y <= maxY && z >= minZ && z <= maxZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean containsWithin(int x, int y, int z, int margin) {
|
||||||
|
return x >= minX - margin && x <= maxX + margin
|
||||||
|
&& y >= minY - margin && y <= maxY + margin
|
||||||
|
&& z >= minZ - margin && z <= maxZ + margin;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Servers
|
||||||
|
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package art.arcane.iris.engine.framework;
|
||||||
|
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chunk keyed memo in front of the platform volume hook. One chunk's object pass fires the same rect query dozens of
|
||||||
|
* times, so the hook is asked once per chunk column and every later object reuses that answer. Long keyed so the
|
||||||
|
* overwhelmingly common "no native structure anywhere near" lookup allocates nothing at all.
|
||||||
|
*/
|
||||||
|
public final class NativeStructureVolumeMemo {
|
||||||
|
private static final int MAX_CACHED_CHUNKS = 2_048;
|
||||||
|
|
||||||
|
private final Long2ObjectLinkedOpenHashMap<KList<NativeStructureVolume>> chunks =
|
||||||
|
new Long2ObjectLinkedOpenHashMap<>();
|
||||||
|
|
||||||
|
public KList<NativeStructureVolume> volumes(Engine engine, EnginePlatformHooks hooks,
|
||||||
|
int minX, int minZ, int maxX, int maxZ) {
|
||||||
|
if (hooks == null) {
|
||||||
|
return NativeStructureVolume.NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fromChunkX = Math.min(minX, maxX) >> 4;
|
||||||
|
int toChunkX = Math.max(minX, maxX) >> 4;
|
||||||
|
int fromChunkZ = Math.min(minZ, maxZ) >> 4;
|
||||||
|
int toChunkZ = Math.max(minZ, maxZ) >> 4;
|
||||||
|
KList<NativeStructureVolume> matches = null;
|
||||||
|
for (int chunkX = fromChunkX; chunkX <= toChunkX; chunkX++) {
|
||||||
|
for (int chunkZ = fromChunkZ; chunkZ <= toChunkZ; chunkZ++) {
|
||||||
|
for (NativeStructureVolume volume : chunkVolumes(engine, hooks, chunkX, chunkZ)) {
|
||||||
|
if (!volume.intersectsRect(minX, minZ, maxX, maxZ)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (matches == null) {
|
||||||
|
matches = new KList<>();
|
||||||
|
}
|
||||||
|
if (!matches.contains(volume)) {
|
||||||
|
matches.add(volume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matches == null ? NativeStructureVolume.NONE : matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
synchronized (chunks) {
|
||||||
|
chunks.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private KList<NativeStructureVolume> chunkVolumes(Engine engine, EnginePlatformHooks hooks,
|
||||||
|
int chunkX, int chunkZ) {
|
||||||
|
long key = ((long) chunkX << 32) ^ (chunkZ & 0xffffffffL);
|
||||||
|
synchronized (chunks) {
|
||||||
|
KList<NativeStructureVolume> hit = chunks.getAndMoveToFirst(key);
|
||||||
|
if (hit != null) {
|
||||||
|
return hit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int minX = chunkX << 4;
|
||||||
|
int minZ = chunkZ << 4;
|
||||||
|
KList<NativeStructureVolume> resolved = hooks.nativeStructureVolumes(engine, minX, minZ, minX + 15, minZ + 15);
|
||||||
|
if (resolved == null) {
|
||||||
|
resolved = NativeStructureVolume.NONE;
|
||||||
|
}
|
||||||
|
synchronized (chunks) {
|
||||||
|
chunks.putAndMoveToFirst(key, resolved);
|
||||||
|
while (chunks.size() > MAX_CACHED_CHUNKS) {
|
||||||
|
chunks.removeLast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ package art.arcane.iris.engine.object;
|
|||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
import art.arcane.iris.engine.IrisComplex;
|
import art.arcane.iris.engine.IrisComplex;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.framework.NativeStructureVolume;
|
||||||
import art.arcane.iris.engine.framework.PlacedObject;
|
import art.arcane.iris.engine.framework.PlacedObject;
|
||||||
import art.arcane.iris.engine.framework.placer.HeightmapObjectPlacer;
|
import art.arcane.iris.engine.framework.placer.HeightmapObjectPlacer;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
@@ -343,6 +344,12 @@ final class IrisObjectPlacementRunner {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int warpMargin = warped ? (int) Math.ceil(Math.abs(config.getWarp().getMultiplier()) / 2D) : 0;
|
||||||
|
if (!rawStructurePiece && nativeStructureVetoes(placer, config, spin, translating, translateOffset, ceilingHang,
|
||||||
|
yv < 0 && config.getMode() == ObjectPlaceMode.PAINT, warpMargin, x, y + yrand, z)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
if (!config.isForcePlace() && !rawStructurePiece && (!config.getAllowedCollisions().isEmpty() || !config.getForbiddenCollisions().isEmpty())) {
|
if (!config.isForcePlace() && !rawStructurePiece && (!config.getAllowedCollisions().isEmpty() || !config.getForbiddenCollisions().isEmpty())) {
|
||||||
Engine engine = rdata.getEngine();
|
Engine engine = rdata.getEngine();
|
||||||
IrisBlockVector offset = new IrisBlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ());
|
IrisBlockVector offset = new IrisBlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ());
|
||||||
@@ -841,6 +848,87 @@ final class IrisObjectPlacementRunner {
|
|||||||
return !wouldReplace && (rawStructurePiece || !air);
|
return !wouldReplace && (rawStructurePiece || !air);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Objects may never intersect a native structure piece. The rect query is the fast path: no native structures
|
||||||
|
* near this placement means no per block work at all. Only when the placement envelope meets a piece does the
|
||||||
|
* precise pass run, and the first solid block inside a piece rejects the whole object before any write.
|
||||||
|
*/
|
||||||
|
private boolean nativeStructureVetoes(IObjectPlacer placer, IrisObjectPlacement config, SpinKernel spin,
|
||||||
|
boolean translating, IrisBlockVector translateOffset, boolean ceilingHang,
|
||||||
|
boolean paint, int warpMargin, int x, int y, int z) {
|
||||||
|
Engine engine = placer.getEngine();
|
||||||
|
if (engine == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int margin = (Math.max(self.getW(), Math.max(self.getH(), self.getD())) / 2) + 1 + warpMargin;
|
||||||
|
if (translating) {
|
||||||
|
margin += Math.max(Math.abs(translateOffset.getBlockX()),
|
||||||
|
Math.max(Math.abs(translateOffset.getBlockY()), Math.abs(translateOffset.getBlockZ())));
|
||||||
|
}
|
||||||
|
|
||||||
|
KList<NativeStructureVolume> volumes = engine.getNativeStructureVolumes(x - margin, z - margin, x + margin, z + margin);
|
||||||
|
if (volumes == null || volumes.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int envelopeMinY = paint ? Integer.MIN_VALUE : y - margin;
|
||||||
|
int envelopeMaxY = paint ? Integer.MAX_VALUE : y + margin;
|
||||||
|
boolean envelopeMeetsPiece = false;
|
||||||
|
for (NativeStructureVolume volume : volumes) {
|
||||||
|
if (volume.intersects(x - margin, envelopeMinY, z - margin, x + margin, envelopeMaxY, z + margin)) {
|
||||||
|
envelopeMeetsPiece = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!envelopeMeetsPiece) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.readLock.lock();
|
||||||
|
try {
|
||||||
|
VectorMap<PlatformBlockState>.Cursor cursor = self.blocks.cursor();
|
||||||
|
while (cursor.next()) {
|
||||||
|
PlatformBlockState state = cursor.value();
|
||||||
|
if (state == null || isAirBlock(state)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
IrisBlockVector i = cursor.key().clone();
|
||||||
|
spin.rotate(i);
|
||||||
|
if (ceilingHang) {
|
||||||
|
i.setY(-i.getBlockY());
|
||||||
|
}
|
||||||
|
if (translating) {
|
||||||
|
i.add(translateOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
int xx = x + (int) Math.round(i.getX());
|
||||||
|
int zz = z + (int) Math.round(i.getZ());
|
||||||
|
int yy = paint
|
||||||
|
? (int) Math.round(i.getY()) + Math.floorDiv(self.h, 2)
|
||||||
|
+ placer.getHighest(xx, zz, self.getLoader(), config.isUnderwater())
|
||||||
|
: y + (int) Math.round(i.getY());
|
||||||
|
|
||||||
|
for (NativeStructureVolume volume : volumes) {
|
||||||
|
if (volume.containsWithin(xx, yy, zz, warpMargin)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
self.readLock.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isAirBlock(PlatformBlockState state) {
|
||||||
|
String material = IrisObjectShaping.materialKey(state);
|
||||||
|
return material.equals("minecraft:air") || material.equals("minecraft:cave_air");
|
||||||
|
}
|
||||||
|
|
||||||
private void warnImplausibleBedrockPlacement(IObjectPlacer placer, IrisObjectPlacement config, int x, int y, int z) {
|
private void warnImplausibleBedrockPlacement(IObjectPlacer placer, IrisObjectPlacement config, int x, int y, int z) {
|
||||||
String key = self.getLoadKey();
|
String key = self.getLoadKey();
|
||||||
String fingerprint = (key == null ? "<null>" : key) + "|" + config.getMode();
|
String fingerprint = (key == null ? "<null>" : key) + "|" + config.getMode();
|
||||||
|
|||||||
+63
@@ -107,6 +107,69 @@ public class ServerConfiguratorDatapackFingerprintTest {
|
|||||||
assertEquals(before, ServerConfigurator.computePackFingerprint(packsDir));
|
assertEquals(before, ServerConfigurator.computePackFingerprint(packsDir));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void computePackFingerprintIgnoresGeneratedCodeWorkspaceFiles() throws Exception {
|
||||||
|
File packsDir = tmp.newFolder("workspace-packs");
|
||||||
|
Path dimension = packsDir.toPath().resolve("overworld/dimensions/overworld.json");
|
||||||
|
Files.createDirectories(dimension.getParent());
|
||||||
|
Files.writeString(dimension, "authored", StandardCharsets.UTF_8);
|
||||||
|
String before = ServerConfigurator.computePackFingerprint(packsDir);
|
||||||
|
|
||||||
|
Path workspace = packsDir.toPath().resolve("overworld/overworld.code-workspace");
|
||||||
|
Files.writeString(workspace, "{\"folders\":[]}", StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
assertEquals("Iris-generated workspace files must not alter the fingerprint",
|
||||||
|
before, ServerConfigurator.computePackFingerprint(packsDir));
|
||||||
|
|
||||||
|
Files.writeString(workspace, "{\"folders\":[{\"path\":\".\"}]}", StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
assertEquals("Reordered workspace bytes must not alter the fingerprint",
|
||||||
|
before, ServerConfigurator.computePackFingerprint(packsDir));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resolvePackFingerprintReusesCachedContentWhileMetadataIsUnchanged() throws Exception {
|
||||||
|
File packsDir = tmp.newFolder("two-tier-packs");
|
||||||
|
Path dimension = packsDir.toPath().resolve("testpack/dimensions/overworld.json");
|
||||||
|
Files.createDirectories(dimension.getParent());
|
||||||
|
Files.writeString(dimension, "aaaa", StandardCharsets.UTF_8);
|
||||||
|
ServerConfigurator.PackFingerprint first =
|
||||||
|
ServerConfigurator.resolvePackFingerprint(packsDir, "", "");
|
||||||
|
assertEquals(ServerConfigurator.computePackFingerprint(packsDir), first.content());
|
||||||
|
assertNotEquals("", first.metadata());
|
||||||
|
|
||||||
|
FileTime originalMtime = Files.getLastModifiedTime(dimension);
|
||||||
|
Files.writeString(dimension, "bbbb", StandardCharsets.UTF_8);
|
||||||
|
Files.setLastModifiedTime(dimension, originalMtime);
|
||||||
|
ServerConfigurator.PackFingerprint reused =
|
||||||
|
ServerConfigurator.resolvePackFingerprint(packsDir, first.metadata(), first.content());
|
||||||
|
|
||||||
|
assertEquals("Unchanged metadata must reuse the cached content fingerprint",
|
||||||
|
first.content(), reused.content());
|
||||||
|
|
||||||
|
Files.setLastModifiedTime(dimension, FileTime.fromMillis(originalMtime.toMillis() + 5000L));
|
||||||
|
ServerConfigurator.PackFingerprint rehashed =
|
||||||
|
ServerConfigurator.resolvePackFingerprint(packsDir, first.metadata(), first.content());
|
||||||
|
|
||||||
|
assertNotEquals("Changed metadata must re-hash pack contents",
|
||||||
|
first.content(), rehashed.content());
|
||||||
|
assertEquals(ServerConfigurator.computePackFingerprint(packsDir), rehashed.content());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void computePackMetadataDigestIgnoresGeneratedCodeWorkspaceFiles() throws Exception {
|
||||||
|
File packsDir = tmp.newFolder("metadata-workspace-packs");
|
||||||
|
Path dimension = packsDir.toPath().resolve("overworld/dimensions/overworld.json");
|
||||||
|
Files.createDirectories(dimension.getParent());
|
||||||
|
Files.writeString(dimension, "authored", StandardCharsets.UTF_8);
|
||||||
|
String before = ServerConfigurator.computePackMetadataDigest(packsDir);
|
||||||
|
|
||||||
|
Files.writeString(packsDir.toPath().resolve("overworld/overworld.code-workspace"),
|
||||||
|
"{\"folders\":[]}", StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
assertEquals(before, ServerConfigurator.computePackMetadataDigest(packsDir));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void computePackFingerprintRejectsSymbolicLinks() throws Exception {
|
public void computePackFingerprintRejectsSymbolicLinks() throws Exception {
|
||||||
File packsDir = tmp.newFolder("unsafe-packs");
|
File packsDir = tmp.newFolder("unsafe-packs");
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import java.nio.channels.ServerSocketChannel;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.attribute.FileTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
@@ -2557,6 +2558,111 @@ public class DatapackIngestServiceTest {
|
|||||||
assertFalse(new File(staging, entry.id).exists());
|
assertFalse(new File(staging, entry.id).exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void reapplyRecordsStagingAndInstallMetadataForTheNextPass() throws Exception {
|
||||||
|
ReapplyFixture fixture = reapplyFixture("reapply-record");
|
||||||
|
|
||||||
|
assertTrue(DatapackIngestService.reapplyStagedDirectories(
|
||||||
|
fixture.root(), fixture.stagingRoot(), fixture.worlds(), false));
|
||||||
|
|
||||||
|
assertTrue(new File(fixture.target(), ".iris-managed.json").isFile());
|
||||||
|
JsonObject recorded = manifestEntry(fixture.root());
|
||||||
|
assertFalse(recorded.get("stagingMetadata").getAsString().isBlank());
|
||||||
|
assertTrue(recorded.getAsJsonObject("installMetadata")
|
||||||
|
.has(fixture.target().toPath().toAbsolutePath().normalize().toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unchangedStagingAndTargetSkipContentHashingOnReapply() throws Exception {
|
||||||
|
ReapplyFixture fixture = reapplyFixture("reapply-shortcircuit");
|
||||||
|
assertTrue(DatapackIngestService.reapplyStagedDirectories(
|
||||||
|
fixture.root(), fixture.stagingRoot(), fixture.worlds(), false));
|
||||||
|
|
||||||
|
Path staged = fixture.staging().toPath().resolve("value.txt");
|
||||||
|
FileTime stamp = Files.getLastModifiedTime(staged);
|
||||||
|
Files.writeString(staged, "wxyz", StandardCharsets.UTF_8);
|
||||||
|
Files.setLastModifiedTime(staged, stamp);
|
||||||
|
|
||||||
|
assertTrue(DatapackIngestService.reapplyStagedDirectories(
|
||||||
|
fixture.root(), fixture.stagingRoot(), fixture.worlds(), false));
|
||||||
|
|
||||||
|
assertEquals("abcd", Files.readString(
|
||||||
|
new File(fixture.target(), "value.txt").toPath(), StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void changedStagingMetadataForcesFullReapplyVerification() throws Exception {
|
||||||
|
ReapplyFixture fixture = reapplyFixture("reapply-staging-change");
|
||||||
|
assertTrue(DatapackIngestService.reapplyStagedDirectories(
|
||||||
|
fixture.root(), fixture.stagingRoot(), fixture.worlds(), false));
|
||||||
|
|
||||||
|
Path staged = fixture.staging().toPath().resolve("value.txt");
|
||||||
|
FileTime stamp = Files.getLastModifiedTime(staged);
|
||||||
|
Files.writeString(staged, "wxyz", StandardCharsets.UTF_8);
|
||||||
|
Files.setLastModifiedTime(staged, FileTime.fromMillis(stamp.toMillis() + 5000L));
|
||||||
|
|
||||||
|
assertFalse(DatapackIngestService.reapplyStagedDirectories(
|
||||||
|
fixture.root(), fixture.stagingRoot(), fixture.worlds(), false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void changedInstallTargetIsRepairedDespiteRecordedMetadata() throws Exception {
|
||||||
|
ReapplyFixture fixture = reapplyFixture("reapply-target-change");
|
||||||
|
assertTrue(DatapackIngestService.reapplyStagedDirectories(
|
||||||
|
fixture.root(), fixture.stagingRoot(), fixture.worlds(), false));
|
||||||
|
|
||||||
|
Path installed = fixture.target().toPath().resolve("value.txt");
|
||||||
|
Files.writeString(installed, "zzzz", StandardCharsets.UTF_8);
|
||||||
|
Files.setLastModifiedTime(installed, FileTime.fromMillis(
|
||||||
|
Files.getLastModifiedTime(installed).toMillis() + 5000L));
|
||||||
|
|
||||||
|
assertTrue(DatapackIngestService.reapplyStagedDirectories(
|
||||||
|
fixture.root(), fixture.stagingRoot(), fixture.worlds(), false));
|
||||||
|
|
||||||
|
assertEquals("abcd", Files.readString(installed, StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void flippedOverrideStrippingForcesFullReapplyVerification() throws Exception {
|
||||||
|
ReapplyFixture fixture = reapplyFixture("reapply-strip-change");
|
||||||
|
assertTrue(DatapackIngestService.reapplyStagedDirectories(
|
||||||
|
fixture.root(), fixture.stagingRoot(), fixture.worlds(), false));
|
||||||
|
assertFalse(new File(fixture.target(), ".iris-overrides-stripped").exists());
|
||||||
|
|
||||||
|
assertTrue(DatapackIngestService.reapplyStagedDirectories(
|
||||||
|
fixture.root(), fixture.stagingRoot(), fixture.worlds(), true));
|
||||||
|
|
||||||
|
assertTrue(new File(fixture.target(), ".iris-overrides-stripped").isFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReapplyFixture reapplyFixture(String name) throws Exception {
|
||||||
|
File root = temporaryFolder.newFolder(name + "-root");
|
||||||
|
DatapackIngestService.Entry entry = entry("managed", "v1", "1", "sha");
|
||||||
|
File stagingRoot = new File(root, "staging");
|
||||||
|
File staging = new File(stagingRoot, entry.id);
|
||||||
|
writeManagedDatapack(staging, entry, "abcd");
|
||||||
|
writeManifest(root, entry);
|
||||||
|
File world = temporaryFolder.newFolder(name + "-world");
|
||||||
|
KList<File> worlds = new KList<>();
|
||||||
|
worlds.add(world);
|
||||||
|
return new ReapplyFixture(root, stagingRoot, staging, worlds, new File(world, entry.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonObject manifestEntry(File root) throws Exception {
|
||||||
|
JsonObject manifest = JsonParser.parseString(Files.readString(
|
||||||
|
new File(root, "manifest.json").toPath(), StandardCharsets.UTF_8)).getAsJsonObject();
|
||||||
|
return manifest.getAsJsonArray("entries").get(0).getAsJsonObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ReapplyFixture(
|
||||||
|
File root,
|
||||||
|
File stagingRoot,
|
||||||
|
File staging,
|
||||||
|
KList<File> worlds,
|
||||||
|
File target
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
private LegacyStagingFixture legacyStagingFixture(
|
private LegacyStagingFixture legacyStagingFixture(
|
||||||
String name,
|
String name,
|
||||||
boolean committed,
|
boolean committed,
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package art.arcane.iris.core.project;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.IrisSettings;
|
||||||
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.engine.framework.MeteredCache;
|
||||||
|
import art.arcane.iris.engine.framework.PreservationRegistry;
|
||||||
|
import art.arcane.iris.spi.IrisPlatform;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
|
import art.arcane.iris.spi.IrisServices;
|
||||||
|
import art.arcane.volmlib.util.json.JSONArray;
|
||||||
|
import art.arcane.volmlib.util.json.JSONObject;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
import org.mockito.Answers;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.attribute.FileTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class IrisCodeWorkspaceTest {
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
private IrisPlatform previousPlatform;
|
||||||
|
private IrisSettings previousSettings;
|
||||||
|
private IrisData data;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void bindPlatform() {
|
||||||
|
previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
|
||||||
|
previousSettings = IrisSettings.settings;
|
||||||
|
IrisPlatforms.unbind();
|
||||||
|
IrisPlatform platform = mock(IrisPlatform.class, Answers.CALLS_REAL_METHODS);
|
||||||
|
when(platform.dataFolder()).thenReturn(temporaryFolder.getRoot());
|
||||||
|
IrisPlatforms.bind(platform);
|
||||||
|
IrisSettings.settings = new IrisSettings();
|
||||||
|
IrisServices.register(PreservationRegistry.class, new NoOpPreservationRegistry());
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void restorePlatform() {
|
||||||
|
if (data != null) {
|
||||||
|
data.close();
|
||||||
|
data = null;
|
||||||
|
}
|
||||||
|
IrisServices.clear();
|
||||||
|
IrisPlatforms.unbind();
|
||||||
|
if (previousPlatform != null) {
|
||||||
|
IrisPlatforms.bind(previousPlatform);
|
||||||
|
}
|
||||||
|
IrisSettings.settings = previousSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void updateWorkspaceDoesNotRewriteAnUnchangedWorkspaceFile() throws Exception {
|
||||||
|
File pack = temporaryFolder.newFolder("overworld");
|
||||||
|
IrisCodeWorkspace workspace = new IrisCodeWorkspace(new IrisProject(pack));
|
||||||
|
|
||||||
|
assertTrue(workspace.updateWorkspace());
|
||||||
|
data = IrisData.get(pack);
|
||||||
|
File file = workspace.getCodeWorkspaceFile();
|
||||||
|
byte[] first = Files.readAllBytes(file.toPath());
|
||||||
|
FileTime stamp = FileTime.fromMillis(Files.getLastModifiedTime(file.toPath()).toMillis() - 60_000L);
|
||||||
|
Files.setLastModifiedTime(file.toPath(), stamp);
|
||||||
|
|
||||||
|
assertTrue(workspace.updateWorkspace());
|
||||||
|
|
||||||
|
assertArrayEquals("Unchanged workspace bytes must stay identical",
|
||||||
|
first, Files.readAllBytes(file.toPath()));
|
||||||
|
assertEquals("Unchanged workspace content must not be rewritten",
|
||||||
|
stamp.toMillis(), Files.getLastModifiedTime(file.toPath()).toMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void workspaceSchemaEntriesAreEmittedInStableSortedOrder() throws Exception {
|
||||||
|
File pack = temporaryFolder.newFolder("sorted");
|
||||||
|
JSONObject configuration = new IrisCodeWorkspace(new IrisProject(pack)).createCodeWorkspaceConfig();
|
||||||
|
data = IrisData.get(pack);
|
||||||
|
|
||||||
|
JSONArray schemas = configuration.getJSONObject("settings").getJSONArray("json.schemas");
|
||||||
|
List<String> urls = new ArrayList<>();
|
||||||
|
for (int i = 0; i < schemas.length(); i++) {
|
||||||
|
urls.add(schemas.getJSONObject(i).getString("url"));
|
||||||
|
}
|
||||||
|
List<String> sorted = new ArrayList<>(urls);
|
||||||
|
Collections.sort(sorted);
|
||||||
|
|
||||||
|
assertFalse("Expected the workspace to declare schemas", urls.isEmpty());
|
||||||
|
assertEquals("Schema entries must be emitted in a boot-stable order", sorted, urls);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class NoOpPreservationRegistry implements PreservationRegistry {
|
||||||
|
@Override
|
||||||
|
public void register(Thread thread) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void register(ExecutorService service) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerCache(MeteredCache cache) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dereference() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
@@ -32,7 +32,10 @@ public class WorldRuntimeControlServiceTimeLockTest {
|
|||||||
PluginManager pluginManager = mock(PluginManager.class);
|
PluginManager pluginManager = mock(PluginManager.class);
|
||||||
doReturn(pluginManager).when(server).getPluginManager();
|
doReturn(pluginManager).when(server).getPluginManager();
|
||||||
doReturn(Logger.getLogger("WorldRuntimeControlServiceTimeLockTest")).when(server).getLogger();
|
doReturn(Logger.getLogger("WorldRuntimeControlServiceTimeLockTest")).when(server).getLogger();
|
||||||
|
try {
|
||||||
Bukkit.setServer(server);
|
Bukkit.setServer(server);
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
package art.arcane.iris.engine.framework;
|
||||||
|
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class NativeStructureVolumeMemoTest {
|
||||||
|
@Test
|
||||||
|
public void repeatedQueriesInsideOneChunkAskThePlatformOnce() {
|
||||||
|
RecordingHooks hooks = new RecordingHooks(volume(0, 64, 0, 15, 78, 15));
|
||||||
|
NativeStructureVolumeMemo memo = new NativeStructureVolumeMemo();
|
||||||
|
|
||||||
|
KList<NativeStructureVolume> first = memo.volumes(null, hooks, 2, 2, 6, 6);
|
||||||
|
KList<NativeStructureVolume> second = memo.volumes(null, hooks, 8, 8, 12, 12);
|
||||||
|
|
||||||
|
assertEquals(1, hooks.queries().size());
|
||||||
|
assertEquals(first, second);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void chunkQueriesCoverTheWholeChunkColumn() {
|
||||||
|
RecordingHooks hooks = new RecordingHooks();
|
||||||
|
NativeStructureVolumeMemo memo = new NativeStructureVolumeMemo();
|
||||||
|
|
||||||
|
memo.volumes(null, hooks, 20, 36, 21, 37);
|
||||||
|
|
||||||
|
assertEquals(1, hooks.queries().size());
|
||||||
|
assertEquals(List.of(16, 32, 31, 47), hooks.queries().getFirst());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rectsSpanningChunksUnionWithoutDuplicates() {
|
||||||
|
NativeStructureVolume shared = volume(10, 64, 10, 40, 78, 40);
|
||||||
|
RecordingHooks hooks = new RecordingHooks(shared);
|
||||||
|
NativeStructureVolumeMemo memo = new NativeStructureVolumeMemo();
|
||||||
|
|
||||||
|
KList<NativeStructureVolume> volumes = memo.volumes(null, hooks, 12, 12, 20, 20);
|
||||||
|
|
||||||
|
assertEquals(4, hooks.queries().size());
|
||||||
|
assertEquals(1, volumes.size());
|
||||||
|
assertEquals(shared, volumes.getFirst());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void volumesOutsideTheRectAreFiltered() {
|
||||||
|
RecordingHooks hooks = new RecordingHooks(volume(0, 64, 0, 3, 78, 3));
|
||||||
|
NativeStructureVolumeMemo memo = new NativeStructureVolumeMemo();
|
||||||
|
|
||||||
|
assertTrue(memo.volumes(null, hooks, 5, 5, 9, 9).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void clearingTheMemoReQueriesThePlatform() {
|
||||||
|
RecordingHooks hooks = new RecordingHooks(volume(0, 64, 0, 15, 78, 15));
|
||||||
|
NativeStructureVolumeMemo memo = new NativeStructureVolumeMemo();
|
||||||
|
|
||||||
|
memo.volumes(null, hooks, 2, 2, 6, 6);
|
||||||
|
memo.clear();
|
||||||
|
memo.volumes(null, hooks, 2, 2, 6, 6);
|
||||||
|
|
||||||
|
assertEquals(2, hooks.queries().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void missingHooksResolveNoVolumes() {
|
||||||
|
NativeStructureVolumeMemo memo = new NativeStructureVolumeMemo();
|
||||||
|
|
||||||
|
assertTrue(memo.volumes(null, null, 0, 0, 15, 15).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NativeStructureVolume volume(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
||||||
|
return new NativeStructureVolume("minecraft:village_swamp", minX, minY, minZ, maxX, maxY, maxZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RecordingHooks implements EnginePlatformHooks {
|
||||||
|
private final KList<List<Integer>> queries = new KList<>();
|
||||||
|
private final List<NativeStructureVolume> answer = new ArrayList<>();
|
||||||
|
|
||||||
|
private RecordingHooks(NativeStructureVolume... volumes) {
|
||||||
|
for (NativeStructureVolume volume : volumes) {
|
||||||
|
answer.add(volume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private KList<List<Integer>> queries() {
|
||||||
|
return queries;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KList<NativeStructureVolume> nativeStructureVolumes(Engine engine, int minX, int minZ, int maxX, int maxZ) {
|
||||||
|
queries.add(List.of(minX, minZ, maxX, maxZ));
|
||||||
|
KList<NativeStructureVolume> volumes = new KList<>();
|
||||||
|
for (NativeStructureVolume volume : answer) {
|
||||||
|
if (volume.intersectsRect(minX, minZ, maxX, maxZ)) {
|
||||||
|
volumes.add(volume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return volumes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
package art.arcane.iris.engine.framework;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
|
import art.arcane.iris.engine.object.IrisImportedStructureControl;
|
||||||
|
import art.arcane.iris.engine.object.IrisNativeStructure;
|
||||||
|
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
||||||
|
import art.arcane.iris.engine.object.StructureDistribution;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The gate the native structure volume index applies before it assembles anything: a structure the pack suppresses
|
||||||
|
* contributes no piece volumes, so it can never veto an object placement.
|
||||||
|
*/
|
||||||
|
public class NativeStructureVolumeSuppressionTest {
|
||||||
|
@Test
|
||||||
|
public void packDisabledStructuresContributeNoVolumes() {
|
||||||
|
Engine engine = engine();
|
||||||
|
|
||||||
|
assertFalse(NativeStructureGenerationPolicy
|
||||||
|
.resolve(engine, "minecraft:village_swamp", false).generate());
|
||||||
|
assertFalse(NativeStructureGenerationPolicy
|
||||||
|
.resolve(engine, "minecraft:village_plains", false).generate());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void enabledStructuresContributeVolumes() {
|
||||||
|
Engine engine = engine();
|
||||||
|
|
||||||
|
assertTrue(NativeStructureGenerationPolicy
|
||||||
|
.resolve(engine, "towns_and_towers:village_swamp", false).generate());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void plannedIrisStartsContributeVolumesThroughTheirOwnDecision() {
|
||||||
|
IrisStructurePlacement placement = new IrisStructurePlacement()
|
||||||
|
.setDistribution(StructureDistribution.DENSITY)
|
||||||
|
.setDensity(1D);
|
||||||
|
placement.getNativeStructures().add(new IrisNativeStructure()
|
||||||
|
.setStructure("minecraft:ancient_city")
|
||||||
|
.setWeight(1));
|
||||||
|
NativeStructureStartPlan plan = new NativeStructureStartPlan(
|
||||||
|
placement, placement.getNativeStructures().getFirst(), 3, 5, -30);
|
||||||
|
|
||||||
|
assertTrue(NativeStructurePlacementPlanner.decisionFor(plan).generate());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Engine engine() {
|
||||||
|
IrisImportedStructureControl control = new IrisImportedStructureControl();
|
||||||
|
control.getDisabled().add("minecraft:village");
|
||||||
|
IrisDimension dimension = mock(IrisDimension.class);
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
when(engine.getData()).thenReturn(mock(IrisData.class));
|
||||||
|
when(engine.getDimension()).thenReturn(dimension);
|
||||||
|
when(dimension.getImportedStructures()).thenReturn(control);
|
||||||
|
when(dimension.getStructures()).thenReturn(new KList<>());
|
||||||
|
when(dimension.getAllRegions(engine)).thenReturn(new KList<>());
|
||||||
|
when(dimension.getReachableBiomes(engine)).thenReturn(new KList<>());
|
||||||
|
return engine;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package art.arcane.iris.engine.framework;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class NativeStructureVolumeTest {
|
||||||
|
private static final NativeStructureVolume PIECE =
|
||||||
|
new NativeStructureVolume("minecraft:village_plains", 10, 64, 20, 25, 78, 40);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rectQueriesIgnoreTheVerticalAxis() {
|
||||||
|
assertTrue(PIECE.intersectsRect(0, 0, 10, 20));
|
||||||
|
assertTrue(PIECE.intersectsRect(25, 40, 60, 60));
|
||||||
|
assertFalse(PIECE.intersectsRect(26, 20, 60, 40));
|
||||||
|
assertFalse(PIECE.intersectsRect(10, 41, 25, 60));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void boxQueriesSeparateStackedVolumes() {
|
||||||
|
assertTrue(PIECE.intersects(0, 60, 0, 40, 70, 50));
|
||||||
|
assertFalse(PIECE.intersects(0, 0, 0, 40, 63, 50));
|
||||||
|
assertFalse(PIECE.intersects(0, 79, 0, 40, 200, 50));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void containmentIsInclusiveOnEveryFace() {
|
||||||
|
assertTrue(PIECE.contains(10, 64, 20));
|
||||||
|
assertTrue(PIECE.contains(25, 78, 40));
|
||||||
|
assertFalse(PIECE.contains(9, 64, 20));
|
||||||
|
assertFalse(PIECE.contains(25, 79, 40));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void marginWidensContainmentSymmetrically() {
|
||||||
|
assertFalse(PIECE.containsWithin(8, 64, 20, 1));
|
||||||
|
assertTrue(PIECE.containsWithin(8, 64, 20, 2));
|
||||||
|
assertTrue(PIECE.containsWithin(25, 80, 42, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void factoryNormalizesCornerOrder() {
|
||||||
|
NativeStructureVolume normalized = NativeStructureVolume.of("test:piece", 25, 78, 40, 10, 64, 20);
|
||||||
|
|
||||||
|
assertEquals(PIECE.minX(), normalized.minX());
|
||||||
|
assertEquals(PIECE.minY(), normalized.minY());
|
||||||
|
assertEquals(PIECE.minZ(), normalized.minZ());
|
||||||
|
assertEquals(PIECE.maxX(), normalized.maxX());
|
||||||
|
assertEquals(PIECE.maxY(), normalized.maxY());
|
||||||
|
assertEquals(PIECE.maxZ(), normalized.maxZ());
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
@@ -46,7 +46,10 @@ public class IrisDimensionCarvingResolverParityTest {
|
|||||||
doReturn("1.0").when(server).getBukkitVersion();
|
doReturn("1.0").when(server).getBukkitVersion();
|
||||||
doAnswer((InvocationOnMock invocation) -> namedBlockData(invocation.getArgument(0, Material.class).name().toLowerCase(Locale.ROOT))).when(server).createBlockData(any(Material.class));
|
doAnswer((InvocationOnMock invocation) -> namedBlockData(invocation.getArgument(0, Material.class).name().toLowerCase(Locale.ROOT))).when(server).createBlockData(any(Material.class));
|
||||||
doAnswer((InvocationOnMock invocation) -> namedBlockData(invocation.getArgument(0, String.class))).when(server).createBlockData(anyString());
|
doAnswer((InvocationOnMock invocation) -> namedBlockData(invocation.getArgument(0, String.class))).when(server).createBlockData(anyString());
|
||||||
|
try {
|
||||||
Bukkit.setServer(server);
|
Bukkit.setServer(server);
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static BlockData namedBlockData(String key) {
|
private static BlockData namedBlockData(String key) {
|
||||||
|
|||||||
+3
@@ -38,7 +38,10 @@ public class IrisFloatingChildBiomesCarvingResolutionTest {
|
|||||||
doReturn("1.0").when(server).getBukkitVersion();
|
doReturn("1.0").when(server).getBukkitVersion();
|
||||||
doAnswer((InvocationOnMock invocation) -> namedBlockData(invocation.getArgument(0, Material.class).name().toLowerCase(Locale.ROOT))).when(server).createBlockData(any(Material.class));
|
doAnswer((InvocationOnMock invocation) -> namedBlockData(invocation.getArgument(0, Material.class).name().toLowerCase(Locale.ROOT))).when(server).createBlockData(any(Material.class));
|
||||||
doAnswer((InvocationOnMock invocation) -> namedBlockData(invocation.getArgument(0, String.class))).when(server).createBlockData(anyString());
|
doAnswer((InvocationOnMock invocation) -> namedBlockData(invocation.getArgument(0, String.class))).when(server).createBlockData(anyString());
|
||||||
|
try {
|
||||||
Bukkit.setServer(server);
|
Bukkit.setServer(server);
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static BlockData namedBlockData(String key) {
|
private static BlockData namedBlockData(String key) {
|
||||||
|
|||||||
@@ -0,0 +1,320 @@
|
|||||||
|
package art.arcane.iris.engine.object;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.engine.IrisComplex;
|
||||||
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.framework.NativeStructureVolume;
|
||||||
|
import art.arcane.iris.spi.IrisPlatform;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
|
import art.arcane.iris.spi.PlatformRegistries;
|
||||||
|
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class NativeStructureObjectVetoTest {
|
||||||
|
private static final int SURFACE_Y = 80;
|
||||||
|
|
||||||
|
private IrisData data;
|
||||||
|
private Engine engine;
|
||||||
|
private PlatformBlockState log;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void bindPlatform() {
|
||||||
|
IrisPlatforms.unbind();
|
||||||
|
PlatformBlockState block = mock(PlatformBlockState.class);
|
||||||
|
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||||
|
when(registries.block(anyString())).thenReturn(block);
|
||||||
|
IrisPlatform platform = mock(IrisPlatform.class);
|
||||||
|
when(platform.registries()).thenReturn(registries);
|
||||||
|
IrisPlatforms.bind(platform);
|
||||||
|
|
||||||
|
log = state("minecraft:oak_log", true);
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
ProceduralStream<Double> heightStream = mock(ProceduralStream.class);
|
||||||
|
IrisComplex complex = mock(IrisComplex.class);
|
||||||
|
when(complex.getHeightStream()).thenReturn(heightStream);
|
||||||
|
engine = mock(Engine.class);
|
||||||
|
when(engine.getHeight()).thenReturn(256);
|
||||||
|
when(engine.getComplex()).thenReturn(complex);
|
||||||
|
when(engine.getDimension()).thenReturn(mock(IrisDimension.class));
|
||||||
|
volumes();
|
||||||
|
data = mock(IrisData.class);
|
||||||
|
when(data.getEngine()).thenReturn(engine);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void unbindPlatform() {
|
||||||
|
IrisPlatforms.unbind();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void objectPlacesWhenNoNativeStructureIsNear() {
|
||||||
|
RecordingPlacer placer = new RecordingPlacer(engine);
|
||||||
|
|
||||||
|
assertTrue(place(placer) >= 0);
|
||||||
|
assertFalse(placer.written().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void canopyBlockInsideAPieceRejectsTheWholeObject() {
|
||||||
|
int canopyY = plantedTopY();
|
||||||
|
RecordingPlacer placer = new RecordingPlacer(engine);
|
||||||
|
volumes(volume(-1, canopyY, -1, 1, canopyY + 4, 1));
|
||||||
|
|
||||||
|
assertEquals(-1, place(placer));
|
||||||
|
assertTrue(placer.written().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void trunkBlockInsideAPieceRejectsTheWholeObject() {
|
||||||
|
int baseY = plantedBottomY();
|
||||||
|
RecordingPlacer placer = new RecordingPlacer(engine);
|
||||||
|
volumes(volume(0, baseY, 0, 0, baseY, 0));
|
||||||
|
|
||||||
|
assertEquals(-1, place(placer));
|
||||||
|
assertTrue(placer.written().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void pieceOverlappingOnlyTheEnvelopeStillPlaces() {
|
||||||
|
RecordingPlacer placer = new RecordingPlacer(engine);
|
||||||
|
volumes(volume(2, plantedBottomY(), 2, 4, plantedTopY(), 4));
|
||||||
|
|
||||||
|
assertTrue(place(placer) >= 0);
|
||||||
|
assertFalse(placer.written().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void pieceBelowTheObjectStillPlaces() {
|
||||||
|
RecordingPlacer placer = new RecordingPlacer(engine);
|
||||||
|
volumes(volume(-32, -60, -32, 32, plantedBottomY() - 1, 32));
|
||||||
|
|
||||||
|
assertTrue(place(placer) >= 0);
|
||||||
|
assertFalse(placer.written().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectionWritesNoBlocksTilesOrMarkers() {
|
||||||
|
RecordingPlacer placer = new RecordingPlacer(engine);
|
||||||
|
volumes(volume(-64, -64, -64, 64, 320, 64));
|
||||||
|
|
||||||
|
assertEquals(-1, place(placer));
|
||||||
|
assertTrue(placer.written().isEmpty());
|
||||||
|
assertEquals(0, placer.tiles());
|
||||||
|
assertEquals(0, placer.markers());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iris authored structure pieces are arbitrated by the jigsaw placement scope, not by this veto: rejecting them
|
||||||
|
* one piece at a time would publish a partial structure. The sibling placement pins the test to the exemption
|
||||||
|
* rather than to geometry that simply misses every volume.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void irisStructurePiecesBypassTheVetoUnlikeTheirSiblings() {
|
||||||
|
volumes(volume(-64, -64, -64, 64, 320, 64));
|
||||||
|
|
||||||
|
RecordingPlacer vetoed = new RecordingPlacer(engine);
|
||||||
|
assertEquals(-1, tree().place(0, SURFACE_Y, 0, vetoed, placement(), new RNG(1234L), data));
|
||||||
|
assertTrue(vetoed.written().isEmpty());
|
||||||
|
|
||||||
|
RecordingPlacer exempt = new RecordingPlacer(engine);
|
||||||
|
IrisObjectPlacement structurePiece = placement();
|
||||||
|
structurePiece.setMode(ObjectPlaceMode.STRUCTURE_PIECE);
|
||||||
|
|
||||||
|
assertTrue(tree().place(0, SURFACE_Y, 0, exempt, structurePiece, new RNG(1234L), data) >= 0);
|
||||||
|
assertFalse(exempt.written().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void forcePlaceStillObeysTheVeto() {
|
||||||
|
RecordingPlacer placer = new RecordingPlacer(engine);
|
||||||
|
volumes(volume(-64, -64, -64, 64, 320, 64));
|
||||||
|
IrisObjectPlacement placement = placement();
|
||||||
|
placement.setForcePlace(true);
|
||||||
|
|
||||||
|
assertEquals(-1, tree().place(0, -1, 0, placer, placement, new RNG(1234L), data));
|
||||||
|
assertTrue(placer.written().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void structurePiecePlacementsBypassTheVeto() {
|
||||||
|
RecordingPlacer placer = new RecordingPlacer(engine);
|
||||||
|
volumes(volume(-64, -64, -64, 64, 320, 64));
|
||||||
|
IrisObjectPlacement placement = placement();
|
||||||
|
placement.setMode(ObjectPlaceMode.STRUCTURE_PIECE);
|
||||||
|
|
||||||
|
assertTrue(tree().place(0, 100, 0, placer, placement, new RNG(1234L), data) >= 0);
|
||||||
|
assertFalse(placer.written().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private int plantedTopY() {
|
||||||
|
int top = Integer.MIN_VALUE;
|
||||||
|
for (int[] position : plantedPositions()) {
|
||||||
|
top = Math.max(top, position[1]);
|
||||||
|
}
|
||||||
|
return top;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int plantedBottomY() {
|
||||||
|
int bottom = Integer.MAX_VALUE;
|
||||||
|
for (int[] position : plantedPositions()) {
|
||||||
|
bottom = Math.min(bottom, position[1]);
|
||||||
|
}
|
||||||
|
return bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<int[]> plantedPositions() {
|
||||||
|
RecordingPlacer placer = new RecordingPlacer(engine);
|
||||||
|
volumes();
|
||||||
|
assertTrue(place(placer) >= 0);
|
||||||
|
assertFalse(placer.written().isEmpty());
|
||||||
|
return placer.written();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int place(RecordingPlacer placer) {
|
||||||
|
return tree().place(0, -1, 0, placer, placement(), new RNG(1234L), data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IrisObjectPlacement placement() {
|
||||||
|
IrisObjectPlacement placement = new IrisObjectPlacement();
|
||||||
|
placement.setMode(ObjectPlaceMode.CENTER_HEIGHT);
|
||||||
|
return placement;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IrisObject tree() {
|
||||||
|
IrisObject object = new IrisObject(3, 7, 3);
|
||||||
|
for (int y = 0; y < 7; y++) {
|
||||||
|
object.setUnsigned(1, y, 1, log);
|
||||||
|
}
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void volumes(NativeStructureVolume... volumes) {
|
||||||
|
KList<NativeStructureVolume> list = new KList<>();
|
||||||
|
for (NativeStructureVolume volume : volumes) {
|
||||||
|
list.add(volume);
|
||||||
|
}
|
||||||
|
when(engine.getNativeStructureVolumes(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
private NativeStructureVolume volume(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
||||||
|
return new NativeStructureVolume("minecraft:village_plains", minX, minY, minZ, maxX, maxY, maxZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlatformBlockState state(String key, boolean solid) {
|
||||||
|
PlatformBlockState state = mock(PlatformBlockState.class);
|
||||||
|
when(state.isSolid()).thenReturn(solid);
|
||||||
|
when(state.key()).thenReturn(key);
|
||||||
|
when(state.materialKey()).thenReturn(key);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RecordingPlacer implements IObjectPlacer {
|
||||||
|
private final List<int[]> written = new ArrayList<>();
|
||||||
|
private final PlatformBlockState air = state("minecraft:air", false);
|
||||||
|
private final Engine engine;
|
||||||
|
private int tiles;
|
||||||
|
private int markers;
|
||||||
|
|
||||||
|
private RecordingPlacer(Engine engine) {
|
||||||
|
this.engine = engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<int[]> written() {
|
||||||
|
return written;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int tiles() {
|
||||||
|
return tiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int markers() {
|
||||||
|
return markers;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getHighest(int x, int z, IrisData data) {
|
||||||
|
return SURFACE_Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
|
||||||
|
return SURFACE_Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void set(int x, int y, int z, PlatformBlockState state) {
|
||||||
|
written.add(new int[]{x, y, z});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PlatformBlockState get(int x, int y, int z) {
|
||||||
|
return air;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPreventingDecay() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCarved(int x, int y, int z) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSolid(int x, int y, int z) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isUnderwater(int x, int z) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getFluidHeight() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDebugSmartBore() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTile(int x, int y, int z, TileData tile) {
|
||||||
|
tiles++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> void setData(int x, int y, int z, T data) {
|
||||||
|
markers++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T getData(int x, int y, int z, Class<T> type) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Engine getEngine() {
|
||||||
|
return engine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,7 +62,10 @@ public class BukkitSpiConformanceTest {
|
|||||||
doReturn("1.0").when(server).getVersion();
|
doReturn("1.0").when(server).getVersion();
|
||||||
doReturn("1.0").when(server).getBukkitVersion();
|
doReturn("1.0").when(server).getBukkitVersion();
|
||||||
doAnswer((InvocationOnMock invocation) -> blockData("minecraft:" + invocation.getArgument(0, Material.class).name().toLowerCase(Locale.ROOT))).when(server).createBlockData(any(Material.class));
|
doAnswer((InvocationOnMock invocation) -> blockData("minecraft:" + invocation.getArgument(0, Material.class).name().toLowerCase(Locale.ROOT))).when(server).createBlockData(any(Material.class));
|
||||||
|
try {
|
||||||
Bukkit.setServer(server);
|
Bukkit.setServer(server);
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
doAnswer((InvocationOnMock invocation) -> blockData(invocation.getArgument(0))).when(server).createBlockData(anyString());
|
doAnswer((InvocationOnMock invocation) -> blockData(invocation.getArgument(0))).when(server).createBlockData(anyString());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,10 @@ public class CNGInjectorParityTest {
|
|||||||
doReturn("1.0").when(server).getBukkitVersion();
|
doReturn("1.0").when(server).getBukkitVersion();
|
||||||
doAnswer((InvocationOnMock invocation) -> namedBlockData(invocation.getArgument(0, Material.class).name().toLowerCase(Locale.ROOT))).when(server).createBlockData(any(Material.class));
|
doAnswer((InvocationOnMock invocation) -> namedBlockData(invocation.getArgument(0, Material.class).name().toLowerCase(Locale.ROOT))).when(server).createBlockData(any(Material.class));
|
||||||
doAnswer((InvocationOnMock invocation) -> namedBlockData(invocation.getArgument(0, String.class))).when(server).createBlockData(anyString());
|
doAnswer((InvocationOnMock invocation) -> namedBlockData(invocation.getArgument(0, String.class))).when(server).createBlockData(anyString());
|
||||||
|
try {
|
||||||
Bukkit.setServer(server);
|
Bukkit.setServer(server);
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static BlockData namedBlockData(String key) {
|
private static BlockData namedBlockData(String key) {
|
||||||
|
|||||||
Reference in New Issue
Block a user