mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 14:30:14 +00:00
Compare commits
20
Commits
No files matched your search
@@ -76,3 +76,8 @@ BUILDS/
|
|||||||
.compatwt/
|
.compatwt/
|
||||||
|
|
||||||
.worktrees/
|
.worktrees/
|
||||||
|
|
||||||
|
/VolmLib/
|
||||||
|
/docs/
|
||||||
|
/.perf/
|
||||||
|
/.codegraph/
|
||||||
+6
@@ -14,6 +14,7 @@ import net.minecraft.SharedConstants;
|
|||||||
import net.minecraft.server.Bootstrap;
|
import net.minecraft.server.Bootstrap;
|
||||||
import net.minecraft.world.level.LevelHeightAccessor;
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
import net.minecraft.world.level.levelgen.Heightmap;
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import org.junit.AfterClass;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -45,6 +46,11 @@ public final class IrisChunkGeneratorHeightCacheTest {
|
|||||||
NativeBlockTags.bindHeightmapFixtures();
|
NativeBlockTags.bindHeightmapFixtures();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void restoreBlockTags() {
|
||||||
|
NativeBlockTags.restoreTags();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void repeatedNativeHeightUsesLiveHeightQueriesAcrossRuntimeChanges() throws Exception {
|
public void repeatedNativeHeightUsesLiveHeightQueriesAcrossRuntimeChanges() throws Exception {
|
||||||
Fixture fixture = fixture();
|
Fixture fixture = fixture();
|
||||||
|
|||||||
+6
@@ -20,6 +20,7 @@ import net.minecraft.world.level.block.Blocks;
|
|||||||
import net.minecraft.world.level.block.RotatedPillarBlock;
|
import net.minecraft.world.level.block.RotatedPillarBlock;
|
||||||
import net.minecraft.world.level.block.LiquidBlock;
|
import net.minecraft.world.level.block.LiquidBlock;
|
||||||
import net.minecraft.world.level.levelgen.Heightmap;
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import org.junit.AfterClass;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -46,6 +47,11 @@ public final class NativeTransitionColumnTest {
|
|||||||
NativeBlockTags.bindHeightmapFixtures();
|
NativeBlockTags.bindHeightmapFixtures();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void restoreBlockTags() {
|
||||||
|
NativeBlockTags.restoreTags();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void customKeysUseCarrierForNativeColumnsAndHeightmaps() {
|
public void customKeysUseCarrierForNativeColumnsAndHeightmaps() {
|
||||||
String key = "itemsadder:rocks/ruby_ore";
|
String key = "itemsadder:rocks/ruby_ore";
|
||||||
|
|||||||
+17
@@ -1,21 +1,30 @@
|
|||||||
package art.arcane.iris.nativegen.v26_3_R1;
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
import net.minecraft.core.Holder;
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
import net.minecraft.core.registries.BuiltInRegistries;
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
import net.minecraft.core.registries.Registries;
|
import net.minecraft.core.registries.Registries;
|
||||||
import net.minecraft.tags.BlockTags;
|
import net.minecraft.tags.BlockTags;
|
||||||
import net.minecraft.tags.TagLoader;
|
import net.minecraft.tags.TagLoader;
|
||||||
|
import net.minecraft.tags.TagKey;
|
||||||
import net.minecraft.world.level.block.Block;
|
import net.minecraft.world.level.block.Block;
|
||||||
import net.minecraft.world.level.block.Blocks;
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public final class NativeBlockTags {
|
public final class NativeBlockTags {
|
||||||
|
private static Map<TagKey<Block>, List<Holder<Block>>> originalTags;
|
||||||
|
|
||||||
private NativeBlockTags() {
|
private NativeBlockTags() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void bindHeightmapFixtures() {
|
public static void bindHeightmapFixtures() {
|
||||||
|
originalTags = new HashMap<>();
|
||||||
|
for (HolderSet.Named<Block> tag : BuiltInRegistries.BLOCK.getTags().toList()) {
|
||||||
|
originalTags.put(tag.key(), tag.stream().toList());
|
||||||
|
}
|
||||||
List<Holder<Block>> solid = List.of(Blocks.STONE.builtInRegistryHolder(),
|
List<Holder<Block>> solid = List.of(Blocks.STONE.builtInRegistryHolder(),
|
||||||
Blocks.OAK_LOG.builtInRegistryHolder(), Blocks.SPRUCE_LOG.builtInRegistryHolder());
|
Blocks.OAK_LOG.builtInRegistryHolder(), Blocks.SPRUCE_LOG.builtInRegistryHolder());
|
||||||
List<Holder<Block>> canopy = List.of(Blocks.STONE.builtInRegistryHolder(),
|
List<Holder<Block>> canopy = List.of(Blocks.STONE.builtInRegistryHolder(),
|
||||||
@@ -25,4 +34,12 @@ public final class NativeBlockTags {
|
|||||||
BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP, canopy,
|
BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP, canopy,
|
||||||
BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP_NO_LEAVES, solid))).apply();
|
BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP_NO_LEAVES, solid))).apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void restoreTags() {
|
||||||
|
if (originalTags == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BuiltInRegistries.BLOCK.prepareTagReload(new TagLoader.LoadResult<>(Registries.BLOCK, originalTags)).apply();
|
||||||
|
originalTags = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+6
@@ -34,6 +34,7 @@ import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
|||||||
import net.minecraft.world.level.levelgen.structure.structures.IglooPieces;
|
import net.minecraft.world.level.levelgen.structure.structures.IglooPieces;
|
||||||
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
|
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
|
||||||
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
|
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
|
||||||
|
import org.junit.AfterClass;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
@@ -68,6 +69,11 @@ public class WorldgenTerrainHeightmapsTest {
|
|||||||
NativeBlockTags.bindHeightmapFixtures();
|
NativeBlockTags.bindHeightmapFixtures();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void restoreBlockTags() {
|
||||||
|
NativeBlockTags.restoreTags();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authoringHeightmapsReadPlacedObjectsAndFluidsWithoutTerrainResolvers() {
|
public void authoringHeightmapsReadPlacedObjectsAndFluidsWithoutTerrainResolvers() {
|
||||||
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
|
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
|
||||||
|
|||||||
+4
@@ -13,6 +13,7 @@ import com.google.gson.JsonParser;
|
|||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.junit.AfterClass;
|
import org.junit.AfterClass;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -26,6 +27,9 @@ import static org.junit.Assert.assertNull;
|
|||||||
import static org.junit.Assert.assertThrows;
|
import static org.junit.Assert.assertThrows;
|
||||||
|
|
||||||
public final class BareMobSpawnHistoryTest {
|
public final class BareMobSpawnHistoryTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
private static final String LEGACY_RENDERER_IDENTITY = "bukkit-v26_3_R1-generated-registry-json-v1";
|
private static final String LEGACY_RENDERER_IDENTITY = "bukkit-v26_3_R1-generated-registry-json-v1";
|
||||||
private static final String NATURAL_MOB_SPAWNS = "minecraft:gameplay/natural_mob_spawns";
|
private static final String NATURAL_MOB_SPAWNS = "minecraft:gameplay/natural_mob_spawns";
|
||||||
private static final GenerationRegistryContract.PhysicalResourceKey INSTALLED =
|
private static final GenerationRegistryContract.PhysicalResourceKey INSTALLED =
|
||||||
|
|||||||
+4
@@ -29,6 +29,7 @@ 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 net.minecraft.world.level.storage.LevelStorageSource;
|
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
import org.junit.rules.TemporaryFolder;
|
||||||
import org.bukkit.craftbukkit.generator.CustomChunkGenerator;
|
import org.bukkit.craftbukkit.generator.CustomChunkGenerator;
|
||||||
@@ -49,6 +50,9 @@ import static org.mockito.Mockito.spy;
|
|||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
public class HeadlessChunkGeneratorTest {
|
public class HeadlessChunkGeneratorTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public TemporaryFolder temporary = new TemporaryFolder();
|
public TemporaryFolder temporary = new TemporaryFolder();
|
||||||
|
|
||||||
|
|||||||
+4
@@ -31,6 +31,7 @@ import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemp
|
|||||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
@@ -48,6 +49,9 @@ import static org.junit.Assert.assertThrows;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public final class HeadlessChunkLightingTest {
|
public final class HeadlessChunkLightingTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public TemporaryFolder temporary = new TemporaryFolder();
|
public TemporaryFolder temporary = new TemporaryFolder();
|
||||||
|
|
||||||
|
|||||||
+4
@@ -38,6 +38,7 @@ import net.minecraft.world.ticks.ScheduledTick;
|
|||||||
import org.bukkit.NamespacedKey;
|
import org.bukkit.NamespacedKey;
|
||||||
import org.bukkit.persistence.PersistentDataType;
|
import org.bukkit.persistence.PersistentDataType;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
@@ -54,6 +55,9 @@ import static org.mockito.Mockito.mock;
|
|||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class HeadlessChunkSerializationTest {
|
public class HeadlessChunkSerializationTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public TemporaryFolder temporary = new TemporaryFolder();
|
public TemporaryFolder temporary = new TemporaryFolder();
|
||||||
|
|
||||||
|
|||||||
+4
@@ -9,6 +9,7 @@ import net.minecraft.world.item.Items;
|
|||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import net.minecraft.world.level.block.Blocks;
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
|
|
||||||
@@ -18,6 +19,9 @@ import static org.junit.Assert.assertEquals;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public final class HeadlessNativeBootstrapTest {
|
public final class HeadlessNativeBootstrapTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void bindsNativeItemComponentsWithoutStartingServer() throws Exception {
|
public void bindsNativeItemComponentsWithoutStartingServer() throws Exception {
|
||||||
HeadlessNativeBootstrap.bindComponents(HeadlessNativeTestRegistries.get());
|
HeadlessNativeBootstrap.bindComponents(HeadlessNativeTestRegistries.get());
|
||||||
|
|||||||
+4
@@ -7,12 +7,16 @@ import net.minecraft.resources.Identifier;
|
|||||||
import net.minecraft.resources.ResourceKey;
|
import net.minecraft.resources.ResourceKey;
|
||||||
import net.minecraft.world.level.biome.Biome;
|
import net.minecraft.world.level.biome.Biome;
|
||||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public final class HeadlessNativeRegistriesTest {
|
public final class HeadlessNativeRegistriesTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void nativeStructureBiomeTagsUseTheLoadedBiomeHolders() throws Exception {
|
public void nativeStructureBiomeTagsUseTheLoadedBiomeHolders() throws Exception {
|
||||||
RegistryAccess registries = HeadlessNativeTestRegistries.get();
|
RegistryAccess registries = HeadlessNativeTestRegistries.get();
|
||||||
|
|||||||
+4
@@ -4,6 +4,7 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeRegistryAccess;
|
|||||||
import net.minecraft.core.registries.Registries;
|
import net.minecraft.core.registries.Registries;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -17,6 +18,9 @@ import static org.junit.Assert.assertFalse;
|
|||||||
import static org.junit.Assert.assertThrows;
|
import static org.junit.Assert.assertThrows;
|
||||||
|
|
||||||
public final class HeadlessNativeRegistryTest {
|
public final class HeadlessNativeRegistryTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
public static void bootstrap() {
|
public static void bootstrap() {
|
||||||
HeadlessNativeBootstrap.initialize();
|
HeadlessNativeBootstrap.initialize();
|
||||||
|
|||||||
+4
@@ -11,6 +11,7 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeTileReader;
|
|||||||
import net.minecraft.nbt.CompoundTag;
|
import net.minecraft.nbt.CompoundTag;
|
||||||
import net.minecraft.data.registries.VanillaRegistries;
|
import net.minecraft.data.registries.VanillaRegistries;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
@@ -23,6 +24,9 @@ import static org.junit.Assert.assertEquals;
|
|||||||
import static org.junit.Assert.assertThrows;
|
import static org.junit.Assert.assertThrows;
|
||||||
|
|
||||||
public final class HeadlessNativeTileTest {
|
public final class HeadlessNativeTileTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
public static void bootstrap() {
|
public static void bootstrap() {
|
||||||
HeadlessNativeBootstrap.initialize();
|
HeadlessNativeBootstrap.initialize();
|
||||||
|
|||||||
+4
@@ -17,6 +17,7 @@ import org.bukkit.Bukkit;
|
|||||||
import org.bukkit.NamespacedKey;
|
import org.bukkit.NamespacedKey;
|
||||||
import org.bukkit.persistence.PersistentDataType;
|
import org.bukkit.persistence.PersistentDataType;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
import org.junit.rules.TemporaryFolder;
|
||||||
import org.spigotmc.SpigotWorldConfig;
|
import org.spigotmc.SpigotWorldConfig;
|
||||||
@@ -36,6 +37,9 @@ import static org.junit.Assert.assertSame;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public final class HeadlessRegionTerrainTest {
|
public final class HeadlessRegionTerrainTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public TemporaryFolder temporary = new TemporaryFolder();
|
public TemporaryFolder temporary = new TemporaryFolder();
|
||||||
|
|
||||||
|
|||||||
+4
@@ -24,6 +24,7 @@ import net.minecraft.world.level.block.state.BlockState;
|
|||||||
import net.minecraft.world.level.chunk.storage.RegionFile;
|
import net.minecraft.world.level.chunk.storage.RegionFile;
|
||||||
import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
|
import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
@@ -38,6 +39,9 @@ import static org.junit.Assert.assertThrows;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public final class NativeRegionTerrainWriterTest {
|
public final class NativeRegionTerrainWriterTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
public static void bootstrap() {
|
public static void bootstrap() {
|
||||||
SharedConstants.tryDetectVersion();
|
SharedConstants.tryDetectVersion();
|
||||||
|
|||||||
+4
@@ -26,6 +26,7 @@ import net.minecraft.world.level.material.Fluids;
|
|||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.junit.AfterClass;
|
import org.junit.AfterClass;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -40,6 +41,9 @@ import static org.junit.Assert.assertThrows;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public final class OfflineNativeFeatureFeasibilityTest {
|
public final class OfflineNativeFeatureFeasibilityTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
private static HeadlessNativeRegistries loaded;
|
private static HeadlessNativeRegistries loaded;
|
||||||
private static RegistryAccess registries;
|
private static RegistryAccess registries;
|
||||||
private static DimensionType dimension;
|
private static DimensionType dimension;
|
||||||
|
|||||||
+4
@@ -1,6 +1,7 @@
|
|||||||
package art.arcane.iris.probe;
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
import org.junit.rules.TemporaryFolder;
|
||||||
import org.mockito.MockedStatic;
|
import org.mockito.MockedStatic;
|
||||||
@@ -22,6 +23,9 @@ import static org.mockito.Mockito.mock;
|
|||||||
import static org.mockito.Mockito.mockStatic;
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
|
||||||
public final class OfflineRegionPublicationTest {
|
public final class OfflineRegionPublicationTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public TemporaryFolder temporary = new TemporaryFolder();
|
public TemporaryFolder temporary = new TemporaryFolder();
|
||||||
|
|
||||||
|
|||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import io.papermc.paper.registry.PaperRegistryAccess;
|
||||||
|
import org.junit.rules.TestRule;
|
||||||
|
import org.junit.runner.Description;
|
||||||
|
import org.junit.runners.model.Statement;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class PaperRegistryIsolation implements TestRule {
|
||||||
|
@Override
|
||||||
|
public Statement apply(Statement base, Description description) {
|
||||||
|
return new Statement() {
|
||||||
|
@Override
|
||||||
|
public void evaluate() throws Throwable {
|
||||||
|
Map<Object, Object> registries = registries();
|
||||||
|
Map<Object, Object> previous = new HashMap<>(registries);
|
||||||
|
registries.clear();
|
||||||
|
try {
|
||||||
|
base.evaluate();
|
||||||
|
} finally {
|
||||||
|
registries.clear();
|
||||||
|
registries.putAll(previous);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static Map<Object, Object> registries() throws ReflectiveOperationException {
|
||||||
|
Field field = PaperRegistryAccess.class.getDeclaredField("registries");
|
||||||
|
field.setAccessible(true);
|
||||||
|
return (Map<Object, Object>) field.get(PaperRegistryAccess.instance());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package art.arcane.iris;
|
package art.arcane.iris;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletionStage;
|
||||||
import art.arcane.volmlib.util.diagnostics.BukkitDebugDump;
|
import art.arcane.volmlib.util.diagnostics.BukkitDebugDump;
|
||||||
import art.arcane.iris.generation.runtime.IrisEngineEffects;
|
import art.arcane.iris.generation.runtime.IrisEngineEffects;
|
||||||
import art.arcane.iris.generation.runtime.IrisWorldManager;
|
import art.arcane.iris.generation.runtime.IrisWorldManager;
|
||||||
@@ -36,6 +37,7 @@ import art.arcane.iris.world.PendingWorldDeleteQueue;
|
|||||||
import art.arcane.iris.world.PendingWorldReplacementManager;
|
import art.arcane.iris.world.PendingWorldReplacementManager;
|
||||||
import art.arcane.iris.world.WorldRefusalReporter;
|
import art.arcane.iris.world.WorldRefusalReporter;
|
||||||
import art.arcane.iris.configuration.SettingsHotloadWatch;
|
import art.arcane.iris.configuration.SettingsHotloadWatch;
|
||||||
|
import art.arcane.iris.update.IrisUpdateNotifier;
|
||||||
import art.arcane.iris.pack.datapack.ServerConfigurator;
|
import art.arcane.iris.pack.datapack.ServerConfigurator;
|
||||||
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
||||||
import art.arcane.iris.pack.datapack.DatapackIngestService.StartupValidationOutcome;
|
import art.arcane.iris.pack.datapack.DatapackIngestService.StartupValidationOutcome;
|
||||||
@@ -131,8 +133,11 @@ import org.bukkit.command.Command;
|
|||||||
import org.bukkit.command.CommandSender;
|
import org.bukkit.command.CommandSender;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.Event;
|
import org.bukkit.event.Event;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
import org.bukkit.event.HandlerList;
|
import org.bukkit.event.HandlerList;
|
||||||
import org.bukkit.event.Listener;
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.server.PluginDisableEvent;
|
||||||
|
import org.bukkit.event.server.PluginEnableEvent;
|
||||||
import org.bukkit.generator.BiomeProvider;
|
import org.bukkit.generator.BiomeProvider;
|
||||||
import org.bukkit.generator.ChunkGenerator;
|
import org.bukkit.generator.ChunkGenerator;
|
||||||
import org.bukkit.plugin.IllegalPluginAccessException;
|
import org.bukkit.plugin.IllegalPluginAccessException;
|
||||||
@@ -204,6 +209,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
private volatile PlaceholderRegistration papiRegistration;
|
private volatile PlaceholderRegistration papiRegistration;
|
||||||
private volatile IrisPapiListener papiListener;
|
private volatile IrisPapiListener papiListener;
|
||||||
private volatile IrisPapiState papiState;
|
private volatile IrisPapiState papiState;
|
||||||
|
private volatile boolean papiLifecycleActive;
|
||||||
private KMap<Class<? extends IrisService>, IrisService> services;
|
private KMap<Class<? extends IrisService>, IrisService> services;
|
||||||
// Copy-on-write: mutated on the main thread during enable() and iterated by the JVM
|
// Copy-on-write: mutated on the main thread during enable() and iterated by the JVM
|
||||||
// shutdown-hook thread during teardown; a plain list would CME and abort the teardown.
|
// shutdown-hook thread during teardown; a plain list would CME and abort the teardown.
|
||||||
@@ -214,6 +220,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
private final PendingWorldDeleteQueue pendingWorldDeletes = new PendingWorldDeleteQueue(this);
|
private final PendingWorldDeleteQueue pendingWorldDeletes = new PendingWorldDeleteQueue(this);
|
||||||
private final PendingWorldReplacementManager pendingWorldReplacements = new PendingWorldReplacementManager(this);
|
private final PendingWorldReplacementManager pendingWorldReplacements = new PendingWorldReplacementManager(this);
|
||||||
private BukkitLanguageSwitcher languageSwitcher;
|
private BukkitLanguageSwitcher languageSwitcher;
|
||||||
|
private IrisUpdateNotifier updateNotifier;
|
||||||
private BukkitDebugDump debugDump;
|
private BukkitDebugDump debugDump;
|
||||||
private volatile SettingsHotloadWatch settingsHotloadWatch;
|
private volatile SettingsHotloadWatch settingsHotloadWatch;
|
||||||
private volatile Thread serverLifecycleThread;
|
private volatile Thread serverLifecycleThread;
|
||||||
@@ -636,6 +643,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
languageSwitcher = BukkitLanguageSwitcher.register(this, IrisLanguage.selections(),
|
languageSwitcher = BukkitLanguageSwitcher.register(this, IrisLanguage.selections(),
|
||||||
new BukkitLanguageSwitcher.Options("iris", "iris.all",
|
new BukkitLanguageSwitcher.Options("iris", "iris.all",
|
||||||
DirectorMiniMenu.Theme.irisGreen(), IrisLanguage.directorResolver(), IrisLanguage.editorOptions()));
|
DirectorMiniMenu.Theme.irisGreen(), IrisLanguage.directorResolver(), IrisLanguage.editorOptions()));
|
||||||
|
updateNotifier = new IrisUpdateNotifier(this);
|
||||||
PaperLibBootstrap.install();
|
PaperLibBootstrap.install();
|
||||||
SimdSupport.install();
|
SimdSupport.install();
|
||||||
timings.mark("bootstrap");
|
timings.mark("bootstrap");
|
||||||
@@ -863,7 +871,14 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
*/
|
*/
|
||||||
private CompletableFuture<ManagedWorldLoader.ManagedWorldLoad> loadManagedWorld(String configuredWorldName) {
|
private CompletableFuture<ManagedWorldLoader.ManagedWorldLoad> loadManagedWorld(String configuredWorldName) {
|
||||||
return worldReconciler.loadWorld(ServerProperties.BUKKIT_YML, configuredWorldName)
|
return worldReconciler.loadWorld(ServerProperties.BUKKIT_YML, configuredWorldName)
|
||||||
.thenApply(result -> new ManagedWorldLoader.ManagedWorldLoad(result.succeeded(), result.message()));
|
.thenApply(this::managedWorldLoadResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ManagedWorldLoader.ManagedWorldLoad managedWorldLoadResult(BukkitWorldReconciler.LoadResult result) {
|
||||||
|
if (!result.succeeded() && result.failure() != null) {
|
||||||
|
reportError("Could not explicitly load Iris world " + result.worldKey() + ": " + result.message(), result.failure());
|
||||||
|
}
|
||||||
|
return new ManagedWorldLoader.ManagedWorldLoad(result.succeeded(), result.message());
|
||||||
}
|
}
|
||||||
|
|
||||||
public PendingWorldReplacementManager pendingWorldReplacements() {
|
public PendingWorldReplacementManager pendingWorldReplacements() {
|
||||||
@@ -927,6 +942,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
ServerConfigurator.restartAtStartupBoundary(restartReason);
|
ServerConfigurator.restartAtStartupBoundary(restartReason);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
papiLifecycleActive = true;
|
||||||
|
setupPapi();
|
||||||
reportLockedRuntime();
|
reportLockedRuntime();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -973,6 +990,12 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void onDisable() {
|
public void onDisable() {
|
||||||
|
preservePendingBlockDrops();
|
||||||
|
papiLifecycleActive = false;
|
||||||
|
if (updateNotifier != null) {
|
||||||
|
updateNotifier.close();
|
||||||
|
updateNotifier = null;
|
||||||
|
}
|
||||||
if (debugDump != null) {
|
if (debugDump != null) {
|
||||||
debugDump.close();
|
debugDump.close();
|
||||||
debugDump = null;
|
debugDump = null;
|
||||||
@@ -1011,20 +1034,23 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPreUnload(ReloadAware.PreUnloadReason reason) {
|
public CompletionStage<Void> commitReload(ReloadAware.PreUnloadReason reason) {
|
||||||
|
preservePendingBlockDrops();
|
||||||
|
papiLifecycleActive = false;
|
||||||
teardownPapi();
|
teardownPapi();
|
||||||
if (IrisToolbelt.isServerStopping()) {
|
if (IrisToolbelt.isServerStopping()) {
|
||||||
quiesceRuntimeForServerShutdown("pre-unload:" + reason);
|
quiesceRuntimeForServerShutdown("pre-unload:" + reason);
|
||||||
startPostStopFinisher();
|
startPostStopFinisher();
|
||||||
Iris.debug("Pre-unload hook deferred generator teardown until Paper closes its chunk schedulers.");
|
Iris.debug("Pre-unload hook deferred generator teardown until Paper closes its chunk schedulers.");
|
||||||
return;
|
return CompletableFuture.completedFuture(null);
|
||||||
}
|
}
|
||||||
if (alreadyDrained.get()) {
|
if (alreadyDrained.get()) {
|
||||||
Iris.debug("Pre-unload hook skipped; Iris already drained.");
|
Iris.debug("Pre-unload hook skipped; Iris already drained.");
|
||||||
return;
|
return CompletableFuture.completedFuture(null);
|
||||||
}
|
}
|
||||||
Iris.debug("BileTools pre-unload hook fired (" + reason + "). Freezing all Iris worlds.");
|
Iris.debug("BileTools pre-unload hook fired (" + reason + "). Freezing all Iris worlds.");
|
||||||
drainOnce("pre-unload:" + reason, 45L);
|
drainOnce("pre-unload:" + reason, 45L);
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1259,6 +1285,20 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
&& preservation.hasActiveResources();
|
&& preservation.hasActiveResources();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void preservePendingBlockDrops() {
|
||||||
|
for (World world : Bukkit.getWorlds()) {
|
||||||
|
PlatformChunkGenerator generator = IrisToolbelt.access(world);
|
||||||
|
if (generator != null && generator.getEngine() != null
|
||||||
|
&& generator.getEngine().getWorldManager() instanceof IrisWorldManager manager) {
|
||||||
|
manager.flushPendingBlockDropsAsync().whenComplete((ignored, failure) -> {
|
||||||
|
if (failure != null) {
|
||||||
|
Iris.reportError("Failed to preserve pending block loot before Iris teardown.", failure);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void drainWorldGenerators(String reason, long timeoutSeconds) {
|
private void drainWorldGenerators(String reason, long timeoutSeconds) {
|
||||||
List<World> irisWorlds = new ArrayList<>();
|
List<World> irisWorlds = new ArrayList<>();
|
||||||
List<PlatformChunkGenerator> generators = new ArrayList<>();
|
List<PlatformChunkGenerator> generators = new ArrayList<>();
|
||||||
@@ -1313,8 +1353,23 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupPapi() {
|
@EventHandler
|
||||||
if (!PlaceholderRegistration.isPlaceholderApiEnabled()) {
|
public void onPlaceholderApiEnable(PluginEnableEvent event) {
|
||||||
|
if ("PlaceholderAPI".equals(event.getPlugin().getName())) {
|
||||||
|
setupPapi();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onPlaceholderApiDisable(PluginDisableEvent event) {
|
||||||
|
if ("PlaceholderAPI".equals(event.getPlugin().getName())) {
|
||||||
|
teardownPapi();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void setupPapi() {
|
||||||
|
if (!papiLifecycleActive || papiRegistration != null
|
||||||
|
|| !PlaceholderRegistration.isPlaceholderApiEnabled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1341,7 +1396,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
papiRegistration = registration;
|
papiRegistration = registration;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void teardownPapi() {
|
private synchronized void teardownPapi() {
|
||||||
IrisPapiListener listener = papiListener;
|
IrisPapiListener listener = papiListener;
|
||||||
papiListener = null;
|
papiListener = null;
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import art.arcane.iris.localization.IrisLanguage;
|
|||||||
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
||||||
import art.arcane.iris.studio.object.ObjectStudioSaveService;
|
import art.arcane.iris.studio.object.ObjectStudioSaveService;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.generation.locator.SubterrainLocator;
|
||||||
|
import art.arcane.iris.generation.subterrain.IrisSubterrainFamily;
|
||||||
import art.arcane.iris.structure.placement.IrisStructureLocator;
|
import art.arcane.iris.structure.placement.IrisStructureLocator;
|
||||||
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
|
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
|
||||||
import art.arcane.iris.structure.placement.StructureReachability;
|
import art.arcane.iris.structure.placement.StructureReachability;
|
||||||
@@ -72,7 +74,7 @@ import java.util.Set;
|
|||||||
|
|
||||||
@Director(name = "find", origin = DirectorOrigin.PLAYER, description = "Iris Find commands", descriptionKey = "iris.director.commandfind.director.iris_find_commands", aliases = "goto")
|
@Director(name = "find", origin = DirectorOrigin.PLAYER, description = "Iris Find commands", descriptionKey = "iris.director.commandfind.director.iris_find_commands", aliases = "goto")
|
||||||
public class CommandFind implements DirectorExecutor {
|
public class CommandFind implements DirectorExecutor {
|
||||||
@Director(description = "Find a biome", descriptionKey = "iris.director.commandfind.director.find_biome")
|
@Director(description = "Find a biome", sync = true, descriptionKey = "iris.director.commandfind.director.find_biome")
|
||||||
public void biome(
|
public void biome(
|
||||||
@Param(description = "The biome to look for", descriptionKey = "iris.director.commandfind.param.biome_look", customHandler = ReachableBiomeHandler.class)
|
@Param(description = "The biome to look for", descriptionKey = "iris.director.commandfind.param.biome_look", customHandler = ReachableBiomeHandler.class)
|
||||||
IrisBiome biome,
|
IrisBiome biome,
|
||||||
@@ -89,6 +91,78 @@ public class CommandFind implements DirectorExecutor {
|
|||||||
EngineBukkitOps.gotoBiome(e, biome, player(), teleport);
|
EngineBukkitOps.gotoBiome(e, biome, player(), teleport);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Director(description = "Find an authored underground feature by id or geometry family", sync = true)
|
||||||
|
public void subterrain(
|
||||||
|
@Param(description = "Feature definition id or family: tectonic_fault, cenote, lava_tube, travertine_terraces")
|
||||||
|
String feature,
|
||||||
|
@Param(description = "Search radius in blocks", defaultValue = "8192")
|
||||||
|
int radius,
|
||||||
|
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||||
|
boolean teleport
|
||||||
|
) {
|
||||||
|
String key = feature.trim();
|
||||||
|
IrisSubterrainFamily family = null;
|
||||||
|
try {
|
||||||
|
family = IrisSubterrainFamily.valueOf(key.toUpperCase(Locale.ROOT).replace('-', '_'));
|
||||||
|
} catch (IllegalArgumentException ignored) {
|
||||||
|
}
|
||||||
|
locateSubterrain(new SubterrainLocator.Query(family == null ? key : "", family, ""), radius, teleport);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Director(name = "underground-biome", description = "Find a bounded authored underground biome", sync = true)
|
||||||
|
public void undergroundBiome(
|
||||||
|
@Param(description = "The underground biome to look for", customHandler = ReachableBiomeHandler.class)
|
||||||
|
IrisBiome biome,
|
||||||
|
@Param(description = "Search radius in blocks", defaultValue = "8192")
|
||||||
|
int radius,
|
||||||
|
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||||
|
boolean teleport
|
||||||
|
) {
|
||||||
|
locateSubterrain(new SubterrainLocator.Query("", null, biome.getLoadKey()), radius, teleport);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void locateSubterrain(SubterrainLocator.Query query, int radius, boolean teleport) {
|
||||||
|
Engine activeEngine = engine();
|
||||||
|
VolmitSender commandSender = sender();
|
||||||
|
Player target = player();
|
||||||
|
if (activeEngine == null || commandSender == null || target == null) {
|
||||||
|
if (commandSender != null) {
|
||||||
|
commandSender.sendMessage(C.RED + "Run this command from an Iris world.");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (radius < 0 || radius > 32768) {
|
||||||
|
commandSender.sendMessage(C.RED + "Search radius must be between 0 and 32768 blocks.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Location origin = target.getLocation();
|
||||||
|
World world = origin.getWorld();
|
||||||
|
commandSender.sendMessage(C.GRAY + "Searching authored underground plans...");
|
||||||
|
J.a(() -> {
|
||||||
|
try {
|
||||||
|
SubterrainLocator.Result found = GenerationSemanticQueries.nearestSubterrain(activeEngine, query,
|
||||||
|
origin.getBlockX(), origin.getBlockY(), origin.getBlockZ(), radius).orElse(null);
|
||||||
|
if (found == null) {
|
||||||
|
sendStructureMessage(target, commandSender,
|
||||||
|
C.YELLOW + "No matching authored underground volume found within " + radius + " blocks.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String label = found.family().name().toLowerCase(Locale.ROOT) + " " + found.featureId();
|
||||||
|
if (!teleport) {
|
||||||
|
sendStructureMessage(target, commandSender, C.GREEN + "Found " + label + " at "
|
||||||
|
+ found.x() + ", " + found.y() + ", " + found.z() + ".");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
prepareStructureTeleport(target, world, commandSender, label,
|
||||||
|
new Location(world, found.x(), found.y(), found.z()), true);
|
||||||
|
} catch (Throwable error) {
|
||||||
|
sendStructureMessage(target, commandSender,
|
||||||
|
C.RED + "Could not locate authored underground volume: " + error.getMessage());
|
||||||
|
Iris.reportError("Could not locate authored underground volume.", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Director(description = "Find a region", descriptionKey = "iris.director.commandfind.director.find_region")
|
@Director(description = "Find a region", descriptionKey = "iris.director.commandfind.director.find_region")
|
||||||
public void region(
|
public void region(
|
||||||
@Param(description = "The region to look for", descriptionKey = "iris.director.commandfind.param.region_look", customHandler = ReachableRegionHandler.class)
|
@Param(description = "The region to look for", descriptionKey = "iris.director.commandfind.param.region_look", customHandler = ReachableRegionHandler.class)
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
|
|
||||||
package art.arcane.iris.command;
|
package art.arcane.iris.command;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.PackFingerprints;
|
||||||
|
|
||||||
import art.arcane.iris.Iris;
|
import art.arcane.iris.Iris;
|
||||||
import art.arcane.iris.pack.validation.PackCompatReport;
|
import art.arcane.iris.pack.validation.PackCompatReport;
|
||||||
import art.arcane.iris.pack.PackDirectoryResolver;
|
import art.arcane.iris.pack.PackDirectoryResolver;
|
||||||
@@ -174,7 +176,6 @@ public class CommandPack implements DirectorExecutor {
|
|||||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_CLEANUP_CANDIDATES_FOUND_PACK, MessageArgument.untrusted("pack", String.valueOf(pack))));
|
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_CLEANUP_CANDIDATES_FOUND_PACK, MessageArgument.untrusted("pack", String.valueOf(pack))));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
PackValidationRegistry.remove(packFolder.getName());
|
|
||||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_QUARANTINED_CLEANUP_CANDIDATE_S_UNDER, MessageArgument.untrusted("size", String.valueOf(result.quarantinedPaths().size())), MessageArgument.untrusted("quarantinePath", String.valueOf(result.quarantinePath()))));
|
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_QUARANTINED_CLEANUP_CANDIDATE_S_UNDER, MessageArgument.untrusted("size", String.valueOf(result.quarantinedPaths().size())), MessageArgument.untrusted("quarantinePath", String.valueOf(result.quarantinePath()))));
|
||||||
reportPaths(s, result.quarantinedPaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_QUARANTINED);
|
reportPaths(s, result.quarantinedPaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_QUARANTINED);
|
||||||
return;
|
return;
|
||||||
@@ -230,7 +231,6 @@ public class CommandPack implements DirectorExecutor {
|
|||||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NOTHING_RESTORE_PACK, MessageArgument.untrusted("pack", String.valueOf(pack))));
|
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NOTHING_RESTORE_PACK, MessageArgument.untrusted("pack", String.valueOf(pack))));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
PackValidationRegistry.remove(packFolder.getName());
|
|
||||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_RESTORED_FILE_S_FROM, MessageArgument.untrusted("size", String.valueOf(result.restoredPaths().size())), MessageArgument.untrusted("dumpPath", String.valueOf(result.dumpPath()))));
|
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_RESTORED_FILE_S_FROM, MessageArgument.untrusted("size", String.valueOf(result.restoredPaths().size())), MessageArgument.untrusted("dumpPath", String.valueOf(result.dumpPath()))));
|
||||||
reportPaths(s, result.restoredPaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_RESTORED);
|
reportPaths(s, result.restoredPaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_RESTORED);
|
||||||
return;
|
return;
|
||||||
@@ -363,47 +363,79 @@ public class CommandPack implements DirectorExecutor {
|
|||||||
return persisted == null || persisted.isBlank() ? "unknown" : persisted;
|
return persisted == null || persisted.isBlank() ? "unknown" : persisted;
|
||||||
}
|
}
|
||||||
|
|
||||||
private PackValidationResult runValidate(VolmitSender s, File packFolder) {
|
PackValidationResult runValidate(VolmitSender s, File packFolder) {
|
||||||
|
PackValidationRegistry.ValidationTicket ticket =
|
||||||
|
PackValidationRegistry.tryBeginValidation(packFolder.toPath());
|
||||||
|
PackValidationResult result;
|
||||||
|
String contentFingerprint = "";
|
||||||
|
String contextFingerprint = "";
|
||||||
try {
|
try {
|
||||||
PackValidationResult result = PackValidator.validate(packFolder);
|
if (ticket == null) {
|
||||||
PackValidationRegistry.publish(result);
|
throw new IllegalStateException("Pack content is currently being changed.");
|
||||||
reportResult(s, result);
|
}
|
||||||
return result;
|
contentFingerprint = PackFingerprints.computePackTreeFingerprint(packFolder);
|
||||||
|
contextFingerprint = PackValidationCache.contextFingerprint();
|
||||||
|
result = PackValidator.validate(packFolder);
|
||||||
|
if (!contentFingerprint.equals(PackFingerprints.computePackTreeFingerprint(packFolder))
|
||||||
|
|| !contextFingerprint.equals(PackValidationCache.contextFingerprint())) {
|
||||||
|
throw new IllegalStateException("Pack content or validation context changed during validation.");
|
||||||
|
}
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Iris.reportError("Pack validation failed for '" + packFolder.getName() + "'", e);
|
Iris.reportError("Pack validation failed for '" + packFolder.getName() + "'", e);
|
||||||
String detail = e.getMessage() == null || e.getMessage().isBlank()
|
String detail = e.getMessage() == null || e.getMessage().isBlank()
|
||||||
? e.getClass().getSimpleName()
|
? e.getClass().getSimpleName()
|
||||||
: e.getMessage();
|
: e.getMessage();
|
||||||
PackValidationResult result = new PackValidationResult(
|
result = new PackValidationResult(
|
||||||
packFolder.getName(),
|
packFolder.getName(),
|
||||||
List.of("Pack validation failed with " + e.getClass().getSimpleName() + ": " + detail),
|
List.of("Pack validation failed with " + e.getClass().getSimpleName() + ": " + detail),
|
||||||
List.of(),
|
List.of(),
|
||||||
System.currentTimeMillis());
|
System.currentTimeMillis());
|
||||||
|
contentFingerprint = "";
|
||||||
|
contextFingerprint = "";
|
||||||
|
}
|
||||||
|
if (PackValidationRegistry.publishIfCurrent(ticket, result, contentFingerprint, contextFingerprint)) {
|
||||||
PackValidationRegistry.publish(result);
|
PackValidationRegistry.publish(result);
|
||||||
|
} else {
|
||||||
|
result = new PackValidationResult(packFolder.getName(),
|
||||||
|
List.of("Pack content changed during validation."), List.of(), System.currentTimeMillis());
|
||||||
|
}
|
||||||
reportResult(s, result);
|
reportResult(s, result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void persistValidationCache(File packsRoot) {
|
private void persistValidationCache(File packsRoot) {
|
||||||
|
try {
|
||||||
|
String contextFingerprint = PackValidationCache.contextFingerprint();
|
||||||
|
PackFingerprints.PackContentSnapshot snapshot = PackFingerprints.computePackContentSnapshot(packsRoot);
|
||||||
List<File> packDirectories = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
|
List<File> packDirectories = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
|
||||||
List<PackValidationResult> results = new ArrayList<>(packDirectories.size());
|
List<PackValidationResult> results = new ArrayList<>(packDirectories.size());
|
||||||
for (File packDirectory : packDirectories) {
|
for (File packDirectory : packDirectories) {
|
||||||
PackValidationResult result = PackValidationRegistry.get(packDirectory.getName());
|
PackValidationResult result = PackValidationRegistry.getMatching(
|
||||||
|
packDirectory.toPath(), snapshot.packContents().get(packDirectory.getName()));
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
// The boot cache only loads when it covers every pack, so a partial write is
|
|
||||||
// pointless - but say so instead of silently skipping the persist forever.
|
|
||||||
Iris.warn("Pack validation cache not written: \"" + packDirectory.getName()
|
Iris.warn("Pack validation cache not written: \"" + packDirectory.getName()
|
||||||
+ "\" has no registered result. Run /iris pack validate all to repopulate it.");
|
+ "\" has no validation matching its current content. Run /iris pack validate to refresh it.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
results.add(result);
|
results.add(result);
|
||||||
}
|
}
|
||||||
try {
|
if (!snapshot.content().equals(PackFingerprints.computePackContentSnapshot(packsRoot).content())
|
||||||
|
|| !contextFingerprint.equals(PackValidationCache.contextFingerprint())) {
|
||||||
|
Iris.warn("Pack validation cache not written because pack content or validation context changed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int index = 0; index < packDirectories.size(); index++) {
|
||||||
|
File packDirectory = packDirectories.get(index);
|
||||||
|
if (PackValidationRegistry.getMatching(packDirectory.toPath(),
|
||||||
|
snapshot.packContents().get(packDirectory.getName())) != results.get(index)) {
|
||||||
|
Iris.warn("Pack validation cache not written because validation changed during persistence.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
PackValidationCache.save(
|
PackValidationCache.save(
|
||||||
Iris.instance.getDataFile("cache", "pack-validation.json").toPath(),
|
Iris.instance.getDataFile("cache", "pack-validation.json").toPath(),
|
||||||
PackValidationCache.contentFingerprint(packsRoot),
|
snapshot.content(),
|
||||||
PackValidationCache.contextFingerprint(),
|
contextFingerprint,
|
||||||
results);
|
results);
|
||||||
} catch (IOException | RuntimeException e) {
|
} catch (IOException | RuntimeException e) {
|
||||||
Iris.reportError("Could not persist refreshed pack-validation results", e);
|
Iris.reportError("Could not persist refreshed pack-validation results", e);
|
||||||
|
|||||||
+29
-1
@@ -6,6 +6,7 @@ import art.arcane.iris.generation.runtime.IrisTelemetrySnapshot;
|
|||||||
import art.arcane.iris.Iris;
|
import art.arcane.iris.Iris;
|
||||||
import art.arcane.iris.generation.runtime.EngineTelemetrySnapshot;
|
import art.arcane.iris.generation.runtime.EngineTelemetrySnapshot;
|
||||||
import art.arcane.iris.spi.IrisServices;
|
import art.arcane.iris.spi.IrisServices;
|
||||||
|
import art.arcane.iris.world.task.J;
|
||||||
import art.arcane.iris.platform.bukkit.plugin.IrisService;
|
import art.arcane.iris.platform.bukkit.plugin.IrisService;
|
||||||
import art.arcane.volmlib.integration.IntegrationHandshakeRequest;
|
import art.arcane.volmlib.integration.IntegrationHandshakeRequest;
|
||||||
import art.arcane.volmlib.integration.IntegrationHandshakeResponse;
|
import art.arcane.volmlib.integration.IntegrationHandshakeResponse;
|
||||||
@@ -13,6 +14,9 @@ import art.arcane.volmlib.integration.IntegrationHeartbeat;
|
|||||||
import art.arcane.volmlib.integration.IntegrationMetricDescriptor;
|
import art.arcane.volmlib.integration.IntegrationMetricDescriptor;
|
||||||
import art.arcane.volmlib.integration.IntegrationMetricGroup;
|
import art.arcane.volmlib.integration.IntegrationMetricGroup;
|
||||||
import art.arcane.volmlib.integration.IntegrationMetricSample;
|
import art.arcane.volmlib.integration.IntegrationMetricSample;
|
||||||
|
import art.arcane.volmlib.integration.IntegrationMetricPublisher;
|
||||||
|
import art.arcane.volmlib.integration.IntegrationMetricSnapshot;
|
||||||
|
import art.arcane.volmlib.integration.IntegrationSnapshotProvider;
|
||||||
import art.arcane.volmlib.integration.IntegrationMetricSchema;
|
import art.arcane.volmlib.integration.IntegrationMetricSchema;
|
||||||
import art.arcane.volmlib.integration.IntegrationProtocolNegotiator;
|
import art.arcane.volmlib.integration.IntegrationProtocolNegotiator;
|
||||||
import art.arcane.volmlib.integration.IntegrationProtocolVersion;
|
import art.arcane.volmlib.integration.IntegrationProtocolVersion;
|
||||||
@@ -30,7 +34,7 @@ import java.util.Optional;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
public class IrisIntegrationService implements IrisService, IntegrationServiceContract {
|
public class IrisIntegrationService implements IrisService, IntegrationSnapshotProvider {
|
||||||
private static final IntegrationProtocolVersion CURRENT_PROTOCOL = new IntegrationProtocolVersion(1, 2);
|
private static final IntegrationProtocolVersion CURRENT_PROTOCOL = new IntegrationProtocolVersion(1, 2);
|
||||||
private static final Set<IntegrationProtocolVersion> SUPPORTED_PROTOCOLS = Set.of(
|
private static final Set<IntegrationProtocolVersion> SUPPORTED_PROTOCOLS = Set.of(
|
||||||
new IntegrationProtocolVersion(1, 0),
|
new IntegrationProtocolVersion(1, 0),
|
||||||
@@ -41,6 +45,7 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
|
|||||||
"handshake",
|
"handshake",
|
||||||
"heartbeat",
|
"heartbeat",
|
||||||
"metrics",
|
"metrics",
|
||||||
|
IntegrationSnapshotProvider.CAPABILITY,
|
||||||
"metric-groups",
|
"metric-groups",
|
||||||
"iris-engine-metrics",
|
"iris-engine-metrics",
|
||||||
"iris-world-metrics"
|
"iris-world-metrics"
|
||||||
@@ -63,6 +68,9 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
|
|||||||
);
|
);
|
||||||
|
|
||||||
private final Supplier<IrisTelemetrySnapshot> telemetrySupplier;
|
private final Supplier<IrisTelemetrySnapshot> telemetrySupplier;
|
||||||
|
private final IntegrationMetricPublisher snapshots = new IntegrationMetricPublisher(
|
||||||
|
IntegrationMetricSchema.irisKeys().size(), 30_000L);
|
||||||
|
private int snapshotTaskId = -1;
|
||||||
|
|
||||||
public IrisIntegrationService() {
|
public IrisIntegrationService() {
|
||||||
this(IrisIntegrationService::currentTelemetry);
|
this(IrisIntegrationService::currentTelemetry);
|
||||||
@@ -77,11 +85,17 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
|
|||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
Bukkit.getServicesManager().register(IntegrationServiceContract.class, this, Iris.instance, ServicePriority.Normal);
|
Bukkit.getServicesManager().register(IntegrationServiceContract.class, this, Iris.instance, ServicePriority.Normal);
|
||||||
|
snapshotTaskId = J.ar(this::publishSnapshots, 20);
|
||||||
Iris.verbose("Integration provider registered for Iris");
|
Iris.verbose("Integration provider registered for Iris");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDisable() {
|
public void onDisable() {
|
||||||
|
if (snapshotTaskId != -1) {
|
||||||
|
J.car(snapshotTaskId);
|
||||||
|
snapshotTaskId = -1;
|
||||||
|
}
|
||||||
|
snapshots.clear();
|
||||||
Bukkit.getServicesManager().unregister(IntegrationServiceContract.class, this);
|
Bukkit.getServicesManager().unregister(IntegrationServiceContract.class, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,6 +202,20 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
|
|||||||
return Map.copyOf(selected);
|
return Map.copyOf(selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IntegrationMetricSnapshot snapshotMetrics(Set<String> metricKeys) {
|
||||||
|
return snapshots.snapshotMetrics(metricKeys, System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
void publishSnapshots() {
|
||||||
|
IntegrationMetricPublisher.Demand demand = snapshots.demandedKeys(System.currentTimeMillis());
|
||||||
|
if (demand.keys().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<String, IntegrationMetricSample> samples = sampleMetrics(demand.keys());
|
||||||
|
snapshots.publish(demand, System.currentTimeMillis(), samples);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<IntegrationMetricGroup> metricGroups() {
|
public List<IntegrationMetricGroup> metricGroups() {
|
||||||
IrisTelemetrySnapshot snapshot = safeSnapshot();
|
IrisTelemetrySnapshot snapshot = safeSnapshot();
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package art.arcane.iris.update;
|
||||||
|
|
||||||
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
|
import art.arcane.volmlib.util.update.BukkitUpdateService;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public final class IrisUpdateNotifier implements AutoCloseable {
|
||||||
|
private final Consumer<IrisSettings> settingsListener = this::reconfigure;
|
||||||
|
private final BukkitUpdateService updates;
|
||||||
|
private volatile boolean enabled;
|
||||||
|
private boolean closed;
|
||||||
|
|
||||||
|
public IrisUpdateNotifier(Plugin plugin) {
|
||||||
|
enabled = IrisSettings.get().getGeneral().isUpdateNotifications();
|
||||||
|
updates = BukkitUpdateService.register(plugin, new BukkitUpdateService.Options(
|
||||||
|
"VolmitSoftware", "Iris", "iris.update", () -> enabled));
|
||||||
|
IrisSettings.addSettingsListener(settingsListener);
|
||||||
|
reconfigure(IrisSettings.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void reconfigure(IrisSettings settings) {
|
||||||
|
if (closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
enabled = settings.getGeneral().isUpdateNotifications();
|
||||||
|
updates.reconfigure();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void close() {
|
||||||
|
closed = true;
|
||||||
|
IrisSettings.removeSettingsListener(settingsListener);
|
||||||
|
updates.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-10
@@ -18,8 +18,9 @@
|
|||||||
|
|
||||||
package art.arcane.iris.world;
|
package art.arcane.iris.world;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.PackFingerprints;
|
||||||
|
|
||||||
import art.arcane.iris.configuration.IrisSettings;
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
import art.arcane.iris.pack.datapack.ServerConfigurator;
|
|
||||||
|
|
||||||
import art.arcane.iris.Iris;
|
import art.arcane.iris.Iris;
|
||||||
import art.arcane.iris.world.lifecycle.WorldLifecycleStaging;
|
import art.arcane.iris.world.lifecycle.WorldLifecycleStaging;
|
||||||
@@ -184,12 +185,12 @@ public final class IrisWorldGeneratorResolver {
|
|||||||
}
|
}
|
||||||
List<String> packNames = packDirs.stream().map(File::getName).sorted().toList();
|
List<String> packNames = packDirs.stream().map(File::getName).sorted().toList();
|
||||||
Path cacheFile = IrisPlatforms.get().dataFile("cache", "pack-validation.json").toPath();
|
Path cacheFile = IrisPlatforms.get().dataFile("cache", "pack-validation.json").toPath();
|
||||||
ServerConfigurator.PackContentSnapshot contentSnapshot =
|
PackFingerprints.PackContentSnapshot contentSnapshot =
|
||||||
new ServerConfigurator.PackContentSnapshot("", Map.of());
|
new PackFingerprints.PackContentSnapshot("", Map.of());
|
||||||
String contextFingerprint = "";
|
String contextFingerprint = "";
|
||||||
Optional<List<PackValidationResult>> cached = Optional.empty();
|
Optional<List<PackValidationResult>> cached = Optional.empty();
|
||||||
try {
|
try {
|
||||||
contentSnapshot = ServerConfigurator.computePackContentSnapshot(packsRoot);
|
contentSnapshot = PackFingerprints.computePackContentSnapshot(packsRoot);
|
||||||
contextFingerprint = PackValidationCache.contextFingerprint();
|
contextFingerprint = PackValidationCache.contextFingerprint();
|
||||||
if (!externalContentChanged) {
|
if (!externalContentChanged) {
|
||||||
cached = PackValidationCache.load(
|
cached = PackValidationCache.load(
|
||||||
@@ -274,19 +275,19 @@ public final class IrisWorldGeneratorResolver {
|
|||||||
private static FreshValidation validateStablePacks(
|
private static FreshValidation validateStablePacks(
|
||||||
File packsRoot,
|
File packsRoot,
|
||||||
List<File> initialPackDirs,
|
List<File> initialPackDirs,
|
||||||
ServerConfigurator.PackContentSnapshot initialSnapshot
|
PackFingerprints.PackContentSnapshot initialSnapshot
|
||||||
) {
|
) {
|
||||||
List<File> packDirs = initialPackDirs;
|
List<File> packDirs = initialPackDirs;
|
||||||
ServerConfigurator.PackContentSnapshot before = initialSnapshot;
|
PackFingerprints.PackContentSnapshot before = initialSnapshot;
|
||||||
for (int attempt = 0; attempt < VALIDATION_STABILITY_ATTEMPTS; attempt++) {
|
for (int attempt = 0; attempt < VALIDATION_STABILITY_ATTEMPTS; attempt++) {
|
||||||
List<String> packNames = packDirs.stream().map(File::getName).sorted().toList();
|
List<String> packNames = packDirs.stream().map(File::getName).sorted().toList();
|
||||||
List<PackValidationResult> results = validatePacks(packDirs);
|
List<PackValidationResult> results = validatePacks(packDirs);
|
||||||
ServerConfigurator.PackContentSnapshot after;
|
PackFingerprints.PackContentSnapshot after;
|
||||||
try {
|
try {
|
||||||
after = ServerConfigurator.computePackContentSnapshot(packsRoot);
|
after = PackFingerprints.computePackContentSnapshot(packsRoot);
|
||||||
} catch (RuntimeException exception) {
|
} catch (RuntimeException exception) {
|
||||||
Iris.reportError("Could not verify Iris pack bytes after validation", exception);
|
Iris.reportError("Could not verify Iris pack bytes after validation", exception);
|
||||||
after = new ServerConfigurator.PackContentSnapshot("", Map.of());
|
after = new PackFingerprints.PackContentSnapshot("", Map.of());
|
||||||
}
|
}
|
||||||
List<File> afterPackDirs = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
|
List<File> afterPackDirs = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
|
||||||
List<String> afterPackNames = afterPackDirs.stream().map(File::getName).sorted().toList();
|
List<String> afterPackNames = afterPackDirs.stream().map(File::getName).sorted().toList();
|
||||||
@@ -923,7 +924,7 @@ public final class IrisWorldGeneratorResolver {
|
|||||||
|
|
||||||
private record FreshValidation(
|
private record FreshValidation(
|
||||||
List<File> packDirs,
|
List<File> packDirs,
|
||||||
ServerConfigurator.PackContentSnapshot contentSnapshot,
|
PackFingerprints.PackContentSnapshot contentSnapshot,
|
||||||
List<PackValidationResult> results,
|
List<PackValidationResult> results,
|
||||||
boolean stable
|
boolean stable
|
||||||
) {
|
) {
|
||||||
|
|||||||
+18
-4
@@ -3,19 +3,33 @@ package art.arcane.iris.world.tree;
|
|||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.block.BlockBreakEvent;
|
import org.bukkit.event.block.BlockBreakEvent;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
final class RoutedBlockBreakEvent extends BlockBreakEvent implements BlockDropRouter {
|
final class RoutedBlockBreakEvent extends BlockBreakEvent implements BlockDropRouter {
|
||||||
private final TreeFellerSVC service;
|
private final TreeFellerSVC service;
|
||||||
private final FellingRun run;
|
private final FellingRun run;
|
||||||
|
private final ItemStack tool;
|
||||||
|
|
||||||
RoutedBlockBreakEvent(Block block, Player player, FellingRun run, TreeFellerSVC service) {
|
RoutedBlockBreakEvent(Options options) {
|
||||||
super(block, player);
|
super(options.block(), options.player());
|
||||||
this.run = run;
|
run = options.run();
|
||||||
this.service = service;
|
service = options.service();
|
||||||
|
tool = Objects.requireNonNull(options.tool(), "tool").clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<ItemStack> captureNativeDrops() {
|
||||||
|
return getBlock().getDrops(tool);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean routeDrop(Object drop) {
|
public boolean routeDrop(Object drop) {
|
||||||
return service.isServiceEnabled() && run.presentation.routeDrop(drop);
|
return service.isServiceEnabled() && run.presentation.routeDrop(drop);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
record Options(Block block, Player player, FellingRun run, TreeFellerSVC service, ItemStack tool) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+2
-1
@@ -387,7 +387,8 @@ final class TreeFellingRunner {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
BlockBreakEvent probe = new RoutedBlockBreakEvent(block, run.candidate.player(), run, service);
|
BlockBreakEvent probe = new RoutedBlockBreakEvent(new RoutedBlockBreakEvent.Options(
|
||||||
|
block, run.candidate.player(), run, service, reservation.toolForDrops()));
|
||||||
service.managedEvents.add(probe);
|
service.managedEvents.add(probe);
|
||||||
try {
|
try {
|
||||||
Bukkit.getPluginManager().callEvent(probe);
|
Bukkit.getPluginManager().callEvent(probe);
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
|
|||||||
website: VolmitSoftware.com
|
website: VolmitSoftware.com
|
||||||
description: More than a Dimension!
|
description: More than a Dimension!
|
||||||
permissions:
|
permissions:
|
||||||
|
iris.update:
|
||||||
|
description: Allows viewing Iris updates in VolmLib's combined update reports.
|
||||||
|
default: op
|
||||||
iris.debugdump:
|
iris.debugdump:
|
||||||
description: Allows creating and optionally uploading diagnostic reports.
|
description: Allows creating and optionally uploading diagnostic reports.
|
||||||
default: op
|
default: op
|
||||||
@@ -25,34 +28,26 @@ permissions:
|
|||||||
dependencies:
|
dependencies:
|
||||||
server:
|
server:
|
||||||
PlaceholderAPI:
|
PlaceholderAPI:
|
||||||
load: BEFORE
|
load: OMIT
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
CraftEngine:
|
CraftEngine:
|
||||||
load: BEFORE
|
load: OMIT
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
Nexo:
|
Nexo:
|
||||||
load: BEFORE
|
load: OMIT
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
Oraxen:
|
Oraxen:
|
||||||
load: BEFORE
|
load: OMIT
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
ItemsAdder:
|
ItemsAdder:
|
||||||
load: BEFORE
|
load: OMIT
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
SCore:
|
SCore:
|
||||||
load: BEFORE
|
|
||||||
required: false
|
|
||||||
join-classpath: true
|
|
||||||
ExecutableItems:
|
|
||||||
load: BEFORE
|
|
||||||
required: false
|
|
||||||
join-classpath: false
|
|
||||||
MythicLib:
|
|
||||||
load: OMIT
|
load: OMIT
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
@@ -61,11 +56,7 @@ dependencies:
|
|||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
eco:
|
eco:
|
||||||
load: BEFORE
|
load: OMIT
|
||||||
required: false
|
|
||||||
join-classpath: true
|
|
||||||
EcoItems:
|
|
||||||
load: BEFORE
|
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
MythicMobs:
|
MythicMobs:
|
||||||
@@ -77,7 +68,7 @@ dependencies:
|
|||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
KGenerators:
|
KGenerators:
|
||||||
load: BEFORE
|
load: OMIT
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
Multiverse-Core:
|
Multiverse-Core:
|
||||||
@@ -85,6 +76,6 @@ dependencies:
|
|||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
WorldEdit:
|
WorldEdit:
|
||||||
load: BEFORE
|
load: OMIT
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
@@ -3,18 +3,6 @@ version: ${version}
|
|||||||
main: ${main}
|
main: ${main}
|
||||||
folia-supported: true
|
folia-supported: true
|
||||||
load: STARTUP
|
load: STARTUP
|
||||||
softdepend:
|
|
||||||
- PlaceholderAPI
|
|
||||||
- CraftEngine
|
|
||||||
- Nexo
|
|
||||||
- Oraxen
|
|
||||||
- ItemsAdder
|
|
||||||
- SCore
|
|
||||||
- ExecutableItems
|
|
||||||
- eco
|
|
||||||
- EcoItems
|
|
||||||
- KGenerators
|
|
||||||
- WorldEdit
|
|
||||||
loadbefore: [ Multiverse-Core ]
|
loadbefore: [ Multiverse-Core ]
|
||||||
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
|
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
|
||||||
website: VolmitSoftware.com
|
website: VolmitSoftware.com
|
||||||
@@ -23,6 +11,9 @@ commands:
|
|||||||
iris:
|
iris:
|
||||||
aliases: [ ir, irs ]
|
aliases: [ ir, irs ]
|
||||||
permissions:
|
permissions:
|
||||||
|
iris.update:
|
||||||
|
description: Allows viewing Iris updates in VolmLib's combined update reports.
|
||||||
|
default: op
|
||||||
iris.debugdump:
|
iris.debugdump:
|
||||||
description: Allows creating and optionally uploading diagnostic reports.
|
description: Allows creating and optionally uploading diagnostic reports.
|
||||||
default: op
|
default: op
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "Flat",
|
||||||
|
"derivative": "minecraft:plains",
|
||||||
|
"vanillaDerivative": "minecraft:plains",
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"minHeight": 1,
|
||||||
|
"maxHeight": 1,
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:lime_wool"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minHeight": 384,
|
||||||
|
"maxHeight": 384,
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:green_concrete"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"generators": [
|
||||||
|
{
|
||||||
|
"generator": "flat",
|
||||||
|
"min": 96,
|
||||||
|
"max": 96
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"decorators": [
|
||||||
|
{
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:oak_stairs",
|
||||||
|
"data": {
|
||||||
|
"half": "bottom",
|
||||||
|
"facing": "north"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"chance": 1,
|
||||||
|
"forcePlace": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "Decorator Fixture",
|
||||||
|
"version": 1,
|
||||||
|
"mode": {
|
||||||
|
"type": "OVERWORLD"
|
||||||
|
},
|
||||||
|
"regions": [
|
||||||
|
"flat"
|
||||||
|
],
|
||||||
|
"focusRegion": "flat",
|
||||||
|
"focus": "flat",
|
||||||
|
"landChance": 1,
|
||||||
|
"environment": "NORMAL",
|
||||||
|
"dimensionHeight": {
|
||||||
|
"min": -64,
|
||||||
|
"max": 320
|
||||||
|
},
|
||||||
|
"logicalHeight": 384,
|
||||||
|
"fluidHeight": 0,
|
||||||
|
"bedrock": false,
|
||||||
|
"rockPalette": {
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:red_concrete"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"interpolator": {
|
||||||
|
"function": "NONE",
|
||||||
|
"horizontalScale": 1
|
||||||
|
},
|
||||||
|
"seed": 310,
|
||||||
|
"composite": [
|
||||||
|
{
|
||||||
|
"seed": 310,
|
||||||
|
"style": {
|
||||||
|
"style": "FLAT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "Flat",
|
||||||
|
"landBiomes": [
|
||||||
|
"flat"
|
||||||
|
],
|
||||||
|
"seaBiomes": [
|
||||||
|
"flat"
|
||||||
|
],
|
||||||
|
"shoreBiomes": [
|
||||||
|
"flat"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { readFile } from 'node:fs/promises'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'iris-decorators',
|
||||||
|
description: 'Verify authored stair decorations occupy one block and retain their half state.',
|
||||||
|
async run(context) {
|
||||||
|
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
|
||||||
|
context.expect(/^isolated=true\s*$/m.test(source), 'Decorator generation requires an isolated instance')
|
||||||
|
await context.step('create a world with authored stair decorations', async () => {
|
||||||
|
const response = await context.command('/iris create name=decorator_probe type=decorator-fixture seed=78264193',
|
||||||
|
/Successfully created your world|Failed to create/, 180000)
|
||||||
|
context.expect(response.includes('Successfully created your world'), response)
|
||||||
|
})
|
||||||
|
await context.step('client sees single bottom-half stair decorations', async () => {
|
||||||
|
let checked = 0
|
||||||
|
await context.waitUntil(() => {
|
||||||
|
checked = 0
|
||||||
|
const center = context.bot.entity.position.floored()
|
||||||
|
for (let x = -6; x <= 6; x++) {
|
||||||
|
for (let z = -6; z <= 6; z++) {
|
||||||
|
for (let y = -4; y <= 1; y++) {
|
||||||
|
const position = center.offset(x, y, z)
|
||||||
|
if (context.bot.blockAt(position)?.name !== 'lime_wool') continue
|
||||||
|
const decoration = context.bot.blockAt(position.offset(0, 1, 0))
|
||||||
|
const above = context.bot.blockAt(position.offset(0, 2, 0))
|
||||||
|
if (!decoration || !above) return false
|
||||||
|
if (decoration.name !== 'oak_stairs' || decoration.getProperties().half !== 'bottom'
|
||||||
|
|| above.name !== 'air') return false
|
||||||
|
checked++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return checked >= 100
|
||||||
|
}, { label: 'At least 100 authored stair columns with empty space above', timeoutMs: 60000, intervalMs: 250 })
|
||||||
|
context.expect(checked >= 100, 'Client verified generated decorator columns', { checked })
|
||||||
|
context.report.decoratorColumns = checked
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+71
-1
@@ -4,6 +4,13 @@ import art.arcane.volmlib.nativelib.NativeAdapters;
|
|||||||
import art.arcane.iris.world.runtime.RuntimeInjection;
|
import art.arcane.iris.world.runtime.RuntimeInjection;
|
||||||
import art.arcane.iris.world.IrisCreator;
|
import art.arcane.iris.world.IrisCreator;
|
||||||
import art.arcane.iris.world.IrisToolbelt;
|
import art.arcane.iris.world.IrisToolbelt;
|
||||||
|
import art.arcane.iris.world.IrisWorldStorage;
|
||||||
|
import art.arcane.iris.world.WorldCreatorCompat;
|
||||||
|
import art.arcane.iris.world.lifecycle.LifecycleOperationCoordinator;
|
||||||
|
import art.arcane.iris.platform.bootstrap.ServerProperties;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.block.Biome;
|
import org.bukkit.block.Biome;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeTerrainAccess;
|
import art.arcane.volmlib.nativelib.terrain.NativeTerrainAccess;
|
||||||
@@ -19,6 +26,10 @@ import org.bukkit.command.CommandSender;
|
|||||||
import org.bukkit.generator.ChunkGenerator;
|
import org.bukkit.generator.ChunkGenerator;
|
||||||
import org.bukkit.plugin.java.JavaPlugin;
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.LinkOption;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
@@ -26,6 +37,7 @@ import java.util.logging.Level;
|
|||||||
public final class NativeLifecycleFixture extends JavaPlugin implements WorldRuntimeExecution {
|
public final class NativeLifecycleFixture extends JavaPlugin implements WorldRuntimeExecution {
|
||||||
private static final NamespacedKey WORLD_KEY = NamespacedKey.minecraft("native_lifecycle_probe");
|
private static final NamespacedKey WORLD_KEY = NamespacedKey.minecraft("native_lifecycle_probe");
|
||||||
private static final NamespacedKey TERRAIN_KEY = new NamespacedKey("iris", "native_terrain_probe");
|
private static final NamespacedKey TERRAIN_KEY = new NamespacedKey("iris", "native_terrain_probe");
|
||||||
|
private static final NamespacedKey COMMAND_KEY = new NamespacedKey("iris", "lifecycle_probe");
|
||||||
private static final long WORLD_SEED = 78264193L;
|
private static final long WORLD_SEED = 78264193L;
|
||||||
private NativeWorldRuntime runtime;
|
private NativeWorldRuntime runtime;
|
||||||
|
|
||||||
@@ -45,7 +57,7 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
|||||||
@Override
|
@Override
|
||||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
|
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
|
||||||
if (arguments.length != 1) {
|
if (arguments.length != 1) {
|
||||||
sender.sendMessage("Use /irislifecycle create|unload|status|structures|terrain-create|terrain-check|terrain-unload");
|
sender.sendMessage("Use /irislifecycle create|unload|status|structures|terrain-create|terrain-check|terrain-unload|lifecycle-status|lifecycle-entry");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -56,6 +68,8 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
|||||||
case "terrain-create" -> createTerrain(sender);
|
case "terrain-create" -> createTerrain(sender);
|
||||||
case "terrain-check" -> checkTerrain(sender);
|
case "terrain-check" -> checkTerrain(sender);
|
||||||
case "terrain-unload" -> unloadTerrain(sender);
|
case "terrain-unload" -> unloadTerrain(sender);
|
||||||
|
case "lifecycle-status" -> lifecycleStatus(sender);
|
||||||
|
case "lifecycle-entry" -> lifecycleEntry(sender);
|
||||||
case "status" -> sender.sendMessage("NATIVE_STATUS available=" + runtime.available()
|
case "status" -> sender.sendMessage("NATIVE_STATUS available=" + runtime.available()
|
||||||
+ " loaded=" + (Bukkit.getWorld(WORLD_KEY) != null));
|
+ " loaded=" + (Bukkit.getWorld(WORLD_KEY) != null));
|
||||||
default -> throw new IllegalArgumentException("Unknown lifecycle operation");
|
default -> throw new IllegalArgumentException("Unknown lifecycle operation");
|
||||||
@@ -66,6 +80,61 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void lifecycleStatus(CommandSender sender) throws Exception {
|
||||||
|
World world = Bukkit.getWorld(COMMAND_KEY);
|
||||||
|
Path storage = WorldCreatorCompat.persistentLevelRoot(COMMAND_KEY).toPath();
|
||||||
|
Path levelRoot = IrisWorldStorage.levelRoot().toPath();
|
||||||
|
Path registry = levelRoot.resolve("iris/worlds.json");
|
||||||
|
boolean registered = Files.exists(registry)
|
||||||
|
&& Files.readString(registry).contains("\"" + COMMAND_KEY + "\"");
|
||||||
|
String startupName = IrisWorldStorage.configuredWorldName(COMMAND_KEY, levelRoot.getFileName().toString());
|
||||||
|
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(ServerProperties.BUKKIT_YML);
|
||||||
|
boolean configured = configuration.getConfigurationSection("worlds") != null
|
||||||
|
&& configuration.getConfigurationSection("worlds").contains(startupName);
|
||||||
|
File quarantineRoot = levelRoot.resolve("dimensions/iris").toFile();
|
||||||
|
File[] quarantines = quarantineRoot.listFiles((directory, name) -> name.startsWith(".iris-delete-"));
|
||||||
|
int quarantineCount = quarantines == null ? 0 : quarantines.length;
|
||||||
|
Path queueFile = Bukkit.getPluginManager().getPlugin("Iris").getDataFolder().toPath()
|
||||||
|
.resolve("pending-world-deletes.txt");
|
||||||
|
List<String> queue = Files.exists(queueFile) ? Files.readAllLines(queueFile) : List.of();
|
||||||
|
boolean queued = quarantineCount > 0;
|
||||||
|
if (quarantines != null) {
|
||||||
|
for (File quarantine : quarantines) {
|
||||||
|
queued &= queue.contains(quarantine.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boolean disk = Files.exists(storage, LinkOption.NOFOLLOW_LINKS);
|
||||||
|
String cleanup = disk ? "present" : quarantineCount == 0 ? "deleted" : queued ? "queued" : "unsafe";
|
||||||
|
boolean idle = true;
|
||||||
|
LifecycleOperationCoordinator coordinator = LifecycleOperationCoordinator.get();
|
||||||
|
for (LifecycleOperationCoordinator.Domain domain : LifecycleOperationCoordinator.Domain.values()) {
|
||||||
|
idle &= coordinator.active(domain).isEmpty();
|
||||||
|
}
|
||||||
|
sender.sendMessage("IRIS_LIFECYCLE loaded=" + (world != null) + " registered=" + registered
|
||||||
|
+ " configured=" + configured + " disk=" + disk + " quarantines=" + quarantineCount
|
||||||
|
+ " cleanup=" + cleanup + " idle=" + idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void lifecycleEntry(CommandSender sender) {
|
||||||
|
if (!(sender instanceof Player player)) {
|
||||||
|
throw new IllegalArgumentException("Lifecycle entry requires a player");
|
||||||
|
}
|
||||||
|
World world = Bukkit.getWorld(COMMAND_KEY);
|
||||||
|
if (world == null || player.getWorld() != world || world.getSeed() != WORLD_SEED
|
||||||
|
|| !IrisToolbelt.isIrisWorld(world)) {
|
||||||
|
throw new IllegalStateException("Player did not enter the requested seeded Iris world");
|
||||||
|
}
|
||||||
|
Location feet = player.getLocation();
|
||||||
|
Material floor = world.getBlockAt(feet.getBlockX(), feet.getBlockY() - 1, feet.getBlockZ()).getType();
|
||||||
|
if (floor != Material.LIME_WOOL || !floor.isSolid()
|
||||||
|
|| !world.getBlockAt(feet.getBlockX(), feet.getBlockY(), feet.getBlockZ()).isPassable()
|
||||||
|
|| !world.getBlockAt(feet.getBlockX(), feet.getBlockY() + 1, feet.getBlockZ()).isPassable()) {
|
||||||
|
throw new IllegalStateException("Lifecycle player entry lacks a safe fixture floor");
|
||||||
|
}
|
||||||
|
sender.sendMessage("IRIS_LIFECYCLE_ENTRY world=" + COMMAND_KEY + " seed=" + world.getSeed()
|
||||||
|
+ " floor=lime_wool safe=true");
|
||||||
|
}
|
||||||
|
|
||||||
private void create(CommandSender sender) throws ReflectiveOperationException {
|
private void create(CommandSender sender) throws ReflectiveOperationException {
|
||||||
if (!runtime.available() || Bukkit.getWorld(WORLD_KEY) != null) {
|
if (!runtime.available() || Bukkit.getWorld(WORLD_KEY) != null) {
|
||||||
throw new IllegalStateException("Native runtime unavailable or probe world already loaded");
|
throw new IllegalStateException("Native runtime unavailable or probe world already loaded");
|
||||||
@@ -124,6 +193,7 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
|||||||
int y = world.getHighestBlockYAt(x, z);
|
int y = world.getHighestBlockYAt(x, z);
|
||||||
if (y < 90 || y > 100 || world.getBlockAt(x, y, z).getType() != Material.LIME_WOOL
|
if (y < 90 || y > 100 || world.getBlockAt(x, y, z).getType() != Material.LIME_WOOL
|
||||||
|| world.getBlockAt(x, y - 3, z).getType() != Material.GREEN_CONCRETE
|
|| world.getBlockAt(x, y - 3, z).getType() != Material.GREEN_CONCRETE
|
||||||
|
|| world.getBlockAt(x, world.getMinHeight(), z).getType() != Material.GREEN_CONCRETE
|
||||||
|| world.getBiome(x, y, z) != Biome.PLAINS) {
|
|| world.getBiome(x, y, z) != Biome.PLAINS) {
|
||||||
throw new IllegalStateException("Terrain column differs at " + x + "," + y + "," + z
|
throw new IllegalStateException("Terrain column differs at " + x + "," + y + "," + z
|
||||||
+ ": " + world.getBlockAt(x, y, z).getType());
|
+ ": " + world.getBlockAt(x, y, z).getType());
|
||||||
|
|||||||
@@ -11,6 +11,15 @@
|
|||||||
"block": "minecraft:lime_wool"
|
"block": "minecraft:lime_wool"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minHeight": 384,
|
||||||
|
"maxHeight": 384,
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:green_concrete"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"generators": [
|
"generators": [
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@
|
|||||||
"rockPalette": {
|
"rockPalette": {
|
||||||
"palette": [
|
"palette": [
|
||||||
{
|
{
|
||||||
"block": "minecraft:green_concrete"
|
"block": "minecraft:red_concrete"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+235
-6
@@ -6,6 +6,11 @@ import art.arcane.iris.generation.runtime.BiomeEnvironment;
|
|||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.iris.world.IrisCreator;
|
import art.arcane.iris.world.IrisCreator;
|
||||||
import art.arcane.iris.world.entity.IrisSpawner;
|
import art.arcane.iris.world.entity.IrisSpawner;
|
||||||
|
import art.arcane.iris.world.entity.IrisMarker;
|
||||||
|
import art.arcane.iris.world.entity.EntitySpawnSeed;
|
||||||
|
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||||
|
import art.arcane.iris.pack.value.IrisPosition;
|
||||||
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
import art.arcane.iris.world.entity.IrisEntitySpawn;
|
import art.arcane.iris.world.entity.IrisEntitySpawn;
|
||||||
import art.arcane.iris.world.entity.IrisEntity;
|
import art.arcane.iris.world.entity.IrisEntity;
|
||||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||||
@@ -28,6 +33,7 @@ import org.bukkit.entity.Entity;
|
|||||||
import org.bukkit.entity.EntityType;
|
import org.bukkit.entity.EntityType;
|
||||||
import org.bukkit.entity.LivingEntity;
|
import org.bukkit.entity.LivingEntity;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
import org.bukkit.event.EventHandler;
|
import org.bukkit.event.EventHandler;
|
||||||
import org.bukkit.event.EventPriority;
|
import org.bukkit.event.EventPriority;
|
||||||
import org.bukkit.event.Listener;
|
import org.bukkit.event.Listener;
|
||||||
@@ -38,9 +44,13 @@ import java.lang.reflect.Field;
|
|||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HexFormat;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
@@ -83,7 +93,7 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
|
|||||||
@Override
|
@Override
|
||||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
|
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
|
||||||
if (arguments.length != 1) {
|
if (arguments.length != 1) {
|
||||||
notifySender(sender, "Use /irisspawnqa setup|frogs|zombies|stress|check|surface|cave");
|
notifySender(sender, "Use /irisspawnqa setup|frogs|zombies|stress|check|seeded|completion|surface|cave");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -93,6 +103,8 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
|
|||||||
case "zombies" -> runRegion(sender, () -> spawnZombies(sender));
|
case "zombies" -> runRegion(sender, () -> spawnZombies(sender));
|
||||||
case "stress" -> runRegion(sender, () -> stress(sender));
|
case "stress" -> runRegion(sender, () -> stress(sender));
|
||||||
case "check" -> runRegion(sender, () -> check(sender));
|
case "check" -> runRegion(sender, () -> check(sender));
|
||||||
|
case "seeded" -> seeded(sender);
|
||||||
|
case "completion" -> completion(sender);
|
||||||
case "surface" -> view(sender, 67D);
|
case "surface" -> view(sender, 67D);
|
||||||
case "cave" -> view(sender, -46D);
|
case "cave" -> view(sender, -46D);
|
||||||
default -> throw new IllegalArgumentException("Unknown spawning operation");
|
default -> throw new IllegalArgumentException("Unknown spawning operation");
|
||||||
@@ -147,6 +159,9 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
|
|||||||
private void configureWorld(CommandSender sender, World world) {
|
private void configureWorld(CommandSender sender, World world) {
|
||||||
try {
|
try {
|
||||||
world.setDifficulty(Difficulty.NORMAL);
|
world.setDifficulty(Difficulty.NORMAL);
|
||||||
|
world.setTime(6000L);
|
||||||
|
world.setStorm(false);
|
||||||
|
world.setThundering(false);
|
||||||
world.setGameRule(GameRules.SPAWN_MOBS, true);
|
world.setGameRule(GameRules.SPAWN_MOBS, true);
|
||||||
List<CompletableFuture<Void>> chunks = new ArrayList<>();
|
List<CompletableFuture<Void>> chunks = new ArrayList<>();
|
||||||
for (int chunkX = -1; chunkX <= 1; chunkX++) {
|
for (int chunkX = -1; chunkX <= 1; chunkX++) {
|
||||||
@@ -176,6 +191,11 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
|
|||||||
Bukkit.getRegionScheduler().execute(this, world, chunkX, chunkZ, () -> {
|
Bukkit.getRegionScheduler().execute(this, world, chunkX, chunkZ, () -> {
|
||||||
try {
|
try {
|
||||||
chunk.addPluginChunkTicket(this);
|
chunk.addPluginChunkTicket(this);
|
||||||
|
for (Entity entity : chunk.getEntities()) {
|
||||||
|
if (entity instanceof LivingEntity && !(entity instanceof Player)) {
|
||||||
|
entity.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
retained.complete(null);
|
retained.complete(null);
|
||||||
} catch (Throwable error) {
|
} catch (Throwable error) {
|
||||||
retained.completeExceptionally(error);
|
retained.completeExceptionally(error);
|
||||||
@@ -244,12 +264,218 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void completion(CommandSender sender) {
|
||||||
|
World world = requireWorld();
|
||||||
|
Bukkit.getGlobalRegionScheduler().execute(this, () -> {
|
||||||
|
world.setGameRule(GameRules.SPAWN_MOBS, false);
|
||||||
|
runRegion(sender, () -> beginCompletion(sender, world));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void beginCompletion(CommandSender sender, World world) {
|
||||||
|
try {
|
||||||
|
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||||
|
Object manager = engine.getWorldManager();
|
||||||
|
Field field = manager.getClass().getDeclaredField("chunkMaintenance");
|
||||||
|
field.setAccessible(true);
|
||||||
|
Object maintenance = field.get(manager);
|
||||||
|
Method run = maintenance.getClass().getDeclaredMethod("runInitialSpawn", World.class, int.class, int.class, Runnable.class);
|
||||||
|
run.setAccessible(true);
|
||||||
|
Field pending = maintenance.getClass().getDeclaredField("markerFlagQueue");
|
||||||
|
pending.setAccessible(true);
|
||||||
|
AtomicInteger callbacks = new AtomicInteger();
|
||||||
|
Runnable callback = () -> {
|
||||||
|
require(Bukkit.isOwnedByCurrentRegion(world, 0, 0), "Initial callback ran outside its owning region");
|
||||||
|
callbacks.incrementAndGet();
|
||||||
|
Chunk chunk = world.getChunkAt(0, 0);
|
||||||
|
invokeAmbient(runtimeSpawn(chunk, "seeded"), chunk, true);
|
||||||
|
};
|
||||||
|
run.invoke(maintenance, world, 0, 0, callback);
|
||||||
|
awaitSkippedInitial(sender, world, new CompletionRun(engine, maintenance, run, pending, callback, callbacks), 0);
|
||||||
|
} catch (ReflectiveOperationException failure) {
|
||||||
|
throw new IllegalStateException("Could not start initial completion test", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void awaitSkippedInitial(CommandSender sender, World world, CompletionRun state, int attempt) {
|
||||||
|
Bukkit.getRegionScheduler().runDelayed(this, world, 0, 0, task -> {
|
||||||
|
try {
|
||||||
|
require(state.callbacks().get() == 0, "Disabled mob spawning consumed the initial pass");
|
||||||
|
if (!((Set<?>) state.pending().get(state.maintenance())).isEmpty()) {
|
||||||
|
require(attempt < 100, "Skipped initial pass did not release its pending state");
|
||||||
|
awaitSkippedInitial(sender, world, state, attempt + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Bukkit.getGlobalRegionScheduler().execute(this, () -> {
|
||||||
|
world.setGameRule(GameRules.SPAWN_MOBS, true);
|
||||||
|
runRegion(sender, () -> retryInitialCompletion(sender, world, state));
|
||||||
|
});
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
fail(sender, failure);
|
||||||
|
}
|
||||||
|
}, 2L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void retryInitialCompletion(CommandSender sender, World world, CompletionRun state) {
|
||||||
|
try {
|
||||||
|
state.run().invoke(state.maintenance(), world, 0, 0, state.callback());
|
||||||
|
state.run().invoke(state.maintenance(), world, 0, 0, state.callback());
|
||||||
|
awaitInitialCompletion(sender, world, state, 0);
|
||||||
|
} catch (ReflectiveOperationException failure) {
|
||||||
|
throw new IllegalStateException("Could not retry initial spawning", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void awaitInitialCompletion(CommandSender sender, World world, CompletionRun state, int attempt) {
|
||||||
|
Bukkit.getRegionScheduler().runDelayed(this, world, 0, 0, task -> {
|
||||||
|
try {
|
||||||
|
if (state.callbacks().get() == 0 || !((Set<?>) state.pending().get(state.maintenance())).isEmpty()) {
|
||||||
|
require(attempt < 100, "Initial spawning did not complete after retry");
|
||||||
|
awaitInitialCompletion(sender, world, state, attempt + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
require(state.callbacks().get() == 1, "Concurrent initial requests spawned duplicate populations");
|
||||||
|
state.run().invoke(state.maintenance(), world, 0, 0, state.callback());
|
||||||
|
Bukkit.getAsyncScheduler().runNow(this, ignored -> {
|
||||||
|
try {
|
||||||
|
require(state.engine().getMantle().getMantle().hasFlag(0, 0, MantleFlag.INITIAL_SPAWNED_MARKER), "Initial completion was not persisted");
|
||||||
|
require(state.callbacks().get() == 1, "Completed initial pass ran again");
|
||||||
|
evidence(sender, "SPAWN_QA_COMPLETION retry=true callbacks=1 persisted=true owned=true");
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
fail(sender, failure);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
fail(sender, failure);
|
||||||
|
}
|
||||||
|
}, 2L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void seeded(CommandSender sender) {
|
||||||
|
requireWorld();
|
||||||
|
List<List<String>> baseline = new ArrayList<>();
|
||||||
|
sampleInitial(0, false).thenCompose(first -> {
|
||||||
|
baseline.add(first);
|
||||||
|
return sampleInitial(1, false);
|
||||||
|
}).thenCompose(second -> {
|
||||||
|
baseline.add(second);
|
||||||
|
return sampleInitial(1, false);
|
||||||
|
}).thenCompose(repeated -> {
|
||||||
|
require(baseline.get(1).equals(repeated), "Second chunk changed after reversed generation order");
|
||||||
|
return sampleInitial(0, false);
|
||||||
|
}).thenCompose(repeated -> {
|
||||||
|
require(baseline.get(0).equals(repeated), "First chunk changed after reversed generation order");
|
||||||
|
return sampleInitial(0, true);
|
||||||
|
}).thenCompose(marker -> {
|
||||||
|
baseline.add(marker);
|
||||||
|
return sampleInitial(1, true);
|
||||||
|
}).thenCompose(marker -> {
|
||||||
|
baseline.add(marker);
|
||||||
|
return sampleInitial(1, true);
|
||||||
|
}).thenCompose(repeated -> {
|
||||||
|
require(baseline.get(3).equals(repeated), "Second marker changed after reversed generation order");
|
||||||
|
return sampleInitial(0, true);
|
||||||
|
}).whenComplete((repeated, failure) -> {
|
||||||
|
if (failure != null) {
|
||||||
|
fail(sender, failure);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
require(baseline.get(2).equals(repeated), "First marker changed after reversed generation order");
|
||||||
|
evidence(sender, "SPAWN_QA_SEEDED chunks=2 markers=2 repeats=2 equipment=true attributes=true");
|
||||||
|
} catch (Throwable error) {
|
||||||
|
fail(sender, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompletableFuture<List<String>> sampleInitial(int chunkX, boolean marker) {
|
||||||
|
CompletableFuture<List<String>> result = new CompletableFuture<>();
|
||||||
|
World world = requireWorld();
|
||||||
|
Bukkit.getRegionScheduler().execute(this, world, chunkX, 0, () -> {
|
||||||
|
try {
|
||||||
|
Chunk chunk = world.getChunkAt(chunkX, 0);
|
||||||
|
for (Entity entity : chunk.getEntities()) {
|
||||||
|
if (entity instanceof LivingEntity && !(entity instanceof Player)) {
|
||||||
|
entity.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int x = 0; x < 16; x++) {
|
||||||
|
for (int z = 0; z < 16; z++) {
|
||||||
|
chunk.getBlock(x, 64, z).setType(Material.GRASS_BLOCK, false);
|
||||||
|
for (int y = 65; y <= 68; y++) {
|
||||||
|
chunk.getBlock(x, y, z).setType(Material.AIR, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RuntimeSpawn spawn = runtimeSpawn(chunk, "seeded");
|
||||||
|
IrisSpawner definition = spawn.environment().data().getSpawnerLoader().load("qa/seeded");
|
||||||
|
require(!definition.canSpawn(spawn.engine(), chunkX, 0), "Ongoing weather/time gate unexpectedly allowed spawning");
|
||||||
|
for (int i = 0; i < 100 + chunkX; i++) {
|
||||||
|
RNG.r.nextLong();
|
||||||
|
}
|
||||||
|
ambientInvocation.set(true);
|
||||||
|
try {
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
LivingEntity witness = (LivingEntity) world.spawnEntity(new Location(world, (chunkX << 4) + 8.5, 65, 8.5), EntityType.ZOMBIE);
|
||||||
|
witness.setCustomName("Population witness");
|
||||||
|
witness.setAI(false);
|
||||||
|
witness.setGravity(false);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
ambientInvocation.remove();
|
||||||
|
}
|
||||||
|
if (marker) {
|
||||||
|
invokeInitialMarker(spawn, definition, new IrisPosition((chunkX << 4) + 5, 64, 5));
|
||||||
|
} else {
|
||||||
|
invokeAmbient(spawn, chunk, true);
|
||||||
|
}
|
||||||
|
List<String> snapshot = new ArrayList<>();
|
||||||
|
for (Entity entity : chunk.getEntities()) {
|
||||||
|
String name = entity.getCustomName();
|
||||||
|
if (!(entity instanceof LivingEntity living) || name == null || !name.startsWith("Seeded ")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ItemStack hand = living.getEquipment().getItemInMainHand();
|
||||||
|
ItemStack helmet = living.getEquipment().getHelmet();
|
||||||
|
require(hand.hasItemMeta() && hand.getItemMeta().hasAttributeModifiers(), "Spawned gear has no authored attributes");
|
||||||
|
require(helmet != null && helmet.getType() == Material.IRON_HELMET, "Spawned helmet was not authored equipment");
|
||||||
|
Location at = entity.getLocation();
|
||||||
|
snapshot.add(entity.getType() + ":" + at.getX() + ":" + at.getY() + ":" + at.getZ()
|
||||||
|
+ ":" + HexFormat.of().formatHex(hand.serializeAsBytes())
|
||||||
|
+ ":" + HexFormat.of().formatHex(helmet.serializeAsBytes()));
|
||||||
|
}
|
||||||
|
require(snapshot.size() >= 2 && snapshot.size() <= 4, "Initial population was blocked by runtime gates or cap: " + snapshot.size());
|
||||||
|
Collections.sort(snapshot);
|
||||||
|
result.complete(List.copyOf(snapshot));
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
result.completeExceptionally(failure);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void invokeInitialMarker(RuntimeSpawn spawn, IrisSpawner definition, IrisPosition position) {
|
||||||
|
ambientInvocation.set(true);
|
||||||
|
try (BiomeEnvironment.Scope ignored = spawn.engine().openBiomeEnvironmentScope(spawn.environment())) {
|
||||||
|
Method method = spawn.spawner().getClass().getDeclaredMethod("spawnMarker", IrisPosition.class, IrisSpawner.class, IrisMarker.class, RNG.class, boolean.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
method.invoke(spawn.spawner(), position, definition, null, EntitySpawnSeed.marker(spawn.engine().getSeedManager().getEntity(), position.getX(), position.getY(), position.getZ()), true);
|
||||||
|
} catch (InvocationTargetException failure) {
|
||||||
|
throw new IllegalStateException("Initial marker spawning failed", failure.getCause());
|
||||||
|
} catch (ReflectiveOperationException failure) {
|
||||||
|
throw new IllegalStateException("Could not invoke initial marker spawning", failure);
|
||||||
|
} finally {
|
||||||
|
ambientInvocation.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void spawnFrogs(CommandSender sender) {
|
private void spawnFrogs(CommandSender sender) {
|
||||||
Chunk chunk = requireWorld().getChunkAt(0, 0);
|
Chunk chunk = requireWorld().getChunkAt(0, 0);
|
||||||
RuntimeSpawn spawn = runtimeSpawn(chunk, "frogs");
|
RuntimeSpawn spawn = runtimeSpawn(chunk, "frogs");
|
||||||
getLogger().info(spawnDiagnostics(spawn, chunk));
|
getLogger().info(spawnDiagnostics(spawn, chunk));
|
||||||
for (int attempt = 0; attempt < 12; attempt++) {
|
for (int attempt = 0; attempt < 12; attempt++) {
|
||||||
invokeAmbient(spawn, chunk);
|
invokeAmbient(spawn, chunk, false);
|
||||||
require(count(chunk, EntityType.FROG) <= FROG_CAPACITY, "Frog batch exceeded its category capacity");
|
require(count(chunk, EntityType.FROG) <= FROG_CAPACITY, "Frog batch exceeded its category capacity");
|
||||||
}
|
}
|
||||||
require(count(chunk, EntityType.FROG) == FROG_CAPACITY,
|
require(count(chunk, EntityType.FROG) == FROG_CAPACITY,
|
||||||
@@ -266,7 +492,7 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
|
|||||||
require(count(chunk, EntityType.ZOMBIE) == 0, "Cave phase already ran");
|
require(count(chunk, EntityType.ZOMBIE) == 0, "Cave phase already ran");
|
||||||
RuntimeSpawn spawn = runtimeSpawn(chunk, "cave-zombies");
|
RuntimeSpawn spawn = runtimeSpawn(chunk, "cave-zombies");
|
||||||
getLogger().info(spawnDiagnostics(spawn, chunk));
|
getLogger().info(spawnDiagnostics(spawn, chunk));
|
||||||
invokeAmbient(spawn, chunk);
|
invokeAmbient(spawn, chunk, false);
|
||||||
firstZombieBatch = count(chunk, EntityType.ZOMBIE);
|
firstZombieBatch = count(chunk, EntityType.ZOMBIE);
|
||||||
require(firstZombieBatch == ZOMBIE_CAPACITY,
|
require(firstZombieBatch == ZOMBIE_CAPACITY,
|
||||||
"Authored seven-zombie batch was not clamped to capacity two: " + spawnDiagnostics(spawn, chunk));
|
"Authored seven-zombie batch was not clamped to capacity two: " + spawnDiagnostics(spawn, chunk));
|
||||||
@@ -280,7 +506,7 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
|
|||||||
Chunk chunk = requireWorld().getChunkAt(0, 0);
|
Chunk chunk = requireWorld().getChunkAt(0, 0);
|
||||||
RuntimeSpawn spawn = runtimeSpawn(chunk, "cave-zombies");
|
RuntimeSpawn spawn = runtimeSpawn(chunk, "cave-zombies");
|
||||||
for (int attempt = 0; attempt < 50; attempt++) {
|
for (int attempt = 0; attempt < 50; attempt++) {
|
||||||
invokeAmbient(spawn, chunk);
|
invokeAmbient(spawn, chunk, false);
|
||||||
require(count(chunk, EntityType.ZOMBIE) == ZOMBIE_CAPACITY, "Saturated zombie category exceeded capacity");
|
require(count(chunk, EntityType.ZOMBIE) == ZOMBIE_CAPACITY, "Saturated zombie category exceeded capacity");
|
||||||
}
|
}
|
||||||
evidence(sender, "SPAWN_QA_STRESS attempts=50 zombies=" + count(chunk, EntityType.ZOMBIE));
|
evidence(sender, "SPAWN_QA_STRESS attempts=50 zombies=" + count(chunk, EntityType.ZOMBIE));
|
||||||
@@ -371,10 +597,10 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
|
|||||||
return diagnostics.toString();
|
return diagnostics.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void invokeAmbient(RuntimeSpawn spawn, Chunk chunk) {
|
private void invokeAmbient(RuntimeSpawn spawn, Chunk chunk, boolean initial) {
|
||||||
ambientInvocation.set(true);
|
ambientInvocation.set(true);
|
||||||
try (BiomeEnvironment.Scope ignored = spawn.engine().openBiomeEnvironmentScope(spawn.environment())) {
|
try (BiomeEnvironment.Scope ignored = spawn.engine().openBiomeEnvironmentScope(spawn.environment())) {
|
||||||
spawn.method().invoke(spawn.spawner(), chunk, false, spawn.environment());
|
spawn.method().invoke(spawn.spawner(), chunk, initial, spawn.environment());
|
||||||
} catch (InvocationTargetException failure) {
|
} catch (InvocationTargetException failure) {
|
||||||
throw new IllegalStateException("Iris ambient spawning failed", failure.getCause());
|
throw new IllegalStateException("Iris ambient spawning failed", failure.getCause());
|
||||||
} catch (ReflectiveOperationException failure) {
|
} catch (ReflectiveOperationException failure) {
|
||||||
@@ -467,6 +693,9 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record CompletionRun(Engine engine, Object maintenance, Method run, Field pending, Runnable callback, AtomicInteger callbacks) {
|
||||||
|
}
|
||||||
|
|
||||||
private record RuntimeSpawn(Engine engine, Object spawner, Method method, BiomeEnvironment environment) {
|
private record RuntimeSpawn(Engine engine, Object spawner, Method method, BiomeEnvironment environment) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "Seeded population",
|
||||||
|
"derivative": "minecraft:plains",
|
||||||
|
"vanillaDerivative": "minecraft:plains",
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"minHeight": 1,
|
||||||
|
"maxHeight": 1,
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:grass_block"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"generators": [
|
||||||
|
{
|
||||||
|
"generator": "flat",
|
||||||
|
"min": 64,
|
||||||
|
"max": 64
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"entitySpawners": [
|
||||||
|
"qa/seeded"
|
||||||
|
]
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"type": "minecraft:husk",
|
||||||
|
"reason": "NATURAL",
|
||||||
|
"surface": "LAND",
|
||||||
|
"ai": false,
|
||||||
|
"aware": false,
|
||||||
|
"gravity": false,
|
||||||
|
"customName": "Seeded husk",
|
||||||
|
"invulnerable": true,
|
||||||
|
"helmet": {
|
||||||
|
"type": "IRON_HELMET",
|
||||||
|
"minDurability": 0.1,
|
||||||
|
"maxDurability": 0.9
|
||||||
|
},
|
||||||
|
"mainHand": {
|
||||||
|
"type": "IRON_SWORD",
|
||||||
|
"minDurability": 0.1,
|
||||||
|
"maxDurability": 0.9,
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"name": "seeded_damage",
|
||||||
|
"attribute": "minecraft:attack_damage",
|
||||||
|
"minAmount": 1.25,
|
||||||
|
"maxAmount": 6.75,
|
||||||
|
"chance": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"type": "minecraft:zombie",
|
||||||
|
"reason": "NATURAL",
|
||||||
|
"surface": "LAND",
|
||||||
|
"ai": false,
|
||||||
|
"aware": false,
|
||||||
|
"gravity": false,
|
||||||
|
"customName": "Seeded zombie",
|
||||||
|
"invulnerable": true,
|
||||||
|
"helmet": {
|
||||||
|
"type": "IRON_HELMET",
|
||||||
|
"minDurability": 0.1,
|
||||||
|
"maxDurability": 0.9
|
||||||
|
},
|
||||||
|
"mainHand": {
|
||||||
|
"type": "IRON_SWORD",
|
||||||
|
"minDurability": 0.1,
|
||||||
|
"maxDurability": 0.9,
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"name": "seeded_damage",
|
||||||
|
"attribute": "minecraft:attack_damage",
|
||||||
|
"minAmount": 1.25,
|
||||||
|
"maxAmount": 6.75,
|
||||||
|
"chance": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"group": "NORMAL",
|
||||||
|
"maxEntitiesPerChunk": 4,
|
||||||
|
"allowedLightLevels": {
|
||||||
|
"min": 0,
|
||||||
|
"max": 0
|
||||||
|
},
|
||||||
|
"weather": "DOWNFALL",
|
||||||
|
"timeBlock": {
|
||||||
|
"startHour": 20,
|
||||||
|
"endHour": 3
|
||||||
|
},
|
||||||
|
"maximumRate": {
|
||||||
|
"amount": 1,
|
||||||
|
"per": {
|
||||||
|
"days": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"initialSpawns": [
|
||||||
|
{
|
||||||
|
"entity": "qa/seeded-zombie",
|
||||||
|
"rarity": 1,
|
||||||
|
"minSpawns": 2,
|
||||||
|
"maxSpawns": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"entity": "qa/seeded-husk",
|
||||||
|
"rarity": 2,
|
||||||
|
"minSpawns": 2,
|
||||||
|
"maxSpawns": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -8,7 +8,8 @@ export default {
|
|||||||
context.expect(response.includes(expected) && !response.includes('SPAWN_QA_FAILED'), response)
|
context.expect(response.includes(expected) && !response.includes('SPAWN_QA_FAILED'), response)
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
const entities = name => Object.values(context.bot.entities).filter(entity => entity.name === name)
|
const entities = name => Object.values(context.bot.entities).filter(entity => entity.name === name
|
||||||
|
&& entity.position.x >= 0 && entity.position.x < 16 && entity.position.z >= 0 && entity.position.z < 16)
|
||||||
await context.step('prepare the synthetic Iris world and dark cave', async () => {
|
await context.step('prepare the synthetic Iris world and dark cave', async () => {
|
||||||
await command('setup', 'SPAWN_QA_SETUP world=iris:spawn_qa seed=78264193', 180000)
|
await command('setup', 'SPAWN_QA_SETUP world=iris:spawn_qa seed=78264193', 180000)
|
||||||
await wait(() => Math.abs(context.bot.entity.position.y - 67) < 1, 'Fixture world teleport')
|
await wait(() => Math.abs(context.bot.entity.position.y - 67) < 1, 'Fixture world teleport')
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "Flat",
|
||||||
|
"derivative": "minecraft:plains",
|
||||||
|
"vanillaDerivative": "minecraft:plains",
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"minHeight": 1,
|
||||||
|
"maxHeight": 1,
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:lime_wool"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minHeight": 384,
|
||||||
|
"maxHeight": 384,
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:green_concrete"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"generators": [
|
||||||
|
{
|
||||||
|
"generator": "flat",
|
||||||
|
"min": 64,
|
||||||
|
"max": 96
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "Noise Sources",
|
||||||
|
"version": 1,
|
||||||
|
"mode": {
|
||||||
|
"type": "OVERWORLD"
|
||||||
|
},
|
||||||
|
"regions": [
|
||||||
|
"flat"
|
||||||
|
],
|
||||||
|
"focusRegion": "flat",
|
||||||
|
"focus": "flat",
|
||||||
|
"landChance": 1,
|
||||||
|
"environment": "NORMAL",
|
||||||
|
"dimensionHeight": {
|
||||||
|
"min": -64,
|
||||||
|
"max": 320
|
||||||
|
},
|
||||||
|
"logicalHeight": 384,
|
||||||
|
"fluidHeight": 0,
|
||||||
|
"bedrock": false,
|
||||||
|
"rockPalette": {
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:red_concrete"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"expression": "0.5 + sin(x * 0.15) * 0.25 + cos(y * 0.13) * 0.25"
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"interpolator": {
|
||||||
|
"function": "NONE",
|
||||||
|
"horizontalScale": 1
|
||||||
|
},
|
||||||
|
"seed": 310,
|
||||||
|
"composite": [
|
||||||
|
{
|
||||||
|
"seed": 310,
|
||||||
|
"style": {
|
||||||
|
"expression": "terrain"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"seed": 311,
|
||||||
|
"style": {
|
||||||
|
"style": "CLOVER_HERMITE",
|
||||||
|
"zoom": 0.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "Flat",
|
||||||
|
"landBiomes": [
|
||||||
|
"flat"
|
||||||
|
],
|
||||||
|
"seaBiomes": [
|
||||||
|
"flat"
|
||||||
|
],
|
||||||
|
"shoreBiomes": [
|
||||||
|
"flat"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { readFile } from 'node:fs/promises'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'iris-noise',
|
||||||
|
description: 'Verify expression and interpolated noise produce varied, bounded terrain visible to a player.',
|
||||||
|
async run(context) {
|
||||||
|
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
|
||||||
|
context.expect(/^isolated=true\s*$/m.test(source), 'Noise generation requires an isolated instance')
|
||||||
|
await context.step('create a world with expression and Clover noise', async () => {
|
||||||
|
const response = await context.command('/iris create name=noise_probe type=noise-fixture seed=78264193',
|
||||||
|
/Successfully created your world|Failed to create/, 180000)
|
||||||
|
context.expect(response.includes('Successfully created your world'), response)
|
||||||
|
})
|
||||||
|
await context.step('client observes bounded terrain with varying heights', async () => {
|
||||||
|
let columns = 0
|
||||||
|
let minimum = Infinity
|
||||||
|
let maximum = -Infinity
|
||||||
|
await context.waitUntil(() => {
|
||||||
|
columns = 0
|
||||||
|
minimum = Infinity
|
||||||
|
maximum = -Infinity
|
||||||
|
const center = context.bot.entity.position.floored()
|
||||||
|
for (let x = -8; x <= 8; x++) {
|
||||||
|
for (let z = -8; z <= 8; z++) {
|
||||||
|
let surface = null
|
||||||
|
for (let y = 60; y <= 100; y++) {
|
||||||
|
const position = center.offset(x, y - center.y, z)
|
||||||
|
const block = context.bot.blockAt(position)
|
||||||
|
if (!block) return false
|
||||||
|
if (block.name === 'lime_wool') {
|
||||||
|
if (surface !== null || context.bot.blockAt(position.offset(0, 1, 0))?.name !== 'air') return false
|
||||||
|
surface = y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (surface === null || surface < 64 || surface > 96) return false
|
||||||
|
columns++
|
||||||
|
minimum = Math.min(minimum, surface)
|
||||||
|
maximum = Math.max(maximum, surface)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return columns === 289 && maximum - minimum >= 3
|
||||||
|
}, { label: '289 noise columns with bounded, varied surfaces', timeoutMs: 60000, intervalMs: 250 })
|
||||||
|
context.expect(columns === 289 && maximum - minimum >= 3, 'Client verified noise terrain', { columns, minimum, maximum })
|
||||||
|
context.report.noiseTerrain = { columns, minimum, maximum }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
export default {
|
||||||
|
name: 'iris-seeded-entities',
|
||||||
|
description: 'Replay initial chunk and marker populations in reversed order, comparing positions and complete equipment data.',
|
||||||
|
async run(context) {
|
||||||
|
const command = async (operation, expected, timeoutMs) => {
|
||||||
|
const response = await context.command(`/irisspawnqa ${operation}`, /SPAWN_QA_[A-Z]+(?:\s|$)/, timeoutMs)
|
||||||
|
context.expect(response.includes(expected) && !response.includes('SPAWN_QA_FAILED'), response)
|
||||||
|
}
|
||||||
|
await context.step('prepare a controlled Iris world', async () => {
|
||||||
|
await command('setup', 'SPAWN_QA_SETUP', 180000)
|
||||||
|
})
|
||||||
|
await context.step('replay initial mobs despite unrelated randomness and live spawn restrictions', async () => {
|
||||||
|
await command('seeded', 'SPAWN_QA_SEEDED chunks=2 markers=2 repeats=2 equipment=true attributes=true', 60000)
|
||||||
|
await context.waitUntil(() => Object.values(context.bot.entities).some(entity => entity.name === 'zombie' || entity.name === 'husk'), {
|
||||||
|
label: 'Generated mobs reached the client', timeoutMs: 10000, intervalMs: 100
|
||||||
|
})
|
||||||
|
context.report.seededEntities = { chunks: 2, markers: 2, repeats: 2, positions: true, equipmentBytes: true, attributeIds: true }
|
||||||
|
})
|
||||||
|
await context.step('retry a skipped initial pass and persist completion once', async () => {
|
||||||
|
await command('completion', 'SPAWN_QA_COMPLETION retry=true callbacks=1 persisted=true owned=true', 30000)
|
||||||
|
context.report.seededEntities.completionRetry = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "Flat",
|
||||||
|
"derivative": "minecraft:plains",
|
||||||
|
"vanillaDerivative": "minecraft:plains",
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"minHeight": 1,
|
||||||
|
"maxHeight": 1,
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:lime_wool"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minHeight": 384,
|
||||||
|
"maxHeight": 384,
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:stone"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"generators": [
|
||||||
|
{
|
||||||
|
"generator": "flat",
|
||||||
|
"min": 64,
|
||||||
|
"max": 64
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"objects": [
|
||||||
|
{
|
||||||
|
"place": [
|
||||||
|
"object"
|
||||||
|
],
|
||||||
|
"chance": 1,
|
||||||
|
"density": 4,
|
||||||
|
"mode": "CENTER_HEIGHT",
|
||||||
|
"rotation": {
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"name": "Structure Fixture",
|
||||||
|
"version": 1,
|
||||||
|
"mode": {
|
||||||
|
"type": "OVERWORLD"
|
||||||
|
},
|
||||||
|
"regions": [
|
||||||
|
"flat"
|
||||||
|
],
|
||||||
|
"focusRegion": "flat",
|
||||||
|
"focus": "flat",
|
||||||
|
"landChance": 1,
|
||||||
|
"environment": "NORMAL",
|
||||||
|
"dimensionHeight": {
|
||||||
|
"min": -64,
|
||||||
|
"max": 320
|
||||||
|
},
|
||||||
|
"logicalHeight": 384,
|
||||||
|
"fluidHeight": 0,
|
||||||
|
"bedrock": false,
|
||||||
|
"rockPalette": {
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:stone"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"structures": [
|
||||||
|
{
|
||||||
|
"structures": [
|
||||||
|
"surface"
|
||||||
|
],
|
||||||
|
"placementId": "surface",
|
||||||
|
"distribution": "DENSITY",
|
||||||
|
"density": 1,
|
||||||
|
"anchor": "SURFACE",
|
||||||
|
"terrain": {
|
||||||
|
"mode": "PRESERVE"
|
||||||
|
},
|
||||||
|
"stilt": {
|
||||||
|
"maxDepth": 16,
|
||||||
|
"palette": {
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:diamond_block"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"structures": [
|
||||||
|
"buried"
|
||||||
|
],
|
||||||
|
"placementId": "buried",
|
||||||
|
"distribution": "DENSITY",
|
||||||
|
"density": 1,
|
||||||
|
"anchor": "HEIGHT_BAND",
|
||||||
|
"minHeight": 24,
|
||||||
|
"maxHeight": 24,
|
||||||
|
"terrain": {
|
||||||
|
"mode": "BORE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"interpolator": {
|
||||||
|
"function": "NONE",
|
||||||
|
"horizontalScale": 1
|
||||||
|
},
|
||||||
|
"seed": 310,
|
||||||
|
"composite": [
|
||||||
|
{
|
||||||
|
"seed": 310,
|
||||||
|
"style": {
|
||||||
|
"style": "FLAT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"object": "buried",
|
||||||
|
"connectors": [],
|
||||||
|
"rotatable": false,
|
||||||
|
"rules": {
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"object": "surface",
|
||||||
|
"connectors": [],
|
||||||
|
"rotatable": false,
|
||||||
|
"rules": {
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"pieces": [
|
||||||
|
{
|
||||||
|
"piece": "buried",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"pieces": [
|
||||||
|
{
|
||||||
|
"piece": "surface",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "Flat",
|
||||||
|
"landBiomes": [
|
||||||
|
"flat"
|
||||||
|
],
|
||||||
|
"seaBiomes": [
|
||||||
|
"flat"
|
||||||
|
],
|
||||||
|
"shoreBiomes": [
|
||||||
|
"flat"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"startPool": "buried",
|
||||||
|
"maxDepth": 1,
|
||||||
|
"maxSizeChunks": 1,
|
||||||
|
"placeMode": "STRUCTURE_PIECE"
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"startPool": "surface",
|
||||||
|
"maxDepth": 1,
|
||||||
|
"maxSizeChunks": 1,
|
||||||
|
"placeMode": "STRUCTURE_PIECE"
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { readFile } from 'node:fs/promises'
|
||||||
|
import { createHash } from 'node:crypto'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'iris-structures',
|
||||||
|
description: 'Verify surface foundations, buried rooms, and waterlogged object blocks in generated chunks.',
|
||||||
|
async run(context) {
|
||||||
|
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
|
||||||
|
context.expect(/^isolated=true\s*$/m.test(source), 'Structure generation requires an isolated instance')
|
||||||
|
const world = context.options.command ?? 'structure_probe'
|
||||||
|
context.expect(/^[a-z0-9_]+$/.test(world), 'Fixture world name must be a simple identifier')
|
||||||
|
await context.step('create the structure fixture world', async () => {
|
||||||
|
const response = await context.command(`/iris create name=${world} type=structure-fixture seed=78264193`,
|
||||||
|
/World ready|Failed to create/, 180000)
|
||||||
|
context.expect(response.includes('World ready'), response)
|
||||||
|
})
|
||||||
|
await context.step('observe structures and objects above and below terrain', async () => {
|
||||||
|
await context.waitUntil(() => {
|
||||||
|
if (Math.abs(context.bot.entity.position.x) > 32 || Math.abs(context.bot.entity.position.z) > 32) return false
|
||||||
|
for (let x = -16; x < 32; x += 16) {
|
||||||
|
for (let z = -16; z < 32; z += 16) {
|
||||||
|
for (let y = 16; y <= 80; y += 16) {
|
||||||
|
if (!context.bot.blockAt(context.bot.entity.position.clone().set(x, y, z))) return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}, { label: 'Complete structure fixture chunks', timeoutMs: 60000, intervalMs: 100 })
|
||||||
|
const digest = createHash('sha256')
|
||||||
|
const counts = { emerald_block: 0, diamond_block: 0, blue_concrete: 0, gold_block: 0, oak_slab: 0 }
|
||||||
|
for (let x = -16; x < 32; x++) {
|
||||||
|
for (let z = -16; z < 32; z++) {
|
||||||
|
for (let y = 16; y <= 80; y++) {
|
||||||
|
const block = context.bot.blockAt(context.bot.entity.position.clone().set(x, y, z))
|
||||||
|
context.expect(block !== null, 'Every sampled structure block must be loaded')
|
||||||
|
digest.update(`${block.stateId}\n`)
|
||||||
|
if (Object.hasOwn(counts, block.name)) counts[block.name]++
|
||||||
|
if (block.name === 'emerald_block') context.expect(y > 64, 'Surface platform must be above terrain')
|
||||||
|
if (block.name === 'blue_concrete') context.expect(y < 40, 'Buried room must remain underground')
|
||||||
|
if (block.name === 'oak_slab') context.expect(block.getProperties().waterlogged === false,
|
||||||
|
'Object slabs placed on dry ground must clear waterlogging')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [block, count] of Object.entries(counts)) {
|
||||||
|
context.expect(count > 0, `Generated fixture must contain ${block}`, { count })
|
||||||
|
}
|
||||||
|
context.report.structures = { counts, hash: digest.digest('hex') }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { readFile, writeFile } from 'node:fs/promises'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { setup, openStudio, closeStudio, sampleTerrain, readJson, writeJson,
|
||||||
|
logPosition, logSince } from './studio-support.mjs'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'iris-studio-hotload-lifecycle',
|
||||||
|
description: 'Generate cold chunks during Studio edits and close after saving an edit.',
|
||||||
|
async run(context) {
|
||||||
|
const fixture = await setup(context)
|
||||||
|
const biomeFile = path.join(fixture.pack, 'biomes', 'flat.json')
|
||||||
|
const original = await readFile(biomeFile, 'utf8')
|
||||||
|
const logOffset = await logPosition(fixture)
|
||||||
|
let active
|
||||||
|
let x = 16384
|
||||||
|
try {
|
||||||
|
active = await openStudio(context, fixture)
|
||||||
|
await sampleTerrain(context, fixture, 0, fixture.state.material)
|
||||||
|
for (let cycle = 0; cycle < 3; cycle++) {
|
||||||
|
await context.step(`Load cold chunks during hotload ${cycle + 1}`, async () => {
|
||||||
|
const manifestFile = fixture.manifestPath(active.key)
|
||||||
|
const before = await readJson(manifestFile)
|
||||||
|
const biome = await readJson(biomeFile)
|
||||||
|
const material = cycle % 2 === 0 ? 'gold_block' : 'diamond_block'
|
||||||
|
biome.name = `Lifecycle terrain ${cycle + 1}`
|
||||||
|
biome.layers[0].palette[0].block = `minecraft:${material}`
|
||||||
|
const startedAt = Date.now()
|
||||||
|
await writeJson(biomeFile, biome)
|
||||||
|
x += 512
|
||||||
|
await context.command(`/minecraft:tp @s ${x + 0.5} 101 0.5`, /Teleported/i, 15000)
|
||||||
|
const after = await fixture.wait(async () => {
|
||||||
|
const manifest = await readJson(manifestFile)
|
||||||
|
return manifest.activeActivationId !== before.activeActivationId ? manifest : false
|
||||||
|
}, 'Hotload did not complete while the player requested fresh chunks', 90000)
|
||||||
|
const activationDurationMs = Date.now() - startedAt
|
||||||
|
await fixture.wait(() => Math.abs(context.bot.entity.position.x - x - 0.5) < 1,
|
||||||
|
'Cold chunk teleport did not arrive after hotload', 30000)
|
||||||
|
context.report.iris.activations.push({ before: before.activeActivationId,
|
||||||
|
after: after.activeActivationId, material, coldChunkRequests: 1, activationDurationMs })
|
||||||
|
x += 2048
|
||||||
|
await sampleTerrain(context, fixture, x, material)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await context.step('Close Studio while a saved edit is activating', async () => {
|
||||||
|
const biome = await readJson(biomeFile)
|
||||||
|
biome.name = 'Lifecycle close terrain'
|
||||||
|
await writeJson(biomeFile, biome)
|
||||||
|
const pending = await fixture.wait(async () => {
|
||||||
|
const manifest = await readJson(fixture.manifestPath(active.key))
|
||||||
|
return manifest.pendingActivationId !== null ? manifest : false
|
||||||
|
}, 'The saved close edit did not enter activation', 90000)
|
||||||
|
context.report.iris.closePendingActivationId = pending.pendingActivationId
|
||||||
|
await closeStudio(context, fixture, active)
|
||||||
|
active = undefined
|
||||||
|
await context.command('/minecraft:seed', /Seed:/i, 10000)
|
||||||
|
})
|
||||||
|
await context.step('Reopen the edited pack and retain usable generation', async () => {
|
||||||
|
active = await openStudio(context, fixture)
|
||||||
|
await sampleTerrain(context, fixture, 0, 'gold_block')
|
||||||
|
await closeStudio(context, fixture, active)
|
||||||
|
active = undefined
|
||||||
|
})
|
||||||
|
const log = await logSince(fixture, logOffset)
|
||||||
|
context.expect(!/Studio generation update failed|Unable to (capture terrain|save the terrain boundary)|ClosedChannelException|Chunk system error|server has stopped responding|could not acquire its engine runtime|Failed to list files/i.test(log),
|
||||||
|
'Studio lifecycle emitted a generation, snapshot, storage, or watchdog failure')
|
||||||
|
} finally {
|
||||||
|
if (active) await closeStudio(context, fixture, active)
|
||||||
|
await writeFile(biomeFile, original)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { readFile, stat } from 'node:fs/promises'
|
||||||
|
import { createHash } from 'node:crypto'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'iris-studio-open',
|
||||||
|
description: 'Measure repeated Studio entry and verify visible terrain, spectator mode, and world cleanup.',
|
||||||
|
async run(context) {
|
||||||
|
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
|
||||||
|
context.expect(/^isolated=true\s*$/m.test(source), 'Studio entry requires an isolated instance')
|
||||||
|
const match = /^([a-z0-9_-]+):([1-4])$/.exec(context.options.command ?? 'overworld:2')
|
||||||
|
context.expect(match, 'Expected pack:cycles with one to four cycles')
|
||||||
|
context.report.studioOpen = []
|
||||||
|
for (let cycle = 0; cycle < Number(match[2]); cycle++) {
|
||||||
|
await context.step(`open and close Studio ${cycle + 1}`, async () => {
|
||||||
|
const before = new Set((await context.observe()).worlds.map(world => world.id))
|
||||||
|
const start = Date.now()
|
||||||
|
let world
|
||||||
|
let center
|
||||||
|
await context.command(`/iris studio open ${match[1]} seed=1337`)
|
||||||
|
await context.waitUntil(async () => {
|
||||||
|
const observation = await context.observe()
|
||||||
|
const player = observation.players.find(entry => entry.username === context.bot.username)
|
||||||
|
world = observation.worlds.find(entry => entry.id === player?.world)
|
||||||
|
if (!world || before.has(world.id) || !/iris-[0-9a-f-]{36}$/.test(world.name)) return false
|
||||||
|
center = context.bot.entity.position.floored()
|
||||||
|
return world.loadedChunks > 0 && context.bot.blockAt(center) !== null && context.bot.game.gameMode === 'spectator'
|
||||||
|
}, { label: 'Studio arrival with terrain and spectator mode', timeoutMs: 180000, intervalMs: 100 })
|
||||||
|
const arrivalMs = Date.now() - start
|
||||||
|
context.expect(Number.isInteger(context.bot.game.minY) && Number.isInteger(context.bot.game.height),
|
||||||
|
'Studio dimension must report its vertical range')
|
||||||
|
await context.waitUntil(() => {
|
||||||
|
for (let x = -1; x <= 1; x++) {
|
||||||
|
for (let z = -1; z <= 1; z++) {
|
||||||
|
for (let y = context.bot.game.minY; y < context.bot.game.minY + context.bot.game.height; y += 16) {
|
||||||
|
if (context.bot.blockAt(center.offset(x, y - center.y, z)) === null) return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}, { label: 'Complete neighboring Studio columns', timeoutMs: 60000, intervalMs: 100 })
|
||||||
|
const digest = createHash('sha256')
|
||||||
|
let solid = 0
|
||||||
|
for (let x = -1; x <= 1; x++) {
|
||||||
|
for (let z = -1; z <= 1; z++) {
|
||||||
|
for (let y = context.bot.game.minY; y < context.bot.game.minY + context.bot.game.height; y++) {
|
||||||
|
const block = context.bot.blockAt(center.offset(x, y - center.y, z))
|
||||||
|
context.expect(block !== null, 'Studio entry column must be available')
|
||||||
|
digest.update(`${block.name}\n`)
|
||||||
|
if (!['air', 'cave_air', 'void_air', 'water', 'lava'].includes(block.name)) solid++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.expect(solid > 0, 'Studio entry must contain solid terrain')
|
||||||
|
context.report.studioOpen.push({ cycle, arrivalMs, world: world.name, x: center.x, z: center.z,
|
||||||
|
terrainHash: digest.digest('hex'), solid })
|
||||||
|
const closed = await context.command('/iris studio close', /^Studio closed\.$|Studio close failed:/, 180000)
|
||||||
|
context.expect(closed === 'Studio closed.', closed)
|
||||||
|
await context.waitUntil(async () => !(await context.observe()).worlds.some(entry => entry.id === world.id),
|
||||||
|
{ label: 'Studio world unload', timeoutMs: 90000, intervalMs: 100 })
|
||||||
|
const transientName = /iris-[0-9a-f-]{36}$/.exec(world.name)[0]
|
||||||
|
const properties = await readFile(path.join(context.server.directory, 'server.properties'), 'utf8')
|
||||||
|
const level = /^level-name=(.+)$/m.exec(properties)?.[1].trim() ?? 'world'
|
||||||
|
const directory = path.join(context.server.directory, level, 'dimensions', 'iris', transientName)
|
||||||
|
await context.waitUntil(async () => {
|
||||||
|
try { await stat(directory); return false } catch (error) {
|
||||||
|
if (error.code === 'ENOENT') return true
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}, { label: 'Studio directory cleanup', timeoutMs: 90000, intervalMs: 100 })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { readFile } from 'node:fs/promises'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
const worldName = 'lifecycle_probe'
|
||||||
|
const worldKey = `iris:${worldName}`
|
||||||
|
const storageName = `world_iris_${worldName}`
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'iris-world-lifecycle',
|
||||||
|
description: 'Create, enter, evacuate, reload, and remove an Iris world through the player commands.',
|
||||||
|
async run(context) {
|
||||||
|
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
|
||||||
|
context.expect(/^isolated=true\s*$/m.test(source), 'World lifecycle requires an isolated instance')
|
||||||
|
const wait = (predicate, label, timeoutMs = 180000) => context.waitUntil(predicate, {
|
||||||
|
label, timeoutMs, intervalMs: 250
|
||||||
|
})
|
||||||
|
const playerWorld = async () => {
|
||||||
|
const observation = await context.observe()
|
||||||
|
const player = observation.players.find(entry => entry.username === context.bot.username)
|
||||||
|
return observation.worlds.find(entry => entry.id === player?.world)?.name
|
||||||
|
}
|
||||||
|
const status = async () => {
|
||||||
|
const response = await context.command('/irislifecycle lifecycle-status', /IRIS_LIFECYCLE |NATIVE_FAILED/, 10000)
|
||||||
|
context.expect(!response.includes('NATIVE_FAILED'), response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
const settledStatus = async expected => {
|
||||||
|
const response = await status()
|
||||||
|
return response.includes(expected) && response.includes('idle=true')
|
||||||
|
}
|
||||||
|
const assertEvacuated = async label => {
|
||||||
|
let destination
|
||||||
|
await wait(async () => {
|
||||||
|
destination = await playerWorld()
|
||||||
|
return Boolean(destination && destination !== storageName)
|
||||||
|
}, label, 30000)
|
||||||
|
context.expect(destination && destination !== storageName, label, { destination })
|
||||||
|
}
|
||||||
|
const assertSafeEntry = async () => {
|
||||||
|
await wait(async () => (await playerWorld()) === storageName, 'Player entered the lifecycle world')
|
||||||
|
await wait(() => {
|
||||||
|
const feet = context.bot.entity.position.floored()
|
||||||
|
const floor = context.bot.blockAt(feet.offset(0, -1, 0))
|
||||||
|
const body = context.bot.blockAt(feet)
|
||||||
|
const head = context.bot.blockAt(feet.offset(0, 1, 0))
|
||||||
|
return context.bot.entity.onGround && floor?.name === 'lime_wool'
|
||||||
|
&& body?.boundingBox === 'empty' && head?.boundingBox === 'empty'
|
||||||
|
}, 'Client sees solid terrain below unobstructed player entry', 30000)
|
||||||
|
const response = await context.command('/irislifecycle lifecycle-entry', /IRIS_LIFECYCLE_ENTRY|NATIVE_FAILED/, 10000)
|
||||||
|
context.expect(response.includes(`IRIS_LIFECYCLE_ENTRY world=${worldKey} seed=78264193 floor=lime_wool safe=true`), response)
|
||||||
|
}
|
||||||
|
await context.step('confirm disposable world is absent', async () => {
|
||||||
|
const response = await status()
|
||||||
|
context.expect(response.includes('loaded=false registered=false configured=false disk=false quarantines=0 cleanup=deleted idle=true'), response)
|
||||||
|
await context.command('/minecraft:gamemode survival @s')
|
||||||
|
await wait(() => context.bot.game.gameMode === 'survival', 'Survival mode', 10000)
|
||||||
|
})
|
||||||
|
await context.step('create through Iris and automatically enter safe terrain', async () => {
|
||||||
|
await context.command(`/iris create name=${worldName} type=native-terrain seed=78264193`)
|
||||||
|
await wait(async () => await settledStatus('loaded=true registered=true configured=true disk=true'), 'Created world registration and storage')
|
||||||
|
await assertSafeEntry()
|
||||||
|
})
|
||||||
|
await context.step('unload while the player is inside and verify evacuation', async () => {
|
||||||
|
context.expect((await playerWorld()) === storageName, 'Player must be inside the world before unload')
|
||||||
|
await context.command(`/iris unloadWorld ${worldKey}`)
|
||||||
|
await wait(async () => await settledStatus('loaded=false registered=true configured=true disk=true'), 'World unloaded with storage preserved')
|
||||||
|
await assertEvacuated('Unload evacuated the player to a loaded world')
|
||||||
|
})
|
||||||
|
await context.step('reload the saved world and enter it again', async () => {
|
||||||
|
await context.command(`/iris loadWorld ${worldName}`)
|
||||||
|
await wait(async () => await settledStatus('loaded=true registered=true configured=true disk=true'), 'World reloaded')
|
||||||
|
await context.command(`/iris tp ${worldKey}`)
|
||||||
|
await assertSafeEntry()
|
||||||
|
})
|
||||||
|
await context.step('remove the occupied world and verify disk cleanup', async () => {
|
||||||
|
await context.command(`/iris remove world=${worldName} delete=true`)
|
||||||
|
let finalStatus
|
||||||
|
await wait(async () => {
|
||||||
|
finalStatus = await status()
|
||||||
|
return finalStatus.includes('loaded=false registered=false configured=false disk=false')
|
||||||
|
&& /cleanup=(deleted|queued)\b/.test(finalStatus) && finalStatus.includes('idle=true')
|
||||||
|
}, 'Removal detached registrations and deleted or queued quarantine')
|
||||||
|
await assertEvacuated('Removal evacuated the player to a loaded world')
|
||||||
|
context.report.worldLifecycle = { world: worldKey, cleanup: /cleanup=(deleted|queued)\b/.exec(finalStatus)[1] }
|
||||||
|
})
|
||||||
|
if (process.env.IRIS_QA_PACK_URL) {
|
||||||
|
await context.step('download and overwrite the fixture pack through Iris', async () => {
|
||||||
|
const response = await context.command(
|
||||||
|
`/iris download link=${process.env.IRIS_QA_PACK_URL} overwrite=true`,
|
||||||
|
/Iris pack 'native-terrain' installed|Iris pack download failed\./,
|
||||||
|
120000
|
||||||
|
)
|
||||||
|
context.expect(response.includes("Iris pack 'native-terrain' installed"), response)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,33 +20,20 @@ import static org.junit.Assert.assertNotNull;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class PaperPluginMetadataTest {
|
public class PaperPluginMetadataTest {
|
||||||
private static final List<String> OPTIONAL_PLUGIN_IDS = List.of(
|
private static final List<String> JOINED_PLUGIN_IDS = List.of(
|
||||||
"PlaceholderAPI",
|
"PlaceholderAPI",
|
||||||
"CraftEngine",
|
"CraftEngine",
|
||||||
"Nexo",
|
"Nexo",
|
||||||
"Oraxen",
|
"Oraxen",
|
||||||
"ItemsAdder",
|
"ItemsAdder",
|
||||||
"SCore",
|
"SCore",
|
||||||
"ExecutableItems",
|
|
||||||
"MythicLib",
|
|
||||||
"MMOItems",
|
"MMOItems",
|
||||||
"eco",
|
"eco",
|
||||||
"EcoItems",
|
|
||||||
"MythicMobs",
|
"MythicMobs",
|
||||||
"MythicCrucible",
|
"MythicCrucible",
|
||||||
"KGenerators",
|
"KGenerators",
|
||||||
"Multiverse-Core",
|
|
||||||
"WorldEdit"
|
"WorldEdit"
|
||||||
);
|
);
|
||||||
private static final List<String> JOINED_PLUGIN_IDS = OPTIONAL_PLUGIN_IDS.stream()
|
|
||||||
.filter(pluginId -> !"ExecutableItems".equals(pluginId) && !"Multiverse-Core".equals(pluginId))
|
|
||||||
.toList();
|
|
||||||
private static final List<String> UNORDERED_PLUGIN_IDS = List.of(
|
|
||||||
"MythicLib", "MMOItems", "MythicMobs", "MythicCrucible");
|
|
||||||
private static final List<String> BUKKIT_SOFT_DEPEND_IDS = OPTIONAL_PLUGIN_IDS.stream()
|
|
||||||
.filter(pluginId -> !"Multiverse-Core".equals(pluginId))
|
|
||||||
.filter(pluginId -> !UNORDERED_PLUGIN_IDS.contains(pluginId))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void paperMetadataDeclaresBootstrapAndFoliaSupport() throws Exception {
|
public void paperMetadataDeclaresBootstrapAndFoliaSupport() throws Exception {
|
||||||
@@ -75,10 +62,13 @@ public class PaperPluginMetadataTest {
|
|||||||
+ " description: Allows survival players to fell Iris-managed trees with an axe.\n"
|
+ " description: Allows survival players to fell Iris-managed trees with an axe.\n"
|
||||||
+ " default: op"));
|
+ " default: op"));
|
||||||
for (String pluginId : JOINED_PLUGIN_IDS) {
|
for (String pluginId : JOINED_PLUGIN_IDS) {
|
||||||
String loadOrder = UNORDERED_PLUGIN_IDS.contains(pluginId) ? "OMIT" : "BEFORE";
|
assertTrue(metadata.contains(optionalDependencyBlock(pluginId, "OMIT", true)));
|
||||||
assertTrue(metadata.contains(optionalDependencyBlock(pluginId, loadOrder, true)));
|
|
||||||
}
|
}
|
||||||
assertTrue(metadata.contains(optionalDependencyBlock("ExecutableItems", "BEFORE", false)));
|
assertFalse(metadata.contains(" ExecutableItems:"));
|
||||||
|
assertFalse(metadata.contains(" EcoItems:"));
|
||||||
|
assertFalse(metadata.contains(" MythicLib:"));
|
||||||
|
assertEquals(1, metadata.lines().filter(line -> line.equals(" load: AFTER")).count());
|
||||||
|
assertEquals(0, metadata.lines().filter(line -> line.equals(" load: BEFORE")).count());
|
||||||
assertTrue(metadata.contains(optionalDependencyBlock("Multiverse-Core", "AFTER", true)));
|
assertTrue(metadata.contains(optionalDependencyBlock("Multiverse-Core", "AFTER", true)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +85,7 @@ public class PaperPluginMetadataTest {
|
|||||||
Map<String, Map<String, Object>> commands = metadata.getCommands();
|
Map<String, Map<String, Object>> commands = metadata.getCommands();
|
||||||
assertTrue(commands.containsKey("iris"));
|
assertTrue(commands.containsKey("iris"));
|
||||||
assertEquals(List.of("ir", "irs"), commands.get("iris").get("aliases"));
|
assertEquals(List.of("ir", "irs"), commands.get("iris").get("aliases"));
|
||||||
assertEquals(BUKKIT_SOFT_DEPEND_IDS, metadata.getSoftDepend());
|
assertTrue(metadata.getSoftDepend().isEmpty());
|
||||||
assertEquals(List.of("Multiverse-Core"), metadata.getLoadBeforePlugins());
|
assertEquals(List.of("Multiverse-Core"), metadata.getLoadBeforePlugins());
|
||||||
Permission all = metadata.getPermissions().stream()
|
Permission all = metadata.getPermissions().stream()
|
||||||
.filter(permission -> "iris.all".equals(permission.getName()))
|
.filter(permission -> "iris.all".equals(permission.getName()))
|
||||||
|
|||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
package art.arcane.iris.command;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.PackFingerprints;
|
||||||
|
|
||||||
|
import art.arcane.iris.Iris;
|
||||||
|
import art.arcane.iris.pack.PackValidationRegistry;
|
||||||
|
import art.arcane.iris.pack.PackValidationCache;
|
||||||
|
import art.arcane.iris.pack.PackValidationResult;
|
||||||
|
import art.arcane.iris.pack.PackValidator;
|
||||||
|
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.List;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
|
||||||
|
public class CommandPackValidationPublicationTest {
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void clearValidation() {
|
||||||
|
PackValidationRegistry.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void changedCachedPackCannotBePersistedAsFreshValidation() throws Exception {
|
||||||
|
File packs = temporaryFolder.newFolder("packs");
|
||||||
|
File root = new File(packs, "terrain");
|
||||||
|
Files.createDirectories(root.toPath());
|
||||||
|
File content = new File(root, "dimension.json");
|
||||||
|
Files.writeString(content.toPath(), "original");
|
||||||
|
File cacheFile = new File(temporaryFolder.getRoot(), "validation-cache.json");
|
||||||
|
Iris previousPlugin = Iris.instance;
|
||||||
|
Iris plugin = mock(Iris.class);
|
||||||
|
Iris.instance = plugin;
|
||||||
|
when(plugin.getDataFile("cache", "pack-validation.json")).thenReturn(cacheFile);
|
||||||
|
try (MockedStatic<Iris> iris = mockStatic(Iris.class);
|
||||||
|
MockedStatic<PackValidationCache> cache = mockStatic(PackValidationCache.class, CALLS_REAL_METHODS)) {
|
||||||
|
cache.when(PackValidationCache::contextFingerprint).thenReturn("context");
|
||||||
|
PackValidationResult valid = new PackValidationResult(root.getName(), List.of(), List.of(), 1L);
|
||||||
|
PackValidationRegistry.publish(valid);
|
||||||
|
PackValidationRegistry.publish(root.toPath(), valid,
|
||||||
|
PackFingerprints.computePackTreeFingerprint(root), "context");
|
||||||
|
Files.writeString(content.toPath(), "changed");
|
||||||
|
Method persist = CommandPack.class.getDeclaredMethod("persistValidationCache", File.class);
|
||||||
|
persist.setAccessible(true);
|
||||||
|
|
||||||
|
persist.invoke(new CommandPack(), packs);
|
||||||
|
|
||||||
|
assertFalse(cacheFile.exists());
|
||||||
|
Files.writeString(content.toPath(), "original");
|
||||||
|
|
||||||
|
persist.invoke(new CommandPack(), packs);
|
||||||
|
|
||||||
|
assertTrue(cacheFile.exists());
|
||||||
|
assertTrue(PackValidationCache.load(cacheFile.toPath(),
|
||||||
|
PackFingerprints.computePackContentSnapshot(packs).content(), "context",
|
||||||
|
List.of(root.getName())).orElseThrow().getFirst().isLoadable());
|
||||||
|
} finally {
|
||||||
|
Iris.instance = previousPlugin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void commandPublishesCanonicalRootValidation() throws Exception {
|
||||||
|
File root = temporaryFolder.newFolder("terrain");
|
||||||
|
PackValidationResult valid = new PackValidationResult(root.getName(), List.of(), List.of(), 1L);
|
||||||
|
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class)) {
|
||||||
|
validator.when(() -> PackValidator.validate(root)).thenReturn(valid);
|
||||||
|
|
||||||
|
assertSame(valid, new CommandPack().runValidate(mock(VolmitSender.class), root));
|
||||||
|
|
||||||
|
assertSame(valid, PackValidationRegistry.get(root.toPath()));
|
||||||
|
assertSame(valid, PackValidationRegistry.get(root.getName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mutationDuringValidationCannotRepublishStaleResult() throws Exception {
|
||||||
|
File root = temporaryFolder.newFolder("terrain");
|
||||||
|
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class);
|
||||||
|
MockedStatic<Iris> iris = mockStatic(Iris.class)) {
|
||||||
|
validator.when(() -> PackValidator.validate(root)).thenAnswer(invocation -> {
|
||||||
|
try (PackValidationRegistry.RootMutation mutation = PackValidationRegistry.beginRootMutation(root.toPath())) {
|
||||||
|
return new PackValidationResult(root.getName(), List.of(), List.of(), 1L);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertFalse(new CommandPack().runValidate(mock(VolmitSender.class), root).isLoadable());
|
||||||
|
assertNull(PackValidationRegistry.get(root.toPath()));
|
||||||
|
assertNull(PackValidationRegistry.get(root.getName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void externalEditDuringValidationPublishesBlockedResult() throws Exception {
|
||||||
|
File root = temporaryFolder.newFolder("terrain");
|
||||||
|
File content = new File(root, "dimension.json");
|
||||||
|
Files.writeString(content.toPath(), "original");
|
||||||
|
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class);
|
||||||
|
MockedStatic<Iris> iris = mockStatic(Iris.class)) {
|
||||||
|
validator.when(() -> PackValidator.validate(root)).thenAnswer(invocation -> {
|
||||||
|
Files.writeString(content.toPath(), "changed");
|
||||||
|
return new PackValidationResult(root.getName(), List.of(), List.of(), 1L);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertFalse(new CommandPack().runValidate(mock(VolmitSender.class), root).isLoadable());
|
||||||
|
assertFalse(PackValidationRegistry.get(root.toPath()).isLoadable());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+47
@@ -5,19 +5,66 @@ import art.arcane.iris.generation.runtime.IrisTelemetrySnapshot;
|
|||||||
import art.arcane.iris.generation.runtime.EngineTelemetrySnapshot;
|
import art.arcane.iris.generation.runtime.EngineTelemetrySnapshot;
|
||||||
import art.arcane.volmlib.integration.IntegrationMetricGroup;
|
import art.arcane.volmlib.integration.IntegrationMetricGroup;
|
||||||
import art.arcane.volmlib.integration.IntegrationMetricSample;
|
import art.arcane.volmlib.integration.IntegrationMetricSample;
|
||||||
|
import art.arcane.volmlib.integration.IntegrationMetricSnapshot;
|
||||||
|
import art.arcane.volmlib.integration.IntegrationSnapshotProvider;
|
||||||
import art.arcane.volmlib.integration.IntegrationMetricSchema;
|
import art.arcane.volmlib.integration.IntegrationMetricSchema;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.plugin.ServicesManager;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class IrisIntegrationServiceTest {
|
public class IrisIntegrationServiceTest {
|
||||||
|
@Test
|
||||||
|
public void disableClearsPublishedSnapshots() {
|
||||||
|
IrisIntegrationService service = new IrisIntegrationService(() -> IrisTelemetrySnapshot.EMPTY);
|
||||||
|
Set<String> keys = Set.of(IntegrationMetricSchema.IRIS_WORLD_COUNT);
|
||||||
|
service.snapshotMetrics(keys);
|
||||||
|
service.publishSnapshots();
|
||||||
|
long generation = service.snapshotMetrics(keys).generation();
|
||||||
|
try (MockedStatic<Bukkit> bukkit = Mockito.mockStatic(Bukkit.class)) {
|
||||||
|
bukkit.when(Bukkit::getServicesManager).thenReturn(Mockito.mock(ServicesManager.class));
|
||||||
|
service.onDisable();
|
||||||
|
}
|
||||||
|
assertTrue(service.snapshotMetrics(keys).samples().isEmpty());
|
||||||
|
assertTrue(service.snapshotMetrics(keys).generation() > generation);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void snapshotRequestsDoNotCollectAndKeepOriginalTelemetryTime() {
|
||||||
|
long sampledAt = System.currentTimeMillis() - 5_000L;
|
||||||
|
IrisTelemetrySnapshot source = telemetry(List.of(), IrisTelemetrySnapshot.PregenSnapshot.INACTIVE, sampledAt);
|
||||||
|
AtomicInteger reads = new AtomicInteger();
|
||||||
|
IrisIntegrationService service = new IrisIntegrationService(() -> {
|
||||||
|
reads.incrementAndGet();
|
||||||
|
return source;
|
||||||
|
});
|
||||||
|
Set<String> keys = Set.of(IntegrationMetricSchema.IRIS_WORLD_COUNT);
|
||||||
|
assertTrue(service.capabilities().contains(IntegrationSnapshotProvider.CAPABILITY));
|
||||||
|
service.publishSnapshots();
|
||||||
|
assertEquals(0, reads.get());
|
||||||
|
assertTrue(service.snapshotMetrics(keys).samples().isEmpty());
|
||||||
|
assertEquals(0, reads.get());
|
||||||
|
|
||||||
|
service.publishSnapshots();
|
||||||
|
IntegrationMetricSnapshot publication = service.snapshotMetrics(keys);
|
||||||
|
assertEquals(1, reads.get());
|
||||||
|
assertEquals(sampledAt, publication.samples().get(IntegrationMetricSchema.IRIS_WORLD_COUNT).sampledAtMs());
|
||||||
|
assertTrue(publication.capturedAtMs() >= sampledAt);
|
||||||
|
assertEquals(publication, service.snapshotMetrics(keys));
|
||||||
|
assertEquals(1, reads.get());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void unavailableTelemetryDoesNotPublishFalseZeroes() {
|
public void unavailableTelemetryDoesNotPublishFalseZeroes() {
|
||||||
IrisIntegrationService service = new IrisIntegrationService(() -> IrisTelemetrySnapshot.EMPTY);
|
IrisIntegrationService service = new IrisIntegrationService(() -> IrisTelemetrySnapshot.EMPTY);
|
||||||
|
|||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
package art.arcane.iris.integration;
|
||||||
|
|
||||||
|
import art.arcane.iris.Iris;
|
||||||
|
import art.arcane.volmlib.util.bukkit.papi.PlaceholderRegistration;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.event.HandlerList;
|
||||||
|
import org.bukkit.event.server.PluginDisableEvent;
|
||||||
|
import org.bukkit.event.server.PluginEnableEvent;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
import org.bukkit.plugin.PluginManager;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedConstruction;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.doCallRealMethod;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockConstruction;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class IrisPapiLifecycleTest {
|
||||||
|
@Test
|
||||||
|
public void lateEnableRegistersOnceAndReattachesAfterDisable() throws Exception {
|
||||||
|
Iris iris = plugin(true);
|
||||||
|
Plugin placeholders = namedPlugin("PlaceholderAPI");
|
||||||
|
PluginManager plugins = mock(PluginManager.class);
|
||||||
|
|
||||||
|
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
|
||||||
|
MockedStatic<HandlerList> handlers = mockStatic(HandlerList.class);
|
||||||
|
MockedStatic<PlaceholderRegistration> availability = mockStatic(PlaceholderRegistration.class);
|
||||||
|
MockedStatic<IrisPapiInstaller> installer = mockStatic(IrisPapiInstaller.class);
|
||||||
|
MockedConstruction<PlaceholderRegistration> registrations = mockConstruction(PlaceholderRegistration.class)) {
|
||||||
|
bukkit.when(Bukkit::getPluginManager).thenReturn(plugins);
|
||||||
|
availability.when(PlaceholderRegistration::isPlaceholderApiEnabled).thenReturn(true);
|
||||||
|
installer.when(() -> IrisPapiInstaller.install(any(), any(), any())).thenReturn(true);
|
||||||
|
|
||||||
|
iris.onPlaceholderApiEnable(new PluginEnableEvent(placeholders));
|
||||||
|
iris.onPlaceholderApiEnable(new PluginEnableEvent(placeholders));
|
||||||
|
assertEquals(1, registrations.constructed().size());
|
||||||
|
verify(plugins).registerEvents(any(IrisPapiListener.class), eq(iris));
|
||||||
|
|
||||||
|
iris.onPlaceholderApiDisable(new PluginDisableEvent(namedPlugin("Unrelated")));
|
||||||
|
verify(registrations.constructed().getFirst(), never()).unregister();
|
||||||
|
|
||||||
|
iris.onPlaceholderApiDisable(new PluginDisableEvent(placeholders));
|
||||||
|
verify(registrations.constructed().getFirst()).unregister();
|
||||||
|
handlers.verify(() -> HandlerList.unregisterAll(any(IrisPapiListener.class)));
|
||||||
|
|
||||||
|
iris.onPlaceholderApiEnable(new PluginEnableEvent(placeholders));
|
||||||
|
assertEquals(2, registrations.constructed().size());
|
||||||
|
verify(plugins, times(2)).registerEvents(any(IrisPapiListener.class), eq(iris));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void enableEventsCannotRegisterBeforeIrisIsReadyOrDuringShutdown() throws Exception {
|
||||||
|
Iris iris = plugin(false);
|
||||||
|
Plugin placeholders = namedPlugin("PlaceholderAPI");
|
||||||
|
|
||||||
|
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
|
||||||
|
MockedStatic<IrisPapiInstaller> installer = mockStatic(IrisPapiInstaller.class)) {
|
||||||
|
iris.onPlaceholderApiEnable(new PluginEnableEvent(placeholders));
|
||||||
|
installer.verifyNoInteractions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unrelatedEnableEventsDoNotRegisterPlaceholders() throws Exception {
|
||||||
|
Iris iris = plugin(true);
|
||||||
|
|
||||||
|
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
|
||||||
|
MockedStatic<IrisPapiInstaller> installer = mockStatic(IrisPapiInstaller.class)) {
|
||||||
|
iris.onPlaceholderApiEnable(new PluginEnableEvent(namedPlugin("Unrelated")));
|
||||||
|
installer.verifyNoInteractions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Iris plugin(boolean active) throws ReflectiveOperationException {
|
||||||
|
Iris iris = mock(Iris.class);
|
||||||
|
when(iris.getLogger()).thenReturn(Logger.getAnonymousLogger());
|
||||||
|
doCallRealMethod().when(iris).onPlaceholderApiEnable(any());
|
||||||
|
doCallRealMethod().when(iris).onPlaceholderApiDisable(any());
|
||||||
|
Field lifecycle = Iris.class.getDeclaredField("papiLifecycleActive");
|
||||||
|
lifecycle.setAccessible(true);
|
||||||
|
lifecycle.setBoolean(iris, active);
|
||||||
|
return iris;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Plugin namedPlugin(String name) {
|
||||||
|
Plugin plugin = mock(Plugin.class);
|
||||||
|
when(plugin.getName()).thenReturn(name);
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
}
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
package art.arcane.iris.update;
|
||||||
|
|
||||||
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
|
import art.arcane.volmlib.util.update.BukkitUpdateService;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.clearInvocations;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
|
|
||||||
|
public class IrisUpdateNotifierTest {
|
||||||
|
@Test
|
||||||
|
public void acceptedSettingsReconfigureChecksAndClosingRemovesTheListener() {
|
||||||
|
IrisSettings previous = IrisSettings.settings;
|
||||||
|
IrisSettings.settings = new IrisSettings();
|
||||||
|
Plugin plugin = mock(Plugin.class);
|
||||||
|
BukkitUpdateService service = mock(BukkitUpdateService.class);
|
||||||
|
try (MockedStatic<BukkitUpdateService> registration = mockStatic(BukkitUpdateService.class)) {
|
||||||
|
registration.when(() -> BukkitUpdateService.register(eq(plugin), any())).thenReturn(service);
|
||||||
|
IrisUpdateNotifier notifier = new IrisUpdateNotifier(plugin);
|
||||||
|
try {
|
||||||
|
ArgumentCaptor<BukkitUpdateService.Options> options = ArgumentCaptor.forClass(BukkitUpdateService.Options.class);
|
||||||
|
registration.verify(() -> BukkitUpdateService.register(eq(plugin), options.capture()));
|
||||||
|
assertEquals("VolmitSoftware", options.getValue().owner());
|
||||||
|
assertEquals("Iris", options.getValue().repository());
|
||||||
|
assertEquals("iris.update", options.getValue().permission());
|
||||||
|
assertTrue(options.getValue().enabled().getAsBoolean());
|
||||||
|
|
||||||
|
clearInvocations(service);
|
||||||
|
assertFalse(IrisSettings.applyHotloadSnapshot(
|
||||||
|
"{\"general\":{\"updateNotifications\":false}}", candidate -> false));
|
||||||
|
assertTrue(options.getValue().enabled().getAsBoolean());
|
||||||
|
verifyNoInteractions(service);
|
||||||
|
|
||||||
|
IrisSettings.installHotloadSnapshot("{\"general\":{\"updateNotifications\":false}}");
|
||||||
|
assertFalse(options.getValue().enabled().getAsBoolean());
|
||||||
|
verify(service).reconfigure();
|
||||||
|
IrisSettings.settings.getGeneral().setUpdateNotifications(true);
|
||||||
|
assertFalse(options.getValue().enabled().getAsBoolean());
|
||||||
|
} finally {
|
||||||
|
notifier.close();
|
||||||
|
}
|
||||||
|
verify(service).close();
|
||||||
|
clearInvocations(service);
|
||||||
|
IrisSettings.installHotloadSnapshot("{}");
|
||||||
|
verifyNoInteractions(service);
|
||||||
|
} finally {
|
||||||
|
IrisSettings.settings = previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
package art.arcane.iris.world.tree;
|
||||||
|
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class RoutedBlockBreakEventTest {
|
||||||
|
@Test
|
||||||
|
public void nativeLootUsesTheReservedToolCloneWithoutReadingPlayerInventory() {
|
||||||
|
Block block = mock(Block.class);
|
||||||
|
Player player = mock(Player.class);
|
||||||
|
ItemStack reservedTool = mock(ItemStack.class);
|
||||||
|
ItemStack capturedTool = mock(ItemStack.class);
|
||||||
|
ItemStack drop = mock(ItemStack.class);
|
||||||
|
Collection<ItemStack> expected = List.of(drop);
|
||||||
|
when(reservedTool.clone()).thenReturn(capturedTool);
|
||||||
|
when(block.getDrops(capturedTool)).thenReturn(expected);
|
||||||
|
RoutedBlockBreakEvent event = new RoutedBlockBreakEvent(new RoutedBlockBreakEvent.Options(
|
||||||
|
block, player, mock(FellingRun.class), mock(TreeFellerSVC.class), reservedTool));
|
||||||
|
|
||||||
|
reservedTool.setType(Material.AIR);
|
||||||
|
|
||||||
|
assertSame(expected, event.captureNativeDrops());
|
||||||
|
verify(reservedTool).clone();
|
||||||
|
verify(block).getDrops(capturedTool);
|
||||||
|
verify(block, never()).getDrops(reservedTool);
|
||||||
|
verifyNoInteractions(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,7 +56,7 @@ apply from: file('../../gradle/volmlib-stamp.gradle')
|
|||||||
|
|
||||||
Properties rootProperties = new Properties()
|
Properties rootProperties = new Properties()
|
||||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.0-26.2'))
|
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.3-26.2'))
|
||||||
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
|
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
|
||||||
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(rootProperties.getProperty('fabricLoaderVersion', '0.19.5'))
|
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(rootProperties.getProperty('fabricLoaderVersion', '0.19.5'))
|
||||||
Closure<String> irisArtifactName = { String platform, String targetVersion ->
|
Closure<String> irisArtifactName = { String platform, String targetVersion ->
|
||||||
@@ -313,6 +313,8 @@ tasks.named('shadowJar', ShadowJar).configure {
|
|||||||
// The Bukkit platform binding cannot load without org.bukkit on the classpath, which no mod
|
// The Bukkit platform binding cannot load without org.bukkit on the classpath, which no mod
|
||||||
// loader has. Shipping it only adds dead classes that reference a missing package.
|
// loader has. Shipping it only adds dead classes that reference a missing package.
|
||||||
exclude('art/arcane/iris/platform/bukkit/**')
|
exclude('art/arcane/iris/platform/bukkit/**')
|
||||||
|
exclude('art/arcane/iris/platform/bootstrap/SlimJar*.class')
|
||||||
|
exclude('art/arcane/iris/platform/bootstrap/NativeRuntimeLibraries*.class')
|
||||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
filesMatching('META-INF/services/**') { FileCopyDetails entry ->
|
filesMatching('META-INF/services/**') { FileCopyDetails entry ->
|
||||||
entry.duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
entry.duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ apply from: file('../../gradle/volmlib-stamp.gradle')
|
|||||||
|
|
||||||
Properties rootProperties = new Properties()
|
Properties rootProperties = new Properties()
|
||||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.0-26.2'))
|
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.3-26.2'))
|
||||||
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
|
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
|
||||||
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(rootProperties.getProperty('forgeVersion', '26.3-66.0.8'))
|
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(rootProperties.getProperty('forgeVersion', '26.3-66.0.8'))
|
||||||
Closure<String> loaderDisplayVersion = { String loaderVersion ->
|
Closure<String> loaderDisplayVersion = { String loaderVersion ->
|
||||||
@@ -305,6 +305,8 @@ tasks.named('shadowJar', ShadowJar).configure {
|
|||||||
// The Bukkit platform binding cannot load without org.bukkit on the classpath, which no mod
|
// The Bukkit platform binding cannot load without org.bukkit on the classpath, which no mod
|
||||||
// loader has. Shipping it only adds dead classes that reference a missing package.
|
// loader has. Shipping it only adds dead classes that reference a missing package.
|
||||||
exclude('art/arcane/iris/platform/bukkit/**')
|
exclude('art/arcane/iris/platform/bukkit/**')
|
||||||
|
exclude('art/arcane/iris/platform/bootstrap/SlimJar*.class')
|
||||||
|
exclude('art/arcane/iris/platform/bootstrap/NativeRuntimeLibraries*.class')
|
||||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
filesMatching('META-INF/services/**') { FileCopyDetails entry ->
|
filesMatching('META-INF/services/**') { FileCopyDetails entry ->
|
||||||
entry.duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
entry.duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||||
|
|||||||
-32
@@ -195,8 +195,6 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
|
|||||||
if (!nativeGenerator.represents(level)) {
|
if (!nativeGenerator.represents(level)) {
|
||||||
throw new IllegalArgumentException("NativeWorld does not use Iris generator '" + dimensionKey + "'");
|
throw new IllegalArgumentException("NativeWorld does not use Iris generator '" + dimensionKey + "'");
|
||||||
}
|
}
|
||||||
requireGlobalStructureGeneration(
|
|
||||||
NativeModdedServer.forWorld(level).generateStructures(), dimensionKey);
|
|
||||||
Engine activeEngine = engineIfBound();
|
Engine activeEngine = engineIfBound();
|
||||||
Engine replacement = ModdedWorldEngines.prepareReplacement(
|
Engine replacement = ModdedWorldEngines.prepareReplacement(
|
||||||
level,
|
level,
|
||||||
@@ -462,13 +460,6 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
|
|||||||
// A recorded failure stands until repoint or unbind resets the binding; re-running the bind would repeat the
|
// A recorded failure stands until repoint or unbind resets the binding; re-running the bind would repeat the
|
||||||
// history verification on every chunk call and fail the same way.
|
// history verification on every chunk call and fail the same way.
|
||||||
engineBinding.throwIfFailed(dimensionKey);
|
engineBinding.throwIfFailed(dimensionKey);
|
||||||
try {
|
|
||||||
requireGlobalStructureGeneration(
|
|
||||||
NativeModdedServer.forWorld(level).generateStructures(), dimensionKey);
|
|
||||||
} catch (RuntimeException error) {
|
|
||||||
recordBindFailure(level, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
// Cache the owning level so hot paths never scan the level map to find themselves.
|
// Cache the owning level so hot paths never scan the level map to find themselves.
|
||||||
boundLevel = level;
|
boundLevel = level;
|
||||||
Engine cached = engine;
|
Engine cached = engine;
|
||||||
@@ -575,29 +566,6 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void requireGlobalStructureGeneration(boolean enabled, String dimensionKey) {
|
|
||||||
if (enabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String remedy = integratedEnvironment()
|
|
||||||
? "enable 'Generate Structures' for this world; Iris requires it, then deny families through "
|
|
||||||
+ "importedStructures.disabled or complete keys through importedStructures.disabledExact"
|
|
||||||
: "set generate-structures=true in server.properties, restart the server, "
|
|
||||||
+ "then deny families through importedStructures.disabled or complete keys through "
|
|
||||||
+ "importedStructures.disabledExact";
|
|
||||||
throw new IllegalStateException("Iris generator '" + dimensionKey
|
|
||||||
+ "' cannot bind while generate-structures=false; " + remedy);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean integratedEnvironment() {
|
|
||||||
try {
|
|
||||||
return ModdedEngineBootstrap.loader().clientEnvironment();
|
|
||||||
} catch (Throwable e) {
|
|
||||||
// No loader bound (unit tests, very early boot): assume dedicated wording.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Null only while no level uses this generator (world creation screens, codec validation). A bound level whose
|
* Null only while no level uses this generator (world creation screens, codec validation). A bound level whose
|
||||||
* engine is refused, unloading or stopping throws, so no stage can mistake it for an unbound generator.
|
* engine is refused, unloading or stopping throws, so no stage can mistake it for an unbound generator.
|
||||||
|
|||||||
@@ -325,7 +325,7 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
|
|||||||
|
|
||||||
private H getNoiseBiome(Engine engine, int quartX, int quartY, int quartZ,
|
private H getNoiseBiome(Engine engine, int quartX, int quartY, int quartZ,
|
||||||
S sampler) {
|
S sampler) {
|
||||||
if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) {
|
if (isGuaranteedSurfaceBiome(engine, quartY)) {
|
||||||
return getSurfaceStructureBiome(engine, quartX, quartZ, sampler);
|
return getSurfaceStructureBiome(engine, quartX, quartZ, sampler);
|
||||||
}
|
}
|
||||||
boolean cacheable = isBiomeCacheable(
|
boolean cacheable = isBiomeCacheable(
|
||||||
@@ -512,7 +512,7 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
|
|||||||
}
|
}
|
||||||
boolean monumentQuery = isMonumentSurfaceBiomeQuery(
|
boolean monumentQuery = isMonumentSurfaceBiomeQuery(
|
||||||
y, radius, engine.getMinHeight(), engine.getDimension().getFluidHeight());
|
y, radius, engine.getMinHeight(), engine.getDimension().getFluidHeight());
|
||||||
surfaceQuery = monumentQuery || isGuaranteedSurfaceBiome(minQuartY, engine.getMinHeight());
|
surfaceQuery = monumentQuery || isGuaranteedSurfaceBiome(engine, minQuartY);
|
||||||
}
|
}
|
||||||
if (!surfaceQuery) {
|
if (!surfaceQuery) {
|
||||||
return query.fallback().get();
|
return query.fallback().get();
|
||||||
@@ -701,6 +701,11 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
|
|||||||
int blockZ = quartZ << 2;
|
int blockZ = quartZ << 2;
|
||||||
int internalY = blockY - engine.getMinHeight();
|
int internalY = blockY - engine.getMinHeight();
|
||||||
int caveSwitchY = Math.max(-8 - engine.getMinHeight(), 40);
|
int caveSwitchY = Math.max(-8 - engine.getMinHeight(), 40);
|
||||||
|
IrisBiome featureBiome = engine.getSubterrainBiome(blockX, internalY, blockZ);
|
||||||
|
if (featureBiome != null) {
|
||||||
|
return createBiomeResolution(engine, featureBiome, true, engine.getDimension(), engine.getData(),
|
||||||
|
blockX, blockY, blockZ);
|
||||||
|
}
|
||||||
DimensionStackLayout.Layer stackLayer = includeDimensionStack
|
DimensionStackLayout.Layer stackLayer = includeDimensionStack
|
||||||
? stackLayout == null
|
? stackLayout == null
|
||||||
? resolveDimensionStackLayer(engine, blockX, internalY, blockZ)
|
? resolveDimensionStackLayer(engine, blockX, internalY, blockZ)
|
||||||
@@ -846,7 +851,13 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
|
|||||||
return configuredBiomeKeys;
|
return configuredBiomeKeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
static boolean isGuaranteedSurfaceBiome(int quartY, int minHeight) {
|
static boolean isGuaranteedSurfaceBiome(Engine engine, int quartY) {
|
||||||
|
IrisDimension dimension = engine.getDimension();
|
||||||
|
if (dimension != null && dimension.allowsSubterrainFeatures()
|
||||||
|
&& !dimension.getSubterrainFeatures().isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int minHeight = engine.getMinHeight();
|
||||||
int internalY = (quartY << 2) - minHeight;
|
int internalY = (quartY << 2) - minHeight;
|
||||||
int caveSwitchY = Math.max(-8 - minHeight, 40);
|
int caveSwitchY = Math.max(-8 - minHeight, 40);
|
||||||
return internalY > caveSwitchY;
|
return internalY > caveSwitchY;
|
||||||
|
|||||||
+11
-11
@@ -31,6 +31,7 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockDropHooks;
|
|||||||
|
|
||||||
import art.arcane.iris.pack.loading.IrisData;
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.generation.runtime.BiomeEnvironment;
|
||||||
import art.arcane.iris.structure.placement.LootResolver;
|
import art.arcane.iris.structure.placement.LootResolver;
|
||||||
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
|
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
|
||||||
import art.arcane.iris.generation.biome.IrisBiome;
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
@@ -181,7 +182,10 @@ public final class ModdedBlockBreakHandler {
|
|||||||
NativeBlockState brokenState,
|
NativeBlockState brokenState,
|
||||||
Engine engine
|
Engine engine
|
||||||
) {
|
) {
|
||||||
KList<IrisBlockDrops> providers = providers(engine, position, brokenState);
|
BiomeEnvironment environment = engine.getBiomeEnvironment(position.x(),
|
||||||
|
position.y() - engine.getMinHeight(), position.z());
|
||||||
|
try (BiomeEnvironment.Scope ignored = engine.openBiomeEnvironmentScope(environment)) {
|
||||||
|
KList<IrisBlockDrops> providers = providers(environment, brokenState);
|
||||||
if (providers.isEmpty()) {
|
if (providers.isEmpty()) {
|
||||||
return Result.empty();
|
return Result.empty();
|
||||||
}
|
}
|
||||||
@@ -202,6 +206,7 @@ public final class ModdedBlockBreakHandler {
|
|||||||
}
|
}
|
||||||
return new Result(drops, replaceVanillaDrops, null);
|
return new Result(drops, replaceVanillaDrops, null);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void removeMarker(Engine engine, NativeBlockPoint position) {
|
private static void removeMarker(Engine engine, NativeBlockPoint position) {
|
||||||
int mantleY = position.y() - engine.getMinHeight();
|
int mantleY = position.y() - engine.getMinHeight();
|
||||||
@@ -225,22 +230,17 @@ public final class ModdedBlockBreakHandler {
|
|||||||
engine.getMantle().getMantle().remove(position.x(), mantleY, position.z(), TreeBlockMaterial.class);
|
engine.getMantle().getMantle().remove(position.x(), mantleY, position.z(), TreeBlockMaterial.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static KList<IrisBlockDrops> providers(Engine engine, NativeBlockPoint position, NativeBlockState brokenState) {
|
private static KList<IrisBlockDrops> providers(BiomeEnvironment environment, NativeBlockState brokenState) {
|
||||||
KList<IrisBlockDrops> providers = new KList<>();
|
KList<IrisBlockDrops> providers = new KList<>();
|
||||||
IrisData data = engine.getData();
|
IrisData data = environment.data();
|
||||||
int relativeY = position.y() - engine.getMinHeight();
|
IrisBiome biome = environment.biome();
|
||||||
IrisBiome biome = engine.getBiome(position.x(), relativeY, position.z());
|
|
||||||
if (biome != null) {
|
|
||||||
addMatching(providers, biome.getBlockDrops(), brokenState, data);
|
addMatching(providers, biome.getBlockDrops(), brokenState, data);
|
||||||
}
|
|
||||||
if (skipsParents(providers)) {
|
if (skipsParents(providers)) {
|
||||||
return providers;
|
return providers;
|
||||||
}
|
}
|
||||||
IrisRegion region = engine.getRegion(position.x(), relativeY, position.z());
|
IrisRegion region = environment.region();
|
||||||
if (region != null) {
|
|
||||||
addMatching(providers, region.getBlockDrops(), brokenState, data);
|
addMatching(providers, region.getBlockDrops(), brokenState, data);
|
||||||
}
|
addMatching(providers, environment.dimension().getBlockDrops(), brokenState, data);
|
||||||
addMatching(providers, engine.getDimension().getBlockDrops(), brokenState, data);
|
|
||||||
return providers;
|
return providers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -50,7 +50,7 @@ public final class ModdedDecoratorHooks implements DecoratorPlatformHooks.FaceFi
|
|||||||
int xx = rX + dx;
|
int xx = rX + dx;
|
||||||
int yy = y + dy;
|
int yy = y + dy;
|
||||||
int zz = rZ + dz;
|
int zz = rZ + dz;
|
||||||
return xx < 0 || xx > 15 || zz < 0 || zz > 15 || yy < 0 || yy > hunk.getHeight()
|
return xx < 0 || xx > 15 || zz < 0 || zz > 15 || yy < 0 || yy >= hunk.getHeight()
|
||||||
? null : hunk.get(xx, yy, zz);
|
? null : hunk.get(xx, yy, zz);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ import art.arcane.iris.modded.service.ModdedEngineMaintenanceService;
|
|||||||
import art.arcane.iris.modded.service.ModdedEntitySpawnService;
|
import art.arcane.iris.modded.service.ModdedEntitySpawnService;
|
||||||
import art.arcane.iris.modded.service.ModdedLogFilterService;
|
import art.arcane.iris.modded.service.ModdedLogFilterService;
|
||||||
import art.arcane.iris.modded.service.ModdedPreservationService;
|
import art.arcane.iris.modded.service.ModdedPreservationService;
|
||||||
|
import art.arcane.iris.modded.service.ModdedPackUpdateService;
|
||||||
import art.arcane.iris.modded.service.ModdedSettingsHotloadService;
|
import art.arcane.iris.modded.service.ModdedSettingsHotloadService;
|
||||||
import art.arcane.iris.modded.service.ModdedStudioHotloadService;
|
import art.arcane.iris.modded.service.ModdedStudioHotloadService;
|
||||||
import art.arcane.iris.modded.service.ModdedTreeFellerService;
|
import art.arcane.iris.modded.service.ModdedTreeFellerService;
|
||||||
@@ -477,6 +478,7 @@ public final class ModdedEngineBootstrap {
|
|||||||
createdServices.register(ModdedLogFilterService.class, new ModdedLogFilterService());
|
createdServices.register(ModdedLogFilterService.class, new ModdedLogFilterService());
|
||||||
createdServices.register(ModdedEngineMaintenanceService.class, new ModdedEngineMaintenanceService());
|
createdServices.register(ModdedEngineMaintenanceService.class, new ModdedEngineMaintenanceService());
|
||||||
createdServices.register(ModdedSettingsHotloadService.class, new ModdedSettingsHotloadService());
|
createdServices.register(ModdedSettingsHotloadService.class, new ModdedSettingsHotloadService());
|
||||||
|
createdServices.register(ModdedPackUpdateService.class, new ModdedPackUpdateService());
|
||||||
ModdedStudioHotloadService studioHotloadService = createdServices.register(
|
ModdedStudioHotloadService studioHotloadService = createdServices.register(
|
||||||
ModdedStudioHotloadService.class, new ModdedStudioHotloadService());
|
ModdedStudioHotloadService.class, new ModdedStudioHotloadService());
|
||||||
createdServices.register(ModdedChunkUpdateService.class, new ModdedChunkUpdateService());
|
createdServices.register(ModdedChunkUpdateService.class, new ModdedChunkUpdateService());
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import java.util.concurrent.RejectedExecutionException;
|
|||||||
import java.util.concurrent.ThreadPoolExecutor;
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Owns the Iris generation pool and the single decision of whether this loader already runs chunk
|
* Owns the Iris generation pool and the single decision of whether this loader already runs chunk
|
||||||
@@ -43,7 +42,7 @@ public final class ModdedGenPool {
|
|||||||
private static final long SHUTDOWN_DRAIN_MILLIS = 2_000L;
|
private static final long SHUTDOWN_DRAIN_MILLIS = 2_000L;
|
||||||
private static final AtomicInteger GEN_THREAD_SEQ = new AtomicInteger();
|
private static final AtomicInteger GEN_THREAD_SEQ = new AtomicInteger();
|
||||||
private static final ChunkSystem CHUNK_SYSTEM = detectChunkSystem();
|
private static final ChunkSystem CHUNK_SYSTEM = detectChunkSystem();
|
||||||
private static final AtomicReference<ExecutorService> GEN_POOL = new AtomicReference<>(createGenPool());
|
private static ExecutorService generationPool = createGenPool();
|
||||||
|
|
||||||
private ModdedGenPool() {
|
private ModdedGenPool() {
|
||||||
}
|
}
|
||||||
@@ -63,39 +62,32 @@ public final class ModdedGenPool {
|
|||||||
return CHUNK_SYSTEM.description();
|
return CHUNK_SYSTEM.description();
|
||||||
}
|
}
|
||||||
|
|
||||||
static ExecutorService pool() {
|
static synchronized ExecutorService pool() {
|
||||||
ExecutorService pool = GEN_POOL.get();
|
if (generationPool.isShutdown()) {
|
||||||
if (pool != null && !pool.isShutdown()) {
|
|
||||||
return pool;
|
|
||||||
}
|
|
||||||
start();
|
|
||||||
ExecutorService restarted = GEN_POOL.get();
|
|
||||||
if (restarted == null) {
|
|
||||||
throw new RejectedExecutionException("Iris gen pool is shut down");
|
throw new RejectedExecutionException("Iris gen pool is shut down");
|
||||||
}
|
}
|
||||||
return restarted;
|
return generationPool;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void start() {
|
static synchronized void start() {
|
||||||
while (true) {
|
if (!generationPool.isShutdown()) {
|
||||||
ExecutorService current = GEN_POOL.get();
|
|
||||||
if (current != null && !current.isShutdown()) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ExecutorService created = createGenPool();
|
if (!generationPool.isTerminated()) {
|
||||||
if (GEN_POOL.compareAndSet(current, created)) {
|
throw new IllegalStateException("Iris gen pool cannot restart while prior workers are still active");
|
||||||
return;
|
|
||||||
}
|
|
||||||
created.shutdownNow();
|
|
||||||
}
|
}
|
||||||
|
generationPool = createGenPool();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void shutdown() {
|
static void shutdown() {
|
||||||
ExecutorService pool = GEN_POOL.getAndSet(null);
|
ExecutorService pool;
|
||||||
if (pool == null) {
|
synchronized (ModdedGenPool.class) {
|
||||||
|
pool = generationPool;
|
||||||
|
if (pool.isTerminated()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pool.shutdown();
|
pool.shutdown();
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (pool.awaitTermination(SHUTDOWN_DRAIN_MILLIS, TimeUnit.MILLISECONDS)) {
|
if (pool.awaitTermination(SHUTDOWN_DRAIN_MILLIS, TimeUnit.MILLISECONDS)) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
+9
-7
@@ -6,10 +6,9 @@ import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
|||||||
import art.arcane.iris.pack.loading.IrisData;
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
import art.arcane.iris.configuration.IrisSettings;
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
import art.arcane.iris.pack.BrokenPackException;
|
import art.arcane.iris.pack.BrokenPackException;
|
||||||
import art.arcane.iris.pack.PackValidationCache;
|
import art.arcane.iris.pack.PackFingerprints;
|
||||||
import art.arcane.iris.pack.PackValidationRegistry;
|
import art.arcane.iris.pack.PackValidationRegistry;
|
||||||
import art.arcane.iris.pack.PackValidationResult;
|
import art.arcane.iris.pack.PackValidationResult;
|
||||||
import art.arcane.iris.pack.PackValidator;
|
|
||||||
import art.arcane.iris.world.history.GenerationEpoch;
|
import art.arcane.iris.world.history.GenerationEpoch;
|
||||||
import art.arcane.iris.world.history.GenerationEpochContractFactory;
|
import art.arcane.iris.world.history.GenerationEpochContractFactory;
|
||||||
import art.arcane.iris.world.history.GenerationHistory;
|
import art.arcane.iris.world.history.GenerationHistory;
|
||||||
@@ -174,7 +173,7 @@ final class ModdedGenerationHistoryStorage {
|
|||||||
static ActivePack resolveActive(GenerationHistory history) throws IOException {
|
static ActivePack resolveActive(GenerationHistory history) throws IOException {
|
||||||
GenerationEpoch epoch = history.activeEpoch();
|
GenerationEpoch epoch = history.activeEpoch();
|
||||||
Path packRoot = normalize(history.activePackRoot());
|
Path packRoot = normalize(history.activePackRoot());
|
||||||
validatePack(packRoot, epoch.packFingerprint());
|
validatePack(packRoot);
|
||||||
validateDimension(packRoot, epoch);
|
validateDimension(packRoot, epoch);
|
||||||
return new ActivePack(
|
return new ActivePack(
|
||||||
packRoot,
|
packRoot,
|
||||||
@@ -184,13 +183,16 @@ final class ModdedGenerationHistoryStorage {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void validatePack(Path packRoot, String fingerprint) {
|
static PackValidationResult validatePack(Path packRoot) {
|
||||||
String contextFingerprint = PackValidationCache.contextFingerprint();
|
PackValidationResult validation = PackValidationRegistry.getMatching(
|
||||||
PackValidationResult validation = PackValidator.validate(packRoot.toFile());
|
packRoot, PackFingerprints.computePackTreeFingerprint(packRoot.toFile()));
|
||||||
PackValidationRegistry.publish(packRoot, validation, fingerprint, contextFingerprint);
|
if (validation == null) {
|
||||||
|
validation = ModdedStartup.validatePack(packRoot.toFile());
|
||||||
|
}
|
||||||
if (!validation.isLoadable()) {
|
if (!validation.isLoadable()) {
|
||||||
throw new BrokenPackException(packRoot.toString(), validation.getBlockingErrors());
|
throw new BrokenPackException(packRoot.toString(), validation.getBlockingErrors());
|
||||||
}
|
}
|
||||||
|
return validation;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void validateDimension(
|
private static void validateDimension(
|
||||||
|
|||||||
@@ -20,13 +20,11 @@ package art.arcane.iris.modded;
|
|||||||
|
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedLoader;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedLoader;
|
||||||
|
|
||||||
import art.arcane.iris.spi.IrisPlatforms;
|
|
||||||
import art.arcane.iris.configuration.IrisSettings;
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
import art.arcane.iris.diagnostics.splash.IrisSplashComposer;
|
import art.arcane.iris.diagnostics.splash.IrisSplashComposer;
|
||||||
import art.arcane.iris.diagnostics.splash.IrisSplashRenderer;
|
import art.arcane.iris.diagnostics.splash.IrisSplashRenderer;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
public final class ModdedIrisSplash {
|
public final class ModdedIrisSplash {
|
||||||
|
|
||||||
@@ -34,18 +32,11 @@ public final class ModdedIrisSplash {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void print(NativeModdedLoader loader) {
|
public static void print(NativeModdedLoader loader) {
|
||||||
printPacks(loader);
|
|
||||||
if (isLogoEnabled()) {
|
if (isLogoEnabled()) {
|
||||||
printLogo(loader);
|
printLogo(loader);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void printPacks(NativeModdedLoader loader) {
|
|
||||||
File packFolder = IrisPlatforms.get().packsFolderNoCreate();
|
|
||||||
IrisSplashComposer.composePackLines(packFolder, IrisLogging::reportError)
|
|
||||||
.thenAccept(lines -> lines.forEach(IrisLogging::info));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void printLogo(NativeModdedLoader loader) {
|
private static void printLogo(NativeModdedLoader loader) {
|
||||||
String serverLine = loader.platformName() + " / Minecraft " + loader.minecraftVersion();
|
String serverLine = loader.platformName() + " / Minecraft " + loader.minecraftVersion();
|
||||||
String[] splash = IrisSplashRenderer.renderPlain();
|
String[] splash = IrisSplashRenderer.renderPlain();
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ public final class ModdedMixinAudit {
|
|||||||
private static final AtomicBoolean AUDITED = new AtomicBoolean(false);
|
private static final AtomicBoolean AUDITED = new AtomicBoolean(false);
|
||||||
|
|
||||||
static final List<ExpectedMixin> EXPECTED = List.of(
|
static final List<ExpectedMixin> EXPECTED = List.of(
|
||||||
|
new ExpectedMixin(ENTITY_CONFIG, "RegistryClientNamesMixin",
|
||||||
|
NativeMixinTarget.REGISTRY_PACKET, "volmlib$clientRegistryNames",
|
||||||
|
false, NativeMixinFlags::registryClientNamesRan),
|
||||||
new ExpectedMixin(ENTITY_CONFIG, "EntityPersistenceMixin",
|
new ExpectedMixin(ENTITY_CONFIG, "EntityPersistenceMixin",
|
||||||
NativeMixinTarget.ENTITY, "iris$applyGeneratedPersistence",
|
NativeMixinTarget.ENTITY, "iris$applyGeneratedPersistence",
|
||||||
false, NativeMixinFlags::entityPersistenceRan),
|
false, NativeMixinFlags::entityPersistenceRan),
|
||||||
@@ -108,7 +111,7 @@ public final class ModdedMixinAudit {
|
|||||||
ModdedIrisLog.error(" missing: {}", entry);
|
ModdedIrisLog.error(" missing: {}", entry);
|
||||||
}
|
}
|
||||||
ModdedIrisLog.error("The mixin config was not registered for this loader (fabric.mod.json mixins, neoforge.mods.toml [[mixins]], forge MixinConfigs manifest attribute).");
|
ModdedIrisLog.error("The mixin config was not registered for this loader (fabric.mod.json mixins, neoforge.mods.toml [[mixins]], forge MixinConfigs manifest attribute).");
|
||||||
ModdedIrisLog.error("Entity persistence, custom mob loot, parallel structure safety, or Iris world-type labels are disabled until this is fixed.");
|
ModdedIrisLog.error("Entity persistence, custom mob loot, parallel structure safety, readable biome identifiers, or Iris world-type labels are disabled until this is fixed.");
|
||||||
ModdedIrisLog.error("===============================================================");
|
ModdedIrisLog.error("===============================================================");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-11
@@ -122,17 +122,6 @@ final class ModdedNativeStructurePolicy implements NativeModdedStructureStage.Po
|
|||||||
return ((IrisMaterialPalette) source).get(rng, x, y, z, generator.engine().getData());
|
return ((IrisMaterialPalette) source).get(rng, x, y, z, generator.engine().getData());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public RuntimeException structuresDisabled(int chunkX, int chunkZ) {
|
|
||||||
return new IllegalStateException("Iris cannot generate native structures in chunk "
|
|
||||||
+ chunkX + "," + chunkZ
|
|
||||||
+ " because generate-structures=false disables them outside the pack. That flag is fixed when "
|
|
||||||
+ "the world is created (server.properties generate-structures, or the Generate Structures "
|
|
||||||
+ "toggle in singleplayer), so it cannot be changed for this world: create a new world with "
|
|
||||||
+ "structures enabled, then deny families through importedStructures.disabled or complete keys "
|
|
||||||
+ "through importedStructures.disabledExact");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String generationLabel(String structureId) {
|
public String generationLabel(String structureId) {
|
||||||
return "Iris native structure " + structureId;
|
return "Iris native structure " + structureId;
|
||||||
|
|||||||
+41
-20
@@ -27,16 +27,15 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ConcurrentMap;
|
||||||
|
|
||||||
public final class ModdedPrimaryWorldRouter {
|
public final class ModdedPrimaryWorldRouter {
|
||||||
private static final int TICK_INTERVAL = 20;
|
private static final int TICK_INTERVAL = 20;
|
||||||
|
|
||||||
private static final Set<UUID> routed = ConcurrentHashMap.newKeySet();
|
private static final ConcurrentMap<UUID, CompletableFuture<Boolean>> routes = new ConcurrentHashMap<>();
|
||||||
private static final Set<UUID> inFlight = ConcurrentHashMap.newKeySet();
|
|
||||||
private static int tickCounter = 0;
|
private static int tickCounter = 0;
|
||||||
private static volatile String loginRefusal;
|
private static volatile String loginRefusal;
|
||||||
private static volatile String absentPrimary;
|
private static volatile String absentPrimary;
|
||||||
@@ -45,8 +44,12 @@ public final class ModdedPrimaryWorldRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void clear() {
|
public static void clear() {
|
||||||
routed.clear();
|
routes.forEach((id, route) -> {
|
||||||
inFlight.clear();
|
if (routes.remove(id, route)) {
|
||||||
|
route.cancel(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tickCounter = 0;
|
||||||
loginRefusal = null;
|
loginRefusal = null;
|
||||||
absentPrimary = null;
|
absentPrimary = null;
|
||||||
}
|
}
|
||||||
@@ -57,8 +60,10 @@ public final class ModdedPrimaryWorldRouter {
|
|||||||
*/
|
*/
|
||||||
public static void forget(UUID player) {
|
public static void forget(UUID player) {
|
||||||
if (player != null) {
|
if (player != null) {
|
||||||
routed.remove(player);
|
CompletableFuture<Boolean> route = routes.remove(player);
|
||||||
inFlight.remove(player);
|
if (route != null) {
|
||||||
|
route.cancel(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,36 +115,52 @@ public final class ModdedPrimaryWorldRouter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
server.forEachPlayer(player -> {
|
server.forEachPlayer(player -> routePlayer(server, player, overworld, target, primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void routePlayer(NativeModdedServer server, NativeProtocolPlayer player,
|
||||||
|
NativeWorld overworld, NativeWorld target, String primary) {
|
||||||
UUID id = player.id();
|
UUID id = player.id();
|
||||||
if (routed.contains(id) || !inFlight.add(id)) {
|
if (routes.containsKey(id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
CompletableFuture<Boolean> route = new CompletableFuture<>();
|
||||||
|
if (routes.putIfAbsent(id, route) != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
route.whenComplete((success, failure) -> {
|
||||||
|
if (!Boolean.TRUE.equals(success)) {
|
||||||
|
routes.remove(id, route);
|
||||||
|
}
|
||||||
|
});
|
||||||
if (!player.isInWorld(overworld)) {
|
if (!player.isInWorld(overworld)) {
|
||||||
inFlight.remove(id);
|
route.complete(true);
|
||||||
routed.add(id);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
CompletableFuture<Boolean> teleport = NativeWorldTeleport.teleport(player,
|
CompletableFuture<Boolean> teleport = NativeWorldTeleport.teleport(player,
|
||||||
new NativeWorldTeleport.Destination(server, target, player.x(), Double.MIN_VALUE, player.z(),
|
new NativeWorldTeleport.Destination(server, target, player.x(), Double.MIN_VALUE, player.z(),
|
||||||
ModdedTeleportDeadline.fromNow()));
|
ModdedTeleportDeadline.fromNow()));
|
||||||
teleport.whenComplete((success, failure) -> {
|
route.whenComplete((success, failure) -> {
|
||||||
inFlight.remove(id);
|
if (!Boolean.TRUE.equals(success) && !teleport.isDone()) {
|
||||||
if (Boolean.TRUE.equals(success) && failure == null) {
|
teleport.cancel(false);
|
||||||
routed.add(id);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
teleport.whenComplete((success, failure) -> {
|
||||||
if (failure != null) {
|
if (failure != null) {
|
||||||
|
if (route.completeExceptionally(failure)) {
|
||||||
ModdedIrisLog.error("Iris failed to route player {} to primary world '{}'",
|
ModdedIrisLog.error("Iris failed to route player {} to primary world '{}'",
|
||||||
id, primary, failure);
|
id, primary, failure);
|
||||||
}
|
}
|
||||||
});
|
} else {
|
||||||
} catch (Throwable e) {
|
route.complete(Boolean.TRUE.equals(success));
|
||||||
inFlight.remove(id);
|
|
||||||
ModdedIrisLog.error("Iris failed to route player {} to primary world '{}'", id, primary, e);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
if (route.completeExceptionally(failure)) {
|
||||||
|
ModdedIrisLog.error("Iris failed to route player {} to primary world '{}'", id, primary, failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import art.arcane.iris.platform.protocol.IrisVisionRequestService;
|
|||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.iris.spi.IrisServices;
|
import art.arcane.iris.spi.IrisServices;
|
||||||
import art.arcane.iris.spi.protocol.IrisProtocol;
|
import art.arcane.iris.spi.protocol.IrisProtocol;
|
||||||
|
import art.arcane.iris.modded.service.ModdedPackUpdateService;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -128,6 +129,10 @@ public final class ModdedProtocolHandler {
|
|||||||
}
|
}
|
||||||
String sessionId = sessionId(player);
|
String sessionId = sessionId(player);
|
||||||
ModdedStartup.warnStartupFailuresTo(player);
|
ModdedStartup.warnStartupFailuresTo(player);
|
||||||
|
ModdedPackUpdateService packUpdates = ModdedEngineBootstrap.services().service(ModdedPackUpdateService.class);
|
||||||
|
if (packUpdates != null) {
|
||||||
|
packUpdates.notifyPlayer(player);
|
||||||
|
}
|
||||||
IrisSessionRegistry current = registry;
|
IrisSessionRegistry current = registry;
|
||||||
ModdedProtocolTransport currentTransport = transport;
|
ModdedProtocolTransport currentTransport = transport;
|
||||||
if (current == null || currentTransport == null) {
|
if (current == null || currentTransport == null) {
|
||||||
|
|||||||
@@ -18,10 +18,13 @@
|
|||||||
|
|
||||||
package art.arcane.iris.modded;
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.PackFingerprints;
|
||||||
|
|
||||||
import art.arcane.iris.pack.BrokenPackException;
|
import art.arcane.iris.pack.BrokenPackException;
|
||||||
import art.arcane.iris.pack.PackDirectoryResolver;
|
import art.arcane.iris.pack.PackDirectoryResolver;
|
||||||
import art.arcane.iris.pack.PackDownloader;
|
import art.arcane.iris.pack.PackDownloader;
|
||||||
import art.arcane.iris.pack.PackValidationRegistry;
|
import art.arcane.iris.pack.PackValidationRegistry;
|
||||||
|
import art.arcane.iris.pack.PackValidationCache;
|
||||||
import art.arcane.iris.pack.PackValidationResult;
|
import art.arcane.iris.pack.PackValidationResult;
|
||||||
import art.arcane.iris.pack.PackValidator;
|
import art.arcane.iris.pack.PackValidator;
|
||||||
import art.arcane.iris.modded.command.ModdedPackCommands;
|
import art.arcane.iris.modded.command.ModdedPackCommands;
|
||||||
@@ -31,15 +34,11 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
|||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.attribute.BasicFileAttributes;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
public final class ModdedStartup {
|
public final class ModdedStartup {
|
||||||
private static final int COMPAT_BOOT_KEY_CAP = 3;
|
private static final int COMPAT_BOOT_KEY_CAP = 3;
|
||||||
@@ -60,36 +59,9 @@ public final class ModdedStartup {
|
|||||||
if (!PREPARED.compareAndSet(false, true)) {
|
if (!PREPARED.compareAndSet(false, true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
reportLegacyPacksDirectory();
|
|
||||||
validateAllPacks();
|
validateAllPacks();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Older builds mkdir'd (and stale guidance sometimes populated) config/iris/packs, but modded
|
|
||||||
* packs live under config/irisworldgen/packs. Never auto-move user content: warn loudly when
|
|
||||||
* the legacy directory holds packs, and quietly remove it when it is empty.
|
|
||||||
*/
|
|
||||||
private static void reportLegacyPacksDirectory() {
|
|
||||||
try {
|
|
||||||
File legacy = ModdedEngineBootstrap.loader().configDir().resolve("iris").resolve("packs").toFile();
|
|
||||||
if (!legacy.isDirectory()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!PackDirectoryResolver.listVisiblePackDirectories(legacy).isEmpty()) {
|
|
||||||
File real = art.arcane.iris.spi.IrisPlatforms.get().packsFolderNoCreate();
|
|
||||||
ModdedIrisLog.warn("Iris found packs under the legacy directory {} - modded packs load from {} only. Move them there.",
|
|
||||||
legacy.getAbsolutePath(), real.getAbsolutePath());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String[] entries = legacy.list();
|
|
||||||
if (entries == null || entries.length == 0) {
|
|
||||||
legacy.delete();
|
|
||||||
}
|
|
||||||
} catch (Throwable e) {
|
|
||||||
ModdedIrisLog.debug("Iris legacy packs directory check failed", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Boot trigger for the forced datapack. Runs on its own daemon thread rather than the Iris scheduler:
|
* Boot trigger for the forced datapack. Runs on its own daemon thread rather than the Iris scheduler:
|
||||||
* ModdedEngineBootstrap.start clears the async queue at SERVER_STARTING, which would silently drop this
|
* ModdedEngineBootstrap.start clears the async queue at SERVER_STARTING, which would silently drop this
|
||||||
@@ -140,8 +112,7 @@ public final class ModdedStartup {
|
|||||||
}
|
}
|
||||||
for (File packDir : packDirs) {
|
for (File packDir : packDirs) {
|
||||||
try {
|
try {
|
||||||
PackValidationResult result = PackValidator.validate(packDir);
|
PackValidationResult result = validatePack(packDir);
|
||||||
PackValidationRegistry.publish(result);
|
|
||||||
String minecraftVersion = IrisPlatforms.isBound() ? IrisPlatforms.get().minecraftVersion() : null;
|
String minecraftVersion = IrisPlatforms.isBound() ? IrisPlatforms.get().minecraftVersion() : null;
|
||||||
String compatSummary = PackValidator.compatSummary(result, minecraftVersion);
|
String compatSummary = PackValidator.compatSummary(result, minecraftVersion);
|
||||||
String compatSuffix = compatSummary.isEmpty() ? "" : " " + compatSummary;
|
String compatSuffix = compatSummary.isEmpty() ? "" : " " + compatSummary;
|
||||||
@@ -184,16 +155,15 @@ public final class ModdedStartup {
|
|||||||
throw new BrokenPackException(pack, List.of(
|
throw new BrokenPackException(pack, List.of(
|
||||||
"Pack folder does not exist under " + ModdedPackCommands.packsRoot().getAbsolutePath() + "."));
|
"Pack folder does not exist under " + ModdedPackCommands.packsRoot().getAbsolutePath() + "."));
|
||||||
}
|
}
|
||||||
PackValidationResult cached = PackValidationRegistry.get(pack);
|
Path packRoot = packDir.toPath();
|
||||||
if (cached != null && cached.getValidatedAtMillis() >= newestModificationMillis(packDir.toPath())) {
|
|
||||||
// prepareForStartup already validated this pack and nothing in it changed since; re-validating per
|
|
||||||
// persistent dimension at boot costs a full pack parse each time.
|
|
||||||
return PackValidationRegistry.requireLoadable(pack);
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
PackValidationResult result = PackValidator.validate(packDir);
|
PackValidationResult cached = PackValidationRegistry.getMatching(
|
||||||
PackValidationRegistry.publish(result);
|
packRoot, PackFingerprints.computePackTreeFingerprint(packDir));
|
||||||
return PackValidationRegistry.requireLoadable(pack);
|
if (cached != null) {
|
||||||
|
return PackValidationRegistry.requireLoadable(packRoot);
|
||||||
|
}
|
||||||
|
validatePack(packDir);
|
||||||
|
return PackValidationRegistry.requireLoadable(packRoot);
|
||||||
} catch (BrokenPackException e) {
|
} catch (BrokenPackException e) {
|
||||||
throw e;
|
throw e;
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
@@ -212,6 +182,42 @@ public final class ModdedStartup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static PackValidationResult validatePack(File packDir) {
|
||||||
|
Path packRoot = packDir.toPath();
|
||||||
|
PackValidationRegistry.ValidationTicket ticket = PackValidationRegistry.tryBeginValidation(packRoot);
|
||||||
|
if (ticket == null) {
|
||||||
|
throw new BrokenPackException(packDir.getName(), List.of("Pack content is currently being changed."));
|
||||||
|
}
|
||||||
|
String contentFingerprint = PackFingerprints.computePackTreeFingerprint(packDir);
|
||||||
|
String contextFingerprint = PackValidationCache.contextFingerprint();
|
||||||
|
PackValidationResult result;
|
||||||
|
try {
|
||||||
|
result = PackValidator.validate(packDir);
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
ModdedIrisLog.error("Iris pack validation failed for '{}'", packDir.getName(), failure);
|
||||||
|
String detail = failure.getMessage();
|
||||||
|
if (detail == null || detail.isBlank()) {
|
||||||
|
detail = failure.getClass().getSimpleName();
|
||||||
|
}
|
||||||
|
result = new PackValidationResult(
|
||||||
|
packDir.getName(),
|
||||||
|
List.of("Pack validation failed with " + failure.getClass().getSimpleName() + ": " + detail),
|
||||||
|
List.of(),
|
||||||
|
System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
if (!contentFingerprint.equals(PackFingerprints.computePackTreeFingerprint(packDir))
|
||||||
|
|| !contextFingerprint.equals(PackValidationCache.contextFingerprint())) {
|
||||||
|
PackValidationRegistry.remove(packRoot);
|
||||||
|
PackValidationRegistry.remove(packDir.getName());
|
||||||
|
throw new BrokenPackException(packDir.getName(), List.of("Pack content or validation context changed during validation."));
|
||||||
|
}
|
||||||
|
if (!PackValidationRegistry.publishIfCurrent(ticket, result, contentFingerprint, contextFingerprint)) {
|
||||||
|
throw new BrokenPackException(packDir.getName(), List.of("Pack content changed during validation."));
|
||||||
|
}
|
||||||
|
PackValidationRegistry.publish(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
static void reinjectPersistentDimensions(NativeModdedServer server) {
|
static void reinjectPersistentDimensions(NativeModdedServer server) {
|
||||||
List<ModdedDimensionRegistryStore.PersistentDimension> dimensions =
|
List<ModdedDimensionRegistryStore.PersistentDimension> dimensions =
|
||||||
ModdedDimensionRegistryStore.loadForStartup(server);
|
ModdedDimensionRegistryStore.loadForStartup(server);
|
||||||
@@ -266,23 +272,6 @@ public final class ModdedStartup {
|
|||||||
+ GenerationRefusalNotice.summary(failure));
|
+ GenerationRefusalNotice.summary(failure));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long newestModificationMillis(Path root) {
|
|
||||||
long newest = 0L;
|
|
||||||
try (Stream<Path> walk = Files.walk(root)) {
|
|
||||||
for (Path path : (Iterable<Path>) walk::iterator) {
|
|
||||||
BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class);
|
|
||||||
long modified = attributes.lastModifiedTime().toMillis();
|
|
||||||
if (modified > newest) {
|
|
||||||
newest = modified;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException | RuntimeException unreadable) {
|
|
||||||
ModdedIrisLog.debug("Iris could not stat {} for validation reuse; revalidating", root, unreadable);
|
|
||||||
return Long.MAX_VALUE;
|
|
||||||
}
|
|
||||||
return newest;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True once the first tick with a player list has re-injected the persistent dimensions. Before that a runtime
|
* True once the first tick with a player list has re-injected the persistent dimensions. Before that a runtime
|
||||||
* dimension that is not loaded yet is still on its way.
|
* dimension that is not loaded yet is still on its way.
|
||||||
@@ -296,7 +285,7 @@ public final class ModdedStartup {
|
|||||||
* the console. Tell the operators who can actually act on it when they join.
|
* the console. Tell the operators who can actually act on it when they join.
|
||||||
*/
|
*/
|
||||||
public static void warnStartupFailuresTo(NativeProtocolPlayer player) {
|
public static void warnStartupFailuresTo(NativeProtocolPlayer player) {
|
||||||
if (player == null || !player.isGameMaster()) {
|
if (player == null || (!player.isGameMaster() && !player.isServerOwner())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (String failure : DIMENSION_FAILURES) {
|
for (String failure : DIMENSION_FAILURES) {
|
||||||
|
|||||||
+205
-93
@@ -44,6 +44,8 @@ import art.arcane.iris.pack.value.IrisPosition;
|
|||||||
import art.arcane.iris.pack.value.IrisRange;
|
import art.arcane.iris.pack.value.IrisRange;
|
||||||
import art.arcane.iris.world.entity.IrisSpawnGroup;
|
import art.arcane.iris.world.entity.IrisSpawnGroup;
|
||||||
import art.arcane.iris.world.entity.IrisSpawner;
|
import art.arcane.iris.world.entity.IrisSpawner;
|
||||||
|
import art.arcane.iris.world.entity.EntitySpawnSeed;
|
||||||
|
import art.arcane.iris.world.entity.IrisEntitySpawn.SpawnContext;
|
||||||
import art.arcane.iris.generation.decoration.IrisSurface;
|
import art.arcane.iris.generation.decoration.IrisSurface;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
@@ -56,11 +58,13 @@ import art.arcane.volmlib.util.matter.MatterMarker;
|
|||||||
import art.arcane.volmlib.util.matter.slices.MarkerMatter;
|
import art.arcane.volmlib.util.matter.slices.MarkerMatter;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.concurrent.ArrayBlockingQueue;
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
import java.util.concurrent.BlockingQueue;
|
import java.util.concurrent.BlockingQueue;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
@@ -235,6 +239,10 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
boolean initialSpawnChunk(NativeWorld level, int chunkX, int chunkZ) {
|
boolean initialSpawnChunk(NativeWorld level, int chunkX, int chunkZ) {
|
||||||
|
if (!isEntitySpawningEnabledForCurrentWorld()
|
||||||
|
|| !NativeSpawnQueries.ambientAllowed(level, chunkX, chunkZ, true)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!ModdedEntitySpawner.chunksSafe(entityRuntime(level), chunkX, chunkZ)) {
|
if (!ModdedEntitySpawner.chunksSafe(entityRuntime(level), chunkX, chunkZ)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -249,54 +257,25 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
MantleChunk<Matter> chunk = mantle.useChunk(chunkX, chunkZ);
|
MantleChunk<Matter> chunk = mantle.useChunk(chunkX, chunkZ);
|
||||||
try {
|
try {
|
||||||
List<PreparedMarkerSpawn> markers = markerSystemEnabled()
|
List<PreparedMarkerSpawn> markers = markerSystemEnabled()
|
||||||
? prepareMarkerSpawns(level, chunkX, chunkZ, chunk) : List.of();
|
? prepareMarkerSpawns(level, chunkX, chunkZ, chunk, true) : List.of();
|
||||||
Optional<BiomeEnvironment> environment = ambientSystemEnabled()
|
Optional<BiomeEnvironment> environment = ambientSystemEnabled()
|
||||||
? resolveSurfaceEnvironment(chunkX, chunkZ) : Optional.empty();
|
? resolveSurfaceEnvironment(chunkX, chunkZ) : Optional.empty();
|
||||||
chunk.raiseFlagUnchecked(INITIAL_SPAWN_COMPLETION_FLAG, () -> {
|
if (ambientSystemEnabled() && engine.getComplex() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
spawnPreparedMarkers(level, markers, true);
|
spawnPreparedMarkers(level, markers, true);
|
||||||
if (environment.isPresent()) {
|
if (environment.isPresent()) {
|
||||||
scheduleInitialFollowUp(level, chunkX, chunkZ, environment.get());
|
try (BiomeEnvironment.Scope ignored = engine.openBiomeEnvironmentScope(environment.get())) {
|
||||||
|
spawnAmbient(level, chunkX, chunkZ, true, environment.get());
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
chunk.raiseFlagUnchecked(INITIAL_SPAWN_COMPLETION_FLAG, () -> {});
|
||||||
} finally {
|
} finally {
|
||||||
chunk.release();
|
chunk.release();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void scheduleInitialFollowUp(NativeWorld level, int chunkX, int chunkZ, BiomeEnvironment environment) {
|
|
||||||
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
|
|
||||||
if (scheduler == null) {
|
|
||||||
IrisLogging.error("Iris could not schedule the initial entity-spawn follow-up because the modded scheduler is unavailable.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
scheduler.laterGlobal(
|
|
||||||
() -> EngineLifecycleTasks.run(
|
|
||||||
engine,
|
|
||||||
"modded_world_manager_initial_spawn_followup",
|
|
||||||
() -> runInitialFollowUp(level, chunkX, chunkZ, environment)),
|
|
||||||
RNG.r.i(5, 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void runInitialFollowUp(NativeWorld level, int chunkX, int chunkZ, BiomeEnvironment environment) {
|
|
||||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
|
||||||
if (closed || engine.isClosed() || mantle.isClosed() || !isEntitySpawningEnabledForCurrentWorld()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!level.isChunkLoaded(chunkX, chunkZ) || !mantle.isChunkLoaded(chunkX, chunkZ)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (engine.getComplex() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ambientSystemEnabled()) {
|
|
||||||
try (BiomeEnvironment.Scope ignored = engine.openBiomeEnvironmentScope(environment)) {
|
|
||||||
spawnAmbient(level, chunkX, chunkZ, true, environment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void warmupMantleChunkAsync(long key, int chunkX, int chunkZ) {
|
private void warmupMantleChunkAsync(long key, int chunkX, int chunkZ) {
|
||||||
if (closed || !mantleWarmups.add(key)) {
|
if (closed || !mantleWarmups.add(key)) {
|
||||||
return;
|
return;
|
||||||
@@ -380,11 +359,14 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
if (!mantle.isChunkLoaded(chunkX, chunkZ)) {
|
if (!mantle.isChunkLoaded(chunkX, chunkZ)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!mantle.hasFlag(chunkX, chunkZ, INITIAL_SPAWN_COMPLETION_FLAG)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
MantleChunk<Matter> chunk = mantle.useChunk(chunkX, chunkZ);
|
MantleChunk<Matter> chunk = mantle.useChunk(chunkX, chunkZ);
|
||||||
try {
|
try {
|
||||||
if (markerSystemEnabled()) {
|
if (markerSystemEnabled()) {
|
||||||
spawnPreparedMarkers(level, prepareMarkerSpawns(level, chunkX, chunkZ, chunk), false);
|
spawnPreparedMarkers(level, prepareMarkerSpawns(level, chunkX, chunkZ, chunk, false), false);
|
||||||
}
|
}
|
||||||
if (ambientSystemEnabled()) {
|
if (ambientSystemEnabled()) {
|
||||||
spawnAmbient(level, chunkX, chunkZ, false);
|
spawnAmbient(level, chunkX, chunkZ, false);
|
||||||
@@ -394,7 +376,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<PreparedMarkerSpawn> prepareMarkerSpawns(NativeWorld level, int chunkX, int chunkZ, MantleChunk<Matter> chunk) {
|
private List<PreparedMarkerSpawn> prepareMarkerSpawns(NativeWorld level, int chunkX, int chunkZ, MantleChunk<Matter> chunk, boolean initial) {
|
||||||
int minHeight = engine.getWorld().minHeight();
|
int minHeight = engine.getWorld().minHeight();
|
||||||
KList<IrisPosition> obstructed = new KList<>();
|
KList<IrisPosition> obstructed = new KList<>();
|
||||||
List<PreparedMarkerSpawn> prepared = new ArrayList<>();
|
List<PreparedMarkerSpawn> prepared = new ArrayList<>();
|
||||||
@@ -429,62 +411,71 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
if (spawners.isEmpty()) {
|
if (spawners.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
IrisSpawner chosen = spawners.getRandom();
|
if (initial) {
|
||||||
|
spawners.sort(Comparator.comparing(IrisSpawner::getLoadKey, Comparator.nullsFirst(Comparator.naturalOrder())));
|
||||||
|
}
|
||||||
|
RNG markerRng = initial ? EntitySpawnSeed.marker(engine.getSeedManager().getEntity(), worldX, worldY, worldZ)
|
||||||
|
: new RNG(RNG.r.nextLong());
|
||||||
|
IrisSpawner chosen = spawners.getRandom(markerRng);
|
||||||
if (chosen == null) {
|
if (chosen == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
prepared.add(new PreparedMarkerSpawn(new IrisPosition(worldX, worldY, worldZ), chosen, environment));
|
prepared.add(new PreparedMarkerSpawn(new IrisPosition(worldX, worldY, worldZ), chosen, resolved, markerRng, environment));
|
||||||
});
|
});
|
||||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||||
for (IrisPosition position : obstructed) {
|
for (IrisPosition position : obstructed) {
|
||||||
mantle.remove(position.getX(), position.getY(), position.getZ(), MatterMarker.class);
|
mantle.remove(position.getX(), position.getY(), position.getZ(), MatterMarker.class);
|
||||||
}
|
}
|
||||||
|
if (initial) {
|
||||||
|
prepared.sort(Comparator.comparingInt((PreparedMarkerSpawn marker) -> marker.position().getX())
|
||||||
|
.thenComparingInt(marker -> marker.position().getY())
|
||||||
|
.thenComparingInt(marker -> marker.position().getZ()));
|
||||||
|
}
|
||||||
return List.copyOf(prepared);
|
return List.copyOf(prepared);
|
||||||
}
|
}
|
||||||
|
|
||||||
private KList<IrisSpawner> resolveMarkerSpawners(IrisMarker marker, BiomeEnvironment environment) {
|
private KList<IrisSpawner> resolveMarkerSpawners(IrisMarker marker, BiomeEnvironment environment) {
|
||||||
KList<IrisSpawner> spawners = new KList<>();
|
KList<IrisSpawner> spawners = new KList<>();
|
||||||
|
Set<IrisSpawner> seen = new HashSet<>();
|
||||||
for (String key : marker.getSpawners()) {
|
for (String key : marker.getSpawners()) {
|
||||||
IrisSpawner spawner = environment.data().getSpawnerLoader().load(key);
|
IrisSpawner spawner = environment.data().getSpawnerLoader().load(key);
|
||||||
if (spawner == null) {
|
if (spawner == null) {
|
||||||
IrisLogging.error("Cannot load spawner: " + key + " for marker on " + engine.getName());
|
IrisLogging.error("Cannot load spawner: " + key + " for marker on " + engine.getName());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
spawner.setReferenceMarker(marker);
|
if (!spawner.isCompatExcluded() && seen.add(spawner)) {
|
||||||
spawners.add(spawner);
|
spawners.add(spawner);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return spawners;
|
return spawners;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void spawnPreparedMarkers(NativeWorld level, List<PreparedMarkerSpawn> markers, boolean initial) {
|
private void spawnPreparedMarkers(NativeWorld level, List<PreparedMarkerSpawn> markers, boolean initial) {
|
||||||
for (PreparedMarkerSpawn marker : markers) {
|
for (PreparedMarkerSpawn marker : markers) {
|
||||||
try (BiomeEnvironment.Scope ignored = engine.openBiomeEnvironmentScope(marker.environment())) {
|
try (BiomeEnvironment.Scope ignored = engine.openBiomeEnvironmentScope(marker.environment())) {
|
||||||
spawnFromSpawner(level, marker.position(), marker.spawner(), initial);
|
spawnFromSpawner(level, marker.position(), marker.rng(), new SpawnContext(marker.spawner(), marker.marker(), initial));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void spawnFromSpawner(NativeWorld level, IrisPosition position, IrisSpawner spawner, boolean initial) {
|
private void spawnFromSpawner(NativeWorld level, IrisPosition position, RNG rng, SpawnContext context) {
|
||||||
KList<IrisEntitySpawn> spawns = initial ? spawner.getInitialSpawns() : spawner.getSpawns();
|
IrisSpawner spawner = context.spawner();
|
||||||
|
KList<IrisEntitySpawn> spawns = context.initial() ? spawner.getInitialSpawns() : spawner.getSpawns();
|
||||||
if (spawns.isEmpty()) {
|
if (spawns.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (IrisEntitySpawn entry : spawns) {
|
IrisEntitySpawn chosen = rarityPick(spawns, rng);
|
||||||
entry.setReferenceSpawner(spawner);
|
|
||||||
entry.setReferenceMarker(spawner.getReferenceMarker());
|
|
||||||
}
|
|
||||||
IrisEntitySpawn chosen = rarityPick(spawns);
|
|
||||||
if (chosen == null) {
|
if (chosen == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int chunkX = position.getX() >> 4;
|
int chunkX = position.getX() >> 4;
|
||||||
int chunkZ = position.getZ() >> 4;
|
int chunkZ = position.getZ() >> 4;
|
||||||
if (!canSpawn(spawner, chunkX, chunkZ)) {
|
if (!canSpawn(spawner, chunkX, chunkZ, context.initial())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int spawned = spawnEntryAt(level, chosen, spawner, position);
|
int spawned = spawnEntryAt(level, chosen, position, rng, context);
|
||||||
if (spawned > 0) {
|
if (spawned > 0 && !context.initial()) {
|
||||||
spawner.spawn(engine, chunkX, chunkZ);
|
spawner.spawn(engine, chunkX, chunkZ);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -521,59 +512,168 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
IrisBiome biome = environment.biome();
|
IrisBiome biome = environment.biome();
|
||||||
Map<String, Integer> chunkMobs = NativeSpawnQueries.livingEntityCategories(level, chunkX, chunkZ);
|
Map<String, Integer> chunkMobs = new HashMap<>(initial ? Map.of()
|
||||||
|
: NativeSpawnQueries.livingEntityCategories(level, chunkX, chunkZ));
|
||||||
|
RNG rng = initial ? EntitySpawnSeed.chunk(engine.getSeedManager().getEntity(), chunkX, chunkZ)
|
||||||
|
: new RNG(RNG.r.nextLong());
|
||||||
|
|
||||||
KList<IrisEntitySpawn> pool = new KList<>();
|
KList<SpawnCandidate> pool = new KList<>();
|
||||||
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.dimension().getEntitySpawners()), biome, chunkX, chunkZ, chunkMobs, initial);
|
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.dimension().getEntitySpawners()), biome, chunkX, chunkZ, chunkMobs, initial, false);
|
||||||
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.region().getEntitySpawners()), null, chunkX, chunkZ, chunkMobs, initial);
|
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.region().getEntitySpawners()), null, chunkX, chunkZ, chunkMobs, initial, false);
|
||||||
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(biome.getEntitySpawners()), null, chunkX, chunkZ, chunkMobs, initial);
|
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(biome.getEntitySpawners()), null, chunkX, chunkZ, chunkMobs, initial, false);
|
||||||
|
if (environment.dimension().hasUndergroundSpawners(engine)) {
|
||||||
|
spawnUnderground(level, chunkX, chunkZ, initial, chunkMobs, rng.nextParallelRNG(0x5A17D3B48269C0EFL));
|
||||||
|
}
|
||||||
if (pool.isEmpty()) {
|
if (pool.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
IrisEntitySpawn chosen = rarityPick(pool);
|
SpawnCandidate chosen = rarityPick(pool, rng);
|
||||||
if (chosen == null || chosen.getReferenceSpawner() == null) {
|
if (chosen == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
IrisSpawner spawner = chosen.getReferenceSpawner();
|
IrisSpawner spawner = chosen.spawner();
|
||||||
if (!canSpawn(spawner, chunkX, chunkZ)) {
|
if (!canSpawn(spawner, chunkX, chunkZ, initial)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int spawned = spawnEntry(level, chosen, spawner, chunkX, chunkZ,
|
int spawned = spawnEntry(level, chosen.entry(), chunkX, chunkZ,
|
||||||
remainingCapacity(chosen, spawner, chunkMobs));
|
remainingCapacity(chosen.entry(), spawner, chunkMobs), rng, new SpawnContext(spawner, null, initial));
|
||||||
if (spawned > 0) {
|
recordSpawned(chosen.entry(), chunkMobs, spawned);
|
||||||
|
if (spawned > 0 && !initial) {
|
||||||
spawner.spawn(engine, chunkX, chunkZ);
|
spawner.spawn(engine, chunkX, chunkZ);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void collectSpawns(KList<IrisEntitySpawn> pool, KList<IrisSpawner> spawners, IrisBiome biomeFilter, int chunkX, int chunkZ, Map<String, Integer> chunkMobs, boolean initial) {
|
private void spawnUnderground(NativeWorld level, int chunkX, int chunkZ, boolean initial,
|
||||||
|
Map<String, Integer> chunkMobs, RNG random) {
|
||||||
|
for (IrisSurface surface : List.of(IrisSurface.LAND, IrisSurface.WATER, IrisSurface.LAVA)) {
|
||||||
|
IrisPosition candidate = findUndergroundCandidate(level, chunkX, chunkZ,
|
||||||
|
random.nextParallelRNG(surface.ordinal()), surface);
|
||||||
|
if (candidate == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BiomeEnvironment environment;
|
||||||
|
try {
|
||||||
|
environment = engine.getBiomeOrMantleEnvironment(candidate.getX(),
|
||||||
|
candidate.getY() - engine.getWorld().minHeight(), candidate.getZ());
|
||||||
|
} catch (SavedBiomeUnavailableException unavailable) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try (BiomeEnvironment.Scope ignored = engine.openBiomeEnvironmentScope(environment)) {
|
||||||
|
KList<SpawnCandidate> pool = new KList<>();
|
||||||
|
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.dimension().getEntitySpawners()),
|
||||||
|
environment.biome(), chunkX, chunkZ, chunkMobs, initial, true);
|
||||||
|
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.region().getEntitySpawners()),
|
||||||
|
null, chunkX, chunkZ, chunkMobs, initial, true);
|
||||||
|
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.biome().getEntitySpawners()),
|
||||||
|
null, chunkX, chunkZ, chunkMobs, initial, true);
|
||||||
|
pool.removeIf(selection -> !matchesUndergroundSurface(selection.entry(), surface));
|
||||||
|
SpawnCandidate selected = rarityPick(pool, random);
|
||||||
|
if (selected == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int capacity = remainingCapacity(selected.entry(), selected.spawner(), chunkMobs);
|
||||||
|
int spawned = spawnUndergroundEntry(level, candidate, selected, capacity, random, initial);
|
||||||
|
recordSpawned(selected.entry(), chunkMobs, spawned);
|
||||||
|
if (spawned > 0 && !initial) {
|
||||||
|
selected.spawner().spawn(engine, chunkX, chunkZ);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean matchesUndergroundSurface(IrisEntitySpawn entry, IrisSurface surface) {
|
||||||
|
IrisEntity entity = entry.getRealEntity(engine);
|
||||||
|
return entity != null && entity.getSurface().isFluid() == surface.isFluid()
|
||||||
|
&& (!surface.isFluid() || entity.getSurface() == surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
static IrisPosition findUndergroundCandidate(NativeWorld level, int chunkX, int chunkZ, RNG random, IrisSurface surface) {
|
||||||
|
int minimumY = level.minHeight() + 1;
|
||||||
|
for (int attempt = 0; attempt < 8; attempt++) {
|
||||||
|
int x = (chunkX << 4) + random.i(1, 15);
|
||||||
|
int z = (chunkZ << 4) + random.i(1, 15);
|
||||||
|
int maximumY = Math.min(level.maxHeight() - 3, NativeSpawnQueries.surfaceHeight(level, x, z, true) - 1);
|
||||||
|
if (maximumY < minimumY) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int span = maximumY - minimumY + 1;
|
||||||
|
int startY = LootResolver.inclusive(random, minimumY, maximumY);
|
||||||
|
for (int sample = 0; sample < Math.min(span, 128); sample++) {
|
||||||
|
int y = minimumY + Math.floorMod(startY - minimumY - sample, span);
|
||||||
|
NativeBlockState state = level.getBlock(x, y, z);
|
||||||
|
boolean acceptable = surface.isFluid() ? matchesSurface(surface, state)
|
||||||
|
: state != null && state.isAir() && NativeSpawnQueries.solid(level.getBlock(x, y - 1, z))
|
||||||
|
&& level.getBlock(x, y + 1, z).isAir();
|
||||||
|
if (acceptable) {
|
||||||
|
return new IrisPosition(x, y, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int spawnUndergroundEntry(NativeWorld level, IrisPosition candidate, SpawnCandidate selection,
|
||||||
|
int capacity, RNG random, boolean initial) {
|
||||||
|
IrisEntity entity = selection.entry().getRealEntity(engine);
|
||||||
|
if (entity == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int x = candidate.getX();
|
||||||
|
int y = candidate.getY();
|
||||||
|
int z = candidate.getZ();
|
||||||
|
int count = Math.min(Math.max(0, capacity), LootResolver.inclusive(random,
|
||||||
|
selection.entry().getMinSpawns(), selection.entry().getMaxSpawns()));
|
||||||
|
int spawned = 0;
|
||||||
|
for (int ordinal = 0; ordinal < count; ordinal++) {
|
||||||
|
if (!initial && !lightAllowed(selection.spawner(), level, x, y, z)
|
||||||
|
|| !surfaceMatches(entity.getSurface(), level, x, y, z)
|
||||||
|
|| !ModdedEntitySpawner.isAreaClearForSpawn(entityRuntime(level), entity, x, y, z)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ModdedEntitySpawner.spawn(engine, entity, entityRuntime(level), x, y, z,
|
||||||
|
EntitySpawnSeed.entity(random.getSeed(), ordinal)) != null) {
|
||||||
|
spawned++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return spawned;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void collectSpawns(KList<SpawnCandidate> pool, KList<IrisSpawner> spawners, IrisBiome biomeFilter, int chunkX, int chunkZ, Map<String, Integer> chunkMobs, boolean initial, boolean underground) {
|
||||||
for (IrisSpawner spawner : spawners) {
|
for (IrisSpawner spawner : spawners) {
|
||||||
if (spawner == null) {
|
if (spawner == null || (spawner.getGroup() == IrisSpawnGroup.CAVE) != underground) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (biomeFilter != null && !spawner.isValid(biomeFilter)) {
|
if (biomeFilter != null && !spawner.isValid(biomeFilter)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!canSpawn(spawner, chunkX, chunkZ)) {
|
if (!canSpawn(spawner, chunkX, chunkZ, initial)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
KList<IrisEntitySpawn> spawns = initial ? spawner.getInitialSpawns() : spawner.getSpawns();
|
KList<IrisEntitySpawn> spawns = initial ? spawner.getInitialSpawns() : spawner.getSpawns();
|
||||||
for (IrisEntitySpawn entry : spawns) {
|
for (IrisEntitySpawn entry : spawns) {
|
||||||
entry.setReferenceSpawner(spawner);
|
|
||||||
entry.setReferenceMarker(spawner.getReferenceMarker());
|
|
||||||
if (remainingCapacity(entry, spawner, chunkMobs) > 0) {
|
if (remainingCapacity(entry, spawner, chunkMobs) > 0) {
|
||||||
pool.add(entry);
|
pool.add(new SpawnCandidate(entry, spawner));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void recordSpawned(IrisEntitySpawn entry, Map<String, Integer> counts, int spawned) {
|
||||||
|
IrisEntity entity = entry.getRealEntity(engine);
|
||||||
|
if (spawned > 0 && entity != null) {
|
||||||
|
counts.merge(entity.spawnCategory(), spawned, Integer::sum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private int remainingCapacity(IrisEntitySpawn entry, IrisSpawner spawner, Map<String, Integer> counts) {
|
private int remainingCapacity(IrisEntitySpawn entry, IrisSpawner spawner, Map<String, Integer> counts) {
|
||||||
IrisEntity entity = entry.getRealEntity(engine);
|
IrisEntity entity = entry.getRealEntity(engine);
|
||||||
return spawner.remainingCapacity(entity, counts);
|
return spawner.remainingCapacity(entity, counts);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int spawnEntry(NativeWorld level, IrisEntitySpawn entry, IrisSpawner spawner, int chunkX, int chunkZ, int remainingCapacity) {
|
private int spawnEntry(NativeWorld level, IrisEntitySpawn entry, int chunkX, int chunkZ,
|
||||||
|
int remainingCapacity, RNG rng, SpawnContext context) {
|
||||||
IrisEntity irisEntity = entry.getRealEntity(engine);
|
IrisEntity irisEntity = entry.getRealEntity(engine);
|
||||||
if (irisEntity == null) {
|
if (irisEntity == null) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -581,18 +681,24 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
|
|
||||||
int min = entry.getMinSpawns();
|
int min = entry.getMinSpawns();
|
||||||
int max = entry.getMaxSpawns();
|
int max = entry.getMaxSpawns();
|
||||||
int count = Math.min(Math.max(0, remainingCapacity), LootResolver.inclusive(RNG.r, min, max));
|
int count = Math.min(Math.max(0, remainingCapacity), LootResolver.inclusive(rng, min, max));
|
||||||
if (count <= 0) {
|
if (count <= 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
RNG entityRng = entry.getRng().aquire(() -> new RNG(engine.getSeedManager().getEntity()));
|
IrisSpawner spawner = context.spawner();
|
||||||
IrisSpawnGroup group = spawner.getGroup();
|
IrisSpawnGroup group = spawner.getGroup();
|
||||||
KList<IrisPosition> caveFloors = group == IrisSpawnGroup.CAVE
|
KList<IrisPosition> caveFloors = group == IrisSpawnGroup.CAVE
|
||||||
? engine.getMantle().findMarkers(chunkX, chunkZ, MarkerMatter.CAVE_FLOOR)
|
? engine.getMantle().findMarkers(chunkX, chunkZ, MarkerMatter.CAVE_FLOOR)
|
||||||
: new KList<>();
|
: new KList<>();
|
||||||
|
if (context.initial()) {
|
||||||
|
caveFloors.sort(Comparator.comparingInt(IrisPosition::getX)
|
||||||
|
.thenComparingInt(IrisPosition::getY).thenComparingInt(IrisPosition::getZ));
|
||||||
|
}
|
||||||
int spawned = 0;
|
int spawned = 0;
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
|
RNG entityRng = EntitySpawnSeed.entity(rng.getSeed(), i);
|
||||||
|
RNG positionRng = entityRng.nextParallelRNG(0x632BE59BD9B4E019L);
|
||||||
int worldX;
|
int worldX;
|
||||||
int worldY;
|
int worldY;
|
||||||
int worldZ;
|
int worldZ;
|
||||||
@@ -600,16 +706,16 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
if (caveFloors.isEmpty()) {
|
if (caveFloors.isEmpty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
IrisPosition caveFloor = caveFloors.getRandom(RNG.r);
|
IrisPosition caveFloor = caveFloors.getRandom(positionRng);
|
||||||
worldX = caveFloor.getX();
|
worldX = caveFloor.getX();
|
||||||
worldY = caveFloor.getY();
|
worldY = caveFloor.getY();
|
||||||
worldZ = caveFloor.getZ();
|
worldZ = caveFloor.getZ();
|
||||||
} else {
|
} else {
|
||||||
worldX = (chunkX << 4) + RNG.r.i(16);
|
worldX = (chunkX << 4) + positionRng.i(16);
|
||||||
worldZ = (chunkZ << 4) + RNG.r.i(16);
|
worldZ = (chunkZ << 4) + positionRng.i(16);
|
||||||
int surfaceY = NativeSpawnQueries.surfaceHeight(level, worldX, worldZ, false);
|
int surfaceY = NativeSpawnQueries.surfaceHeight(level, worldX, worldZ, false);
|
||||||
int solidY = NativeSpawnQueries.surfaceHeight(level, worldX, worldZ, true);
|
int solidY = NativeSpawnQueries.surfaceHeight(level, worldX, worldZ, true);
|
||||||
Integer selectedY = IrisEntitySpawn.selectSurfaceSpawnY(group, irisEntity.getSurface(), solidY, surfaceY, RNG.r);
|
Integer selectedY = IrisEntitySpawn.selectSurfaceSpawnY(group, irisEntity.getSurface(), solidY, surfaceY, positionRng);
|
||||||
if (selectedY == null) {
|
if (selectedY == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -619,7 +725,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Rarity is applied exactly once, as pool weighting in rarityPick - never re-rolled per position (Bukkit parity).
|
// Rarity is applied exactly once, as pool weighting in rarityPick - never re-rolled per position (Bukkit parity).
|
||||||
if (!lightAllowed(spawner, level, worldX, worldY, worldZ)) {
|
if (!context.initial() && !lightAllowed(spawner, level, worldX, worldY, worldZ)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!surfaceMatches(irisEntity.getSurface(), level, worldX, worldY, worldZ)) {
|
if (!surfaceMatches(irisEntity.getSurface(), level, worldX, worldY, worldZ)) {
|
||||||
@@ -635,7 +741,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
return spawned;
|
return spawned;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int spawnEntryAt(NativeWorld level, IrisEntitySpawn entry, IrisSpawner spawner, IrisPosition position) {
|
private int spawnEntryAt(NativeWorld level, IrisEntitySpawn entry, IrisPosition position, RNG rng, SpawnContext context) {
|
||||||
IrisEntity irisEntity = entry.getRealEntity(engine);
|
IrisEntity irisEntity = entry.getRealEntity(engine);
|
||||||
if (irisEntity == null) {
|
if (irisEntity == null) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -643,21 +749,21 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
|
|
||||||
int min = entry.getMinSpawns();
|
int min = entry.getMinSpawns();
|
||||||
int max = entry.getMaxSpawns();
|
int max = entry.getMaxSpawns();
|
||||||
int count = LootResolver.inclusive(RNG.r, min, max);
|
int count = LootResolver.inclusive(rng, min, max);
|
||||||
if (count <= 0) {
|
if (count <= 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
exhaustMarker(spawner, position);
|
exhaustMarker(context.marker(), position, EntitySpawnSeed.entity(rng.getSeed(), -1));
|
||||||
|
|
||||||
RNG entityRng = entry.getRng().aquire(() -> new RNG(engine.getSeedManager().getEntity()));
|
|
||||||
int worldX = position.getX();
|
int worldX = position.getX();
|
||||||
int worldY = position.getY() + 1;
|
int worldY = position.getY() + 1;
|
||||||
int worldZ = position.getZ();
|
int worldZ = position.getZ();
|
||||||
int spawned = 0;
|
int spawned = 0;
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
|
RNG entityRng = EntitySpawnSeed.entity(rng.getSeed(), i);
|
||||||
// Rarity is applied exactly once, as pool weighting in rarityPick - never re-rolled per position (Bukkit parity).
|
// Rarity is applied exactly once, as pool weighting in rarityPick - never re-rolled per position (Bukkit parity).
|
||||||
if (!lightAllowed(spawner, level, worldX, worldY, worldZ)) {
|
if (!context.initial() && !lightAllowed(context.spawner(), level, worldX, worldY, worldZ)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (irisEntity.getSurface().isFluid()
|
if (irisEntity.getSurface().isFluid()
|
||||||
@@ -672,16 +778,15 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
return spawned;
|
return spawned;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void exhaustMarker(IrisSpawner spawner, IrisPosition position) {
|
private void exhaustMarker(IrisMarker marker, IrisPosition position, RNG rng) {
|
||||||
IrisMarker marker = spawner.getReferenceMarker();
|
if (marker == null || !marker.shouldExhaust(rng)) {
|
||||||
if (marker == null || !marker.shouldExhaust()) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
engine.getMantle().getMantle().remove(position.getX(), position.getY() - engine.getWorld().minHeight(), position.getZ(), MatterMarker.class);
|
engine.getMantle().getMantle().remove(position.getX(), position.getY() - engine.getWorld().minHeight(), position.getZ(), MatterMarker.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean canSpawn(IrisSpawner spawner, int chunkX, int chunkZ) {
|
private boolean canSpawn(IrisSpawner spawner, int chunkX, int chunkZ, boolean initial) {
|
||||||
return spawner.canSpawn(engine, chunkX, chunkZ);
|
return !spawner.isCompatExcluded() && (initial || spawner.canSpawn(engine, chunkX, chunkZ));
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean lightAllowed(IrisSpawner spawner, NativeWorld level, int worldX, int worldY, int worldZ) {
|
private boolean lightAllowed(IrisSpawner spawner, NativeWorld level, int worldX, int worldY, int worldZ) {
|
||||||
@@ -796,9 +901,9 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
return !studio || studioSetting;
|
return !studio || studioSetting;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IrisEntitySpawn rarityPick(KList<IrisEntitySpawn> entries) {
|
private static <T extends Rarity> T rarityPick(KList<T> entries, RNG rng) {
|
||||||
KList<IrisEntitySpawn> weighted = Rarity.expandWeighted(entries);
|
KList<T> weighted = Rarity.expandWeighted(entries);
|
||||||
return weighted.isEmpty() ? entries.getRandom() : weighted.getRandom();
|
return weighted.isEmpty() ? entries.getRandom(rng) : weighted.getRandom(rng);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long pack(int x, int z) {
|
private static long pack(int x, int z) {
|
||||||
@@ -893,7 +998,14 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
return failure;
|
return failure;
|
||||||
}
|
}
|
||||||
|
|
||||||
private record PreparedMarkerSpawn(IrisPosition position, IrisSpawner spawner, BiomeEnvironment environment) {
|
private record PreparedMarkerSpawn(IrisPosition position, IrisSpawner spawner, IrisMarker marker, RNG rng, BiomeEnvironment environment) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record SpawnCandidate(IrisEntitySpawn entry, IrisSpawner spawner) implements Rarity {
|
||||||
|
@Override
|
||||||
|
public int getRarity() {
|
||||||
|
return entry.getRarity();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private NativeEntityRuntime entityRuntime(NativeWorld level) {
|
private NativeEntityRuntime entityRuntime(NativeWorld level) {
|
||||||
|
|||||||
+7
@@ -33,6 +33,7 @@ import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
|||||||
import art.arcane.iris.modded.ModdedDimensionManager;
|
import art.arcane.iris.modded.ModdedDimensionManager;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldTeleport;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldTeleport;
|
||||||
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
||||||
|
import art.arcane.iris.modded.service.ModdedPackUpdateService;
|
||||||
import art.arcane.iris.modded.ModdedForcedDatapack;
|
import art.arcane.iris.modded.ModdedForcedDatapack;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedLoader;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedLoader;
|
||||||
import art.arcane.iris.modded.ModdedScheduler;
|
import art.arcane.iris.modded.ModdedScheduler;
|
||||||
@@ -445,6 +446,12 @@ public final class IrisModdedCommands {
|
|||||||
);
|
);
|
||||||
String completionMessage = downloadCompletionMessage(result);
|
String completionMessage = downloadCompletionMessage(result);
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
|
if (result.changed()) {
|
||||||
|
ModdedPackUpdateService updates = ModdedEngineBootstrap.services().service(ModdedPackUpdateService.class);
|
||||||
|
if (updates != null) {
|
||||||
|
dispatchDownloadFeedback(source, updates::refresh);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (completionMessage != null) {
|
if (completionMessage != null) {
|
||||||
dispatchDownloadFeedback(source, () -> ok(source, completionMessage));
|
dispatchDownloadFeedback(source, () -> ok(source, completionMessage));
|
||||||
}
|
}
|
||||||
|
|||||||
+92
-7
@@ -31,6 +31,9 @@ import art.arcane.iris.generation.runtime.GenerationSessionException;
|
|||||||
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
||||||
import art.arcane.iris.structure.placement.IrisStructureLocator;
|
import art.arcane.iris.structure.placement.IrisStructureLocator;
|
||||||
import art.arcane.iris.generation.locator.Locator;
|
import art.arcane.iris.generation.locator.Locator;
|
||||||
|
import art.arcane.iris.generation.locator.BiomeLocator;
|
||||||
|
import art.arcane.iris.generation.subterrain.SubterrainPosition;
|
||||||
|
import art.arcane.iris.world.history.SavedBiomeRuntime;
|
||||||
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
|
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
|
||||||
import art.arcane.iris.structure.placement.StructureReachability;
|
import art.arcane.iris.structure.placement.StructureReachability;
|
||||||
import art.arcane.iris.generation.runtime.WrongEngineBroException;
|
import art.arcane.iris.generation.runtime.WrongEngineBroException;
|
||||||
@@ -82,7 +85,7 @@ final class ModdedLocateCommands {
|
|||||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_BIOME_2, MessageArgument.untrusted("key", key)));
|
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_BIOME_2, MessageArgument.untrusted("key", key)));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
locate(source, level, engine, player, Locator.surfaceBiome(biome.getLoadKey()), "biome " + biome.getLoadKey());
|
locate(source, level, engine, player, BiomeLocator.forBiome(engine, biome.getLoadKey(), player.blockY()), "biome " + biome.getLoadKey());
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,12 +548,12 @@ final class ModdedLocateCommands {
|
|||||||
previous.cancel(true);
|
previous.cancel(true);
|
||||||
}
|
}
|
||||||
search.whenComplete((Position2 at, Throwable error) -> completeLocate(
|
search.whenComplete((Position2 at, Throwable error) -> completeLocate(
|
||||||
source, level, engine, player, label, server, playerId, search, at, error));
|
source, level, engine, player, label, server, playerId, search, locator, at, error));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void completeLocate(NativeCommandSource source, NativeWorld level, Engine engine,
|
private static void completeLocate(NativeCommandSource source, NativeWorld level, Engine engine,
|
||||||
NativeProtocolPlayer player, String label, NativeModdedServer server, UUID playerId,
|
NativeProtocolPlayer player, String label, NativeModdedServer server, UUID playerId,
|
||||||
CompletableFuture<Position2> search, Position2 at, Throwable error) {
|
CompletableFuture<Position2> search, Locator<?> locator, Position2 at, Throwable error) {
|
||||||
if (ACTIVE_LOCATE_REQUESTS.get(playerId) != search) {
|
if (ACTIVE_LOCATE_REQUESTS.get(playerId) != search) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -576,15 +579,77 @@ final class ModdedLocateCommands {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (locator instanceof BiomeLocator caveLocator) {
|
||||||
|
prepareCaveLocate(new CaveLocateRequest(source, level, engine, player, label, server, playerId,
|
||||||
|
search, caveLocator, at));
|
||||||
|
return;
|
||||||
|
}
|
||||||
server.execute(() -> {
|
server.execute(() -> {
|
||||||
if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) {
|
if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) {
|
||||||
teleportToLocateResult(source, level, engine, player, label, at);
|
teleportToLocateResult(source, level, engine, player, label, locator, at, Optional.empty());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void teleportToLocateResult(NativeCommandSource source, NativeWorld level, Engine engine,
|
private static void prepareCaveLocate(CaveLocateRequest request) {
|
||||||
NativeProtocolPlayer player, String label, Position2 at) {
|
CompletableFuture.supplyAsync(() -> resolveCavePosition(request))
|
||||||
|
.whenComplete((target, error) -> request.server().execute(() -> loadCaveDestination(request, target, error)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SubterrainPosition resolveCavePosition(CaveLocateRequest request) {
|
||||||
|
try (GenerationSessionLease lease = request.engine().acquireGenerationLease("modded_cave_position");
|
||||||
|
IrisContext.Scope ignored = IrisContext.open(request.engine(), lease.sessionId(), null)) {
|
||||||
|
return request.locator().position(request.engine(), request.at());
|
||||||
|
} catch (GenerationSessionException error) {
|
||||||
|
throw new CompletionException(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void loadCaveDestination(CaveLocateRequest request, SubterrainPosition target, Throwable error) {
|
||||||
|
if (ACTIVE_LOCATE_REQUESTS.get(request.playerId()) != request.search()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (error != null) {
|
||||||
|
failCaveLocate(request, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (target == null || !request.player().connected() || !request.player().inWorld(request.level())) {
|
||||||
|
if (ACTIVE_LOCATE_REQUESTS.remove(request.playerId(), request.search())) {
|
||||||
|
IrisModdedCommands.fail(request.source(), "The cave destination is no longer available.");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
request.level().getBlock(target.x(), target.y(), target.z());
|
||||||
|
request.locator().prepareRead(request.engine(), new Position2(target.x() >> 4, target.z() >> 4))
|
||||||
|
.whenComplete((read, failure) -> request.server().execute(() -> finishCaveLocate(request, target, read, failure)));
|
||||||
|
} catch (RuntimeException failure) {
|
||||||
|
failCaveLocate(request, failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void finishCaveLocate(CaveLocateRequest request, SubterrainPosition target,
|
||||||
|
Optional<SavedBiomeRuntime.ReadSession> read, Throwable error) {
|
||||||
|
if (error != null) {
|
||||||
|
failCaveLocate(request, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ACTIVE_LOCATE_REQUESTS.remove(request.playerId(), request.search())) {
|
||||||
|
teleportToLocateResult(request.source(), request.level(), request.engine(), request.player(),
|
||||||
|
request.label(), request.locator(), request.at(), Optional.of(new CaveDestination(target, read)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void failCaveLocate(CaveLocateRequest request, Throwable error) {
|
||||||
|
ModdedIrisLog.error("Iris cave locate failed for {}", request.label(), error);
|
||||||
|
if (ACTIVE_LOCATE_REQUESTS.remove(request.playerId(), request.search())) {
|
||||||
|
IrisModdedCommands.fail(request.source(), "Could not prepare the cave destination. See the server log for details.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void teleportToLocateResult(NativeCommandSource source, NativeWorld level, Engine engine,
|
||||||
|
NativeProtocolPlayer player, String label, Locator<?> locator, Position2 at,
|
||||||
|
Optional<CaveDestination> caveDestination) {
|
||||||
// Same liveness guards the structure completion path has: the search can take up to
|
// Same liveness guards the structure completion path has: the search can take up to
|
||||||
// two minutes, and the captured NativeProtocolPlayer may be gone or elsewhere by then.
|
// two minutes, and the captured NativeProtocolPlayer may be gone or elsewhere by then.
|
||||||
if (!player.connected()) {
|
if (!player.connected()) {
|
||||||
@@ -600,6 +665,17 @@ final class ModdedLocateCommands {
|
|||||||
try (GenerationSessionLease lease = engine.acquireGenerationLease("modded_locator_teleport");
|
try (GenerationSessionLease lease = engine.acquireGenerationLease("modded_locator_teleport");
|
||||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
int blockY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2;
|
int blockY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2;
|
||||||
|
if (locator instanceof BiomeLocator caveLocator) {
|
||||||
|
CaveDestination prepared = caveDestination.orElseThrow();
|
||||||
|
SubterrainPosition landing = caveLocator.landing(engine, level, prepared.target(), prepared.read());
|
||||||
|
if (landing == null) {
|
||||||
|
IrisModdedCommands.fail(source, "No safe cave landing found for " + label + ".");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
blockX = landing.x();
|
||||||
|
blockY = landing.y();
|
||||||
|
blockZ = landing.z();
|
||||||
|
}
|
||||||
boolean teleported = player.teleport(level, blockX + 0.5D, blockY, blockZ + 0.5D);
|
boolean teleported = player.teleport(level, blockX + 0.5D, blockY, blockZ + 0.5D);
|
||||||
if (!teleported) {
|
if (!teleported) {
|
||||||
IrisModdedCommands.fail(source, IrisLanguage.plain(
|
IrisModdedCommands.fail(source, IrisLanguage.plain(
|
||||||
@@ -611,7 +687,8 @@ final class ModdedLocateCommands {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORTED_AT_2, MessageArgument.untrusted("label", label), MessageArgument.untrusted("blockX", blockX), MessageArgument.untrusted("blockY", blockY), MessageArgument.untrusted("blockZ", blockZ)));
|
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORTED_AT_2, MessageArgument.untrusted("label", label), MessageArgument.untrusted("blockX", blockX), MessageArgument.untrusted("blockY", blockY), MessageArgument.untrusted("blockZ", blockZ)));
|
||||||
} catch (GenerationSessionException e) {
|
} catch (GenerationSessionException | RuntimeException e) {
|
||||||
|
ModdedIrisLog.error("Iris locate teleport failed for {}", label, e);
|
||||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_ENGINE_CHANGED_WHILE_LOCATING_TRY_AGAIN, MessageArgument.untrusted("label", label)));
|
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_ENGINE_CHANGED_WHILE_LOCATING_TRY_AGAIN, MessageArgument.untrusted("label", label)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -635,6 +712,14 @@ final class ModdedLocateCommands {
|
|||||||
NO_PLACEMENT
|
NO_PLACEMENT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
record CaveDestination(SubterrainPosition target, Optional<SavedBiomeRuntime.ReadSession> read) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record CaveLocateRequest(NativeCommandSource source, NativeWorld level, Engine engine,
|
||||||
|
NativeProtocolPlayer player, String label, NativeModdedServer server,
|
||||||
|
UUID playerId, CompletableFuture<Position2> search, BiomeLocator locator, Position2 at) {
|
||||||
|
}
|
||||||
|
|
||||||
private record NativeStructureTarget(String key, NativeStructureQueries.Reference holder,
|
private record NativeStructureTarget(String key, NativeStructureQueries.Reference holder,
|
||||||
NativeStructureAvailability availability) {
|
NativeStructureAvailability availability) {
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -18,6 +18,8 @@
|
|||||||
|
|
||||||
package art.arcane.iris.modded.command;
|
package art.arcane.iris.modded.command;
|
||||||
|
|
||||||
|
import art.arcane.iris.modded.ModdedStartup;
|
||||||
|
|
||||||
import art.arcane.iris.modded.ModdedIrisLog;
|
import art.arcane.iris.modded.ModdedIrisLog;
|
||||||
import art.arcane.iris.pack.validation.PackCompatReport;
|
import art.arcane.iris.pack.validation.PackCompatReport;
|
||||||
import art.arcane.iris.spi.IrisPlatforms;
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
@@ -25,7 +27,6 @@ import art.arcane.iris.pack.PackDirectoryResolver;
|
|||||||
import art.arcane.iris.pack.PackResourceCleanup;
|
import art.arcane.iris.pack.PackResourceCleanup;
|
||||||
import art.arcane.iris.pack.PackValidationRegistry;
|
import art.arcane.iris.pack.PackValidationRegistry;
|
||||||
import art.arcane.iris.pack.PackValidationResult;
|
import art.arcane.iris.pack.PackValidationResult;
|
||||||
import art.arcane.iris.pack.PackValidator;
|
|
||||||
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
||||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||||
@@ -138,8 +139,7 @@ public final class ModdedPackCommands {
|
|||||||
int broken = 0;
|
int broken = 0;
|
||||||
for (File target : targets) {
|
for (File target : targets) {
|
||||||
try {
|
try {
|
||||||
PackValidationResult result = PackValidator.validate(target);
|
PackValidationResult result = ModdedStartup.validatePack(target);
|
||||||
PackValidationRegistry.publish(result);
|
|
||||||
if (!result.isLoadable()) {
|
if (!result.isLoadable()) {
|
||||||
broken++;
|
broken++;
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-3
@@ -22,6 +22,7 @@ import art.arcane.iris.modded.ModdedIrisLog;
|
|||||||
import art.arcane.volmlib.nativelib.terrain.NativePregenRuntime;
|
import art.arcane.volmlib.nativelib.terrain.NativePregenRuntime;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativePregenRuntime.ChunkLoad;
|
import art.arcane.volmlib.nativelib.terrain.NativePregenRuntime.ChunkLoad;
|
||||||
import art.arcane.iris.configuration.IrisSettings;
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
|
import art.arcane.iris.world.pregen.MantleHeapPressure;
|
||||||
import art.arcane.iris.world.pregen.PregenListener;
|
import art.arcane.iris.world.pregen.PregenListener;
|
||||||
import art.arcane.iris.world.pregen.PregenMantleBackpressure;
|
import art.arcane.iris.world.pregen.PregenMantleBackpressure;
|
||||||
import art.arcane.iris.world.pregen.PregeneratorMethod;
|
import art.arcane.iris.world.pregen.PregeneratorMethod;
|
||||||
@@ -74,8 +75,8 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
|||||||
sync = configuration.sync();
|
sync = configuration.sync();
|
||||||
pauseGuard = new PauseWhenEmptyGuard(runtime);
|
pauseGuard = new PauseWhenEmptyGuard(runtime);
|
||||||
IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen();
|
IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen();
|
||||||
this.maxInFlight = Math.max(8, pregen.getModdedPregenInFlight());
|
this.maxInFlight = memoryLimitedConcurrency(pregen.getModdedPregenInFlight(), MantleHeapPressure.memoryBudgetBytes());
|
||||||
this.minInFlight = Math.max(4, Math.min(16, maxInFlight / 4));
|
this.minInFlight = Math.min(maxInFlight, Math.max(4, Math.min(16, maxInFlight / 4)));
|
||||||
this.semaphore = new Semaphore(maxInFlight, true);
|
this.semaphore = new Semaphore(maxInFlight, true);
|
||||||
this.adaptiveLimit = new AtomicInteger(sync ? 1 : maxInFlight);
|
this.adaptiveLimit = new AtomicInteger(sync ? 1 : maxInFlight);
|
||||||
this.timeoutSeconds = Math.max(120, pregen.getChunkLoadTimeoutSeconds());
|
this.timeoutSeconds = Math.max(120, pregen.getChunkLoadTimeoutSeconds());
|
||||||
@@ -86,7 +87,13 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
|||||||
pregen.getMantleBackpressureTimeoutMs(),
|
pregen.getMantleBackpressureTimeoutMs(),
|
||||||
() -> {
|
() -> {
|
||||||
},
|
},
|
||||||
() -> "dim=" + runtime.worldIdentity());
|
() -> "dim=" + runtime.worldIdentity(),
|
||||||
|
this::admissionCancelled);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int memoryLimitedConcurrency(int configuredLimit, long memoryBudgetBytes) {
|
||||||
|
long memoryLimit = Math.max(1L, memoryBudgetBytes / (256L * 1024L * 1024L));
|
||||||
|
return (int) Math.min(Math.max(1, configuredLimit), memoryLimit);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -359,12 +366,26 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
|||||||
}
|
}
|
||||||
listener.onChunkGenerating(x, z);
|
listener.onChunkGenerating(x, z);
|
||||||
try {
|
try {
|
||||||
|
while (true) {
|
||||||
synchronized (permitMonitor) {
|
synchronized (permitMonitor) {
|
||||||
while (inFlight.get() >= adaptiveLimit.get()) {
|
while (inFlight.get() >= adaptiveLimit.get()) {
|
||||||
|
if (admissionCancelled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
permitMonitor.wait(500L);
|
permitMonitor.wait(500L);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
semaphore.acquire();
|
semaphore.acquire();
|
||||||
|
if (admissionCancelled()) {
|
||||||
|
semaphore.release();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!MantleHeapPressure.overHighWater()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
semaphore.release();
|
||||||
|
backpressure.awaitHeapHeadroom();
|
||||||
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
return;
|
return;
|
||||||
@@ -400,6 +421,10 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean admissionCancelled() {
|
||||||
|
return Thread.currentThread().isInterrupted() || !runtime.isRunning();
|
||||||
|
}
|
||||||
|
|
||||||
private void markSubmitted() {
|
private void markSubmitted() {
|
||||||
int current = inFlight.incrementAndGet();
|
int current = inFlight.incrementAndGet();
|
||||||
inFlightPeak.accumulateAndGet(current, Math::max);
|
inFlightPeak.accumulateAndGet(current, Math::max);
|
||||||
|
|||||||
+138
@@ -0,0 +1,138 @@
|
|||||||
|
package art.arcane.iris.modded.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.diagnostics.splash.IrisSplashPackScanner;
|
||||||
|
import art.arcane.iris.diagnostics.splash.IrisSplashPackScanner.SplashPackMetadata;
|
||||||
|
import art.arcane.iris.pack.BuiltInPackUpdates;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CompletionException;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
public final class ModdedPackUpdateService implements ModdedTickableService {
|
||||||
|
private final Function<File, CompletableFuture<PackCheck>> checker;
|
||||||
|
private final Set<UUID> notified = new HashSet<>();
|
||||||
|
private CompletableFuture<PackCheck> pending;
|
||||||
|
private List<String> notices = List.of();
|
||||||
|
private File packsFolder;
|
||||||
|
private boolean enabled;
|
||||||
|
private boolean started;
|
||||||
|
|
||||||
|
public ModdedPackUpdateService() {
|
||||||
|
this(ModdedPackUpdateService::checkPacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
ModdedPackUpdateService(Function<File, CompletableFuture<PackCheck>> checker) {
|
||||||
|
this.checker = checker;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable() {
|
||||||
|
enabled = false;
|
||||||
|
if (pending != null) {
|
||||||
|
pending.cancel(true);
|
||||||
|
pending = null;
|
||||||
|
}
|
||||||
|
started = false;
|
||||||
|
notices = List.of();
|
||||||
|
notified.clear();
|
||||||
|
packsFolder = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServerTick(NativeModdedServer server) {
|
||||||
|
if (!enabled || server == null || !server.hasPlayerList()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!started) {
|
||||||
|
started = true;
|
||||||
|
packsFolder = IrisPlatforms.get().packsFolderNoCreate().getAbsoluteFile();
|
||||||
|
pending = checker.apply(packsFolder);
|
||||||
|
}
|
||||||
|
if (pending == null || !pending.isDone()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CompletableFuture<PackCheck> completed = pending;
|
||||||
|
pending = null;
|
||||||
|
try {
|
||||||
|
publish(completed.join());
|
||||||
|
server.forEachPlayer(this::notifyPlayer);
|
||||||
|
} catch (CompletionException error) {
|
||||||
|
IrisLogging.reportError("Iris pack update check failed", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refresh() {
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pending != null) {
|
||||||
|
pending.cancel(true);
|
||||||
|
pending = null;
|
||||||
|
}
|
||||||
|
started = false;
|
||||||
|
notices = List.of();
|
||||||
|
notified.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void notifyPlayer(NativeProtocolPlayer player) {
|
||||||
|
if (!enabled || notices.isEmpty() || player == null || !player.connected()
|
||||||
|
|| (!player.isGameMaster() && !player.isServerOwner()) || !notified.add(player.id())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (String notice : notices) {
|
||||||
|
player.sendMessage(notice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CompletableFuture<PackCheck> checkPacks(File folder) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> IrisSplashPackScanner.collect(folder, IrisLogging::reportError))
|
||||||
|
.thenCompose(packs -> BuiltInPackUpdates.check(packs.stream().map(SplashPackMetadata::name).toList())
|
||||||
|
.thenApply(updates -> new PackCheck(packs, updates)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void publish(PackCheck result) {
|
||||||
|
List<String> updates = new ArrayList<>();
|
||||||
|
IrisLogging.info("Iris packs: " + packsFolder);
|
||||||
|
for (SplashPackMetadata pack : result.packs()) {
|
||||||
|
BuiltInPackUpdates.Update update = result.updates().get(pack.name());
|
||||||
|
String version = pack.version().matches("[0-9]+") ? "v" + pack.version() : "version unknown";
|
||||||
|
IrisLogging.info(" " + pack.name() + " " + version
|
||||||
|
+ (update == null ? "" : update.suffix(pack.version())));
|
||||||
|
if (update != null && update.newerThan(pack.version())) {
|
||||||
|
updates.add("Iris " + pack.name() + " release metadata is outdated: installed v" + pack.version()
|
||||||
|
+ ", latest built-in release v" + update.version() + ".");
|
||||||
|
updates.add("Back up and review custom edits in " + new File(packsFolder, pack.name())
|
||||||
|
+ "; close Studio, then update with /iris download pack=" + pack.name() + " overwrite=true.");
|
||||||
|
updates.add("Existing worlds retain their recorded production pack. Back up the complete world, then stage an update with /iris world update <dimension> "
|
||||||
|
+ pack.name() + " and restart the server to activate it.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notices = List.copyOf(updates);
|
||||||
|
for (String notice : notices) {
|
||||||
|
IrisLogging.warn(notice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record PackCheck(List<SplashPackMetadata> packs, Map<String, BuiltInPackUpdates.Update> updates) {
|
||||||
|
PackCheck {
|
||||||
|
packs = List.copyOf(packs);
|
||||||
|
updates = Map.copyOf(updates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
-5
@@ -57,6 +57,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.RejectedExecutionException;
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
public final class ModdedStudioHotloadService implements ModdedTickableService, EnginePlatformHooks {
|
public final class ModdedStudioHotloadService implements ModdedTickableService, EnginePlatformHooks {
|
||||||
@@ -64,6 +65,8 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
|||||||
private static final long POLL_MILLIS = 250L;
|
private static final long POLL_MILLIS = 250L;
|
||||||
private static final long CHECK_LATCH_MILLIS = 1_000L;
|
private static final long CHECK_LATCH_MILLIS = 1_000L;
|
||||||
private static final long RECENT_GENERATION_HOLDOFF_MILLIS = 2_000L;
|
private static final long RECENT_GENERATION_HOLDOFF_MILLIS = 2_000L;
|
||||||
|
private static final long SHUTDOWN_TIMEOUT_SECONDS = 30L;
|
||||||
|
private static final long INTERRUPT_DRAIN_TIMEOUT_SECONDS = 5L;
|
||||||
|
|
||||||
private final ConcurrentHashMap<String, Watch> watches = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<String, Watch> watches = new ConcurrentHashMap<>();
|
||||||
private volatile ExecutorService executor;
|
private volatile ExecutorService executor;
|
||||||
@@ -81,9 +84,13 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
if (executor != null) {
|
ExecutorService current = executor;
|
||||||
|
if (current != null && !current.isTerminated()) {
|
||||||
|
if (!current.isShutdown()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
throw new IllegalStateException("Iris Studio hotload cannot restart while prior workers are still active");
|
||||||
|
}
|
||||||
executor = Executors.newSingleThreadExecutor((Runnable task) -> {
|
executor = Executors.newSingleThreadExecutor((Runnable task) -> {
|
||||||
Thread thread = new Thread(task, "Iris Studio Hotload");
|
Thread thread = new Thread(task, "Iris Studio Hotload");
|
||||||
thread.setDaemon(true);
|
thread.setDaemon(true);
|
||||||
@@ -96,10 +103,10 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
|||||||
@Override
|
@Override
|
||||||
public void onDisable() {
|
public void onDisable() {
|
||||||
ExecutorService active = executor;
|
ExecutorService active = executor;
|
||||||
executor = null;
|
if (!shutdownAndDrain(active)) {
|
||||||
if (active != null) {
|
throw new IllegalStateException("Iris Studio hotload worker remained active during service shutdown");
|
||||||
active.shutdownNow();
|
|
||||||
}
|
}
|
||||||
|
executor = null;
|
||||||
for (Watch watch : watches.values()) {
|
for (Watch watch : watches.values()) {
|
||||||
watch.close();
|
watch.close();
|
||||||
}
|
}
|
||||||
@@ -197,7 +204,7 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
|||||||
@Override
|
@Override
|
||||||
public void onServerTick(NativeModdedServer server) {
|
public void onServerTick(NativeModdedServer server) {
|
||||||
ExecutorService active = executor;
|
ExecutorService active = executor;
|
||||||
if (active == null) {
|
if (active == null || active.isShutdown()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
@@ -284,6 +291,30 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
|||||||
&& !isPregeneratorActive(engine);
|
&& !isPregeneratorActive(engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean shutdownAndDrain(ExecutorService active) {
|
||||||
|
if (active == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
active.shutdown();
|
||||||
|
try {
|
||||||
|
if (active.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
active.shutdownNow();
|
||||||
|
if (active.awaitTermination(INTERRUPT_DRAIN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
ModdedIrisLog.error("Iris Studio hotload worker did not stop after shutdownNow",
|
||||||
|
new IllegalStateException("Iris Studio hotload worker is still active"));
|
||||||
|
return false;
|
||||||
|
} catch (InterruptedException exception) {
|
||||||
|
active.shutdownNow();
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
ModdedIrisLog.error("Interrupted while draining Iris Studio hotload", exception);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean throttled(IrisModdedChunkGenerator generator, Engine engine, long now) {
|
private boolean throttled(IrisModdedChunkGenerator generator, Engine engine, long now) {
|
||||||
if (now - generator.lastChunkGenAt() < RECENT_GENERATION_HOLDOFF_MILLIS) {
|
if (now - generator.lastChunkGenAt() < RECENT_GENERATION_HOLDOFF_MILLIS) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
+10
-4
@@ -38,12 +38,18 @@ public class IrisModdedStructureParityTest {
|
|||||||
Bootstrap.bootStrap();
|
Bootstrap.bootStrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Engine surfaceEngine(int minHeight) {
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
when(engine.getMinHeight()).thenReturn(minHeight);
|
||||||
|
return engine;
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void surfaceBiomeFastPathBeginsAboveTheCaveSwitch() {
|
public void surfaceBiomeFastPathBeginsAboveTheCaveSwitch() {
|
||||||
assertFalse(ModdedBiomePolicy.isGuaranteedSurfaceBiome(-2, -256));
|
assertFalse(ModdedBiomePolicy.isGuaranteedSurfaceBiome(surfaceEngine(-256), -2));
|
||||||
assertTrue(ModdedBiomePolicy.isGuaranteedSurfaceBiome(-1, -256));
|
assertTrue(ModdedBiomePolicy.isGuaranteedSurfaceBiome(surfaceEngine(-256), -1));
|
||||||
assertFalse(ModdedBiomePolicy.isGuaranteedSurfaceBiome(10, 0));
|
assertFalse(ModdedBiomePolicy.isGuaranteedSurfaceBiome(surfaceEngine(0), 10));
|
||||||
assertTrue(ModdedBiomePolicy.isGuaranteedSurfaceBiome(11, 0));
|
assertTrue(ModdedBiomePolicy.isGuaranteedSurfaceBiome(surfaceEngine(0), 11));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+8
-3
@@ -57,8 +57,11 @@ public class ModdedBindFailureTest {
|
|||||||
when(server.generateStructures()).thenReturn(false);
|
when(server.generateStructures()).thenReturn(false);
|
||||||
|
|
||||||
try (MockedStatic<NativeModdedServer> servers = mockStatic(NativeModdedServer.class);
|
try (MockedStatic<NativeModdedServer> servers = mockStatic(NativeModdedServer.class);
|
||||||
|
MockedStatic<ModdedWorldEngines> engines = mockStatic(ModdedWorldEngines.class);
|
||||||
MockedStatic<ModdedIrisLog> log = mockStatic(ModdedIrisLog.class)) {
|
MockedStatic<ModdedIrisLog> log = mockStatic(ModdedIrisLog.class)) {
|
||||||
servers.when(() -> NativeModdedServer.forWorld(level)).thenReturn(server);
|
servers.when(() -> NativeModdedServer.forWorld(level)).thenReturn(server);
|
||||||
|
engines.when(() -> ModdedWorldEngines.get(eq(level), any(), any(), anyLong(), any()))
|
||||||
|
.thenThrow(new IllegalStateException("broken world binding"));
|
||||||
|
|
||||||
IllegalStateException recorded = assertThrows(IllegalStateException.class, () -> generator.bindLevel(level));
|
IllegalStateException recorded = assertThrows(IllegalStateException.class, () -> generator.bindLevel(level));
|
||||||
IllegalStateException second = assertThrows(IllegalStateException.class, generator::engine);
|
IllegalStateException second = assertThrows(IllegalStateException.class, generator::engine);
|
||||||
@@ -66,9 +69,9 @@ public class ModdedBindFailureTest {
|
|||||||
|
|
||||||
assertSame(recorded, second.getCause());
|
assertSame(recorded, second.getCause());
|
||||||
assertSame(recorded, third.getCause());
|
assertSame(recorded, third.getCause());
|
||||||
servers.verify(() -> NativeModdedServer.forWorld(level), times(1));
|
engines.verify(() -> ModdedWorldEngines.get(eq(level), any(), any(), anyLong(), any()), times(1));
|
||||||
log.verify(() -> ModdedIrisLog.error(argThat((String line) -> line != null && line.startsWith("Cause: ")
|
log.verify(() -> ModdedIrisLog.error(argThat((String line) -> line != null && line.startsWith("Cause: ")
|
||||||
&& line.contains("generate-structures=false"))), times(1));
|
&& line.contains("broken world binding"))), times(1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,13 +120,15 @@ public class ModdedBindFailureTest {
|
|||||||
MockedStatic<ModdedWorldEngines> engines = mockStatic(ModdedWorldEngines.class);
|
MockedStatic<ModdedWorldEngines> engines = mockStatic(ModdedWorldEngines.class);
|
||||||
MockedStatic<ModdedIrisLog> log = mockStatic(ModdedIrisLog.class)) {
|
MockedStatic<ModdedIrisLog> log = mockStatic(ModdedIrisLog.class)) {
|
||||||
servers.when(() -> NativeModdedServer.forWorld(level)).thenReturn(server);
|
servers.when(() -> NativeModdedServer.forWorld(level)).thenReturn(server);
|
||||||
|
engines.when(() -> ModdedWorldEngines.get(eq(level), any(), any(), anyLong(), any()))
|
||||||
|
.thenAnswer(invocation -> { throw new IllegalStateException("broken world binding"); });
|
||||||
|
|
||||||
IllegalStateException first = assertThrows(IllegalStateException.class, () -> generator.bindLevel(level));
|
IllegalStateException first = assertThrows(IllegalStateException.class, () -> generator.bindLevel(level));
|
||||||
generator.unbindEngine(level);
|
generator.unbindEngine(level);
|
||||||
IllegalStateException second = assertThrows(IllegalStateException.class, () -> generator.bindLevel(level));
|
IllegalStateException second = assertThrows(IllegalStateException.class, () -> generator.bindLevel(level));
|
||||||
|
|
||||||
assertNotSame(first, second.getCause());
|
assertNotSame(first, second.getCause());
|
||||||
servers.verify(() -> NativeModdedServer.forWorld(level), times(2));
|
engines.verify(() -> ModdedWorldEngines.get(eq(level), any(), any(), anyLong(), any()), times(2));
|
||||||
engines.verify(() -> ModdedWorldEngines.evictOrThrow(level));
|
engines.verify(() -> ModdedWorldEngines.evictOrThrow(level));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+39
@@ -2,6 +2,11 @@ package art.arcane.iris.modded;
|
|||||||
|
|
||||||
import art.arcane.iris.generation.biome.IrisBiome;
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.generation.runtime.DimensionStackLayout;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
|
import art.arcane.iris.generation.subterrain.IrisSubterrainFeature;
|
||||||
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import art.arcane.iris.generation.runtime.GenerationSessionException;
|
import art.arcane.iris.generation.runtime.GenerationSessionException;
|
||||||
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
||||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||||
@@ -11,20 +16,54 @@ import art.arcane.volmlib.util.stream.interpolation.Interpolated;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.io.File;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertSame;
|
import static org.junit.Assert.assertSame;
|
||||||
import static org.junit.Assert.assertThrows;
|
import static org.junit.Assert.assertThrows;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class ModdedBiomePolicyTest {
|
public class ModdedBiomePolicyTest {
|
||||||
|
@Test
|
||||||
|
public void authoredBiomeWinsAboveSurfaceGateAndBeforeStackOwnership() throws Exception {
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
IrisBiome feature = mock(IrisBiome.class);
|
||||||
|
IrisDimension host = new IrisDimension().setSubterrainFeatures(new KList<>(
|
||||||
|
new IrisSubterrainFeature().setId("room")));
|
||||||
|
IrisData data = mock(IrisData.class);
|
||||||
|
when(data.getDataFolder()).thenReturn(new File("synthetic-pack"));
|
||||||
|
when(engine.getData()).thenReturn(data);
|
||||||
|
when(engine.getDimension()).thenReturn(host);
|
||||||
|
when(engine.getComplex()).thenReturn(mock(IrisComplex.class));
|
||||||
|
when(engine.getMinHeight()).thenReturn(-64);
|
||||||
|
when(engine.getSubterrainBiome(12, 224, -20)).thenReturn(feature);
|
||||||
|
assertFalse(ModdedBiomePolicy.isGuaranteedSurfaceBiome(engine, 40));
|
||||||
|
ModdedBiomePolicy<BiomeToken, Object> policy = boundTo(engine);
|
||||||
|
DimensionStackLayout stack = mock(DimensionStackLayout.class);
|
||||||
|
Method method = ModdedBiomePolicy.class.getDeclaredMethod("resolveBiomeResolution", Engine.class,
|
||||||
|
int.class, int.class, int.class, boolean.class, DimensionStackLayout.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
Object resolution = method.invoke(policy, engine, 3, 40, -5, true, stack);
|
||||||
|
Field biome = resolution.getClass().getDeclaredField("irisBiome");
|
||||||
|
biome.setAccessible(true);
|
||||||
|
assertSame(feature, biome.get(resolution));
|
||||||
|
Field dimension = resolution.getClass().getDeclaredField("dimension");
|
||||||
|
dimension.setAccessible(true);
|
||||||
|
assertSame(host, dimension.get(resolution));
|
||||||
|
verifyNoInteractions(stack);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void registryOrderAndHolderIdentitySurviveThePolicyBoundaryAndRefreshOnRepoint() {
|
public void registryOrderAndHolderIdentitySurviveThePolicyBoundaryAndRefreshOnRepoint() {
|
||||||
BiomeToken ocean = new BiomeToken("minecraft:ocean");
|
BiomeToken ocean = new BiomeToken("minecraft:ocean");
|
||||||
|
|||||||
+121
@@ -18,6 +18,21 @@
|
|||||||
|
|
||||||
package art.arcane.iris.modded;
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
|
import art.arcane.iris.generation.block.IrisBlockData;
|
||||||
|
import art.arcane.iris.generation.runtime.BiomeEnvironment;
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||||
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
|
import art.arcane.iris.world.loot.IrisBlockDrops;
|
||||||
|
import art.arcane.iris.world.loot.IrisLoot;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeItemStack;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldGenerators;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockPoint;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
import net.minecraft.core.Direction;
|
import net.minecraft.core.Direction;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
|
||||||
import net.minecraft.world.level.block.Blocks;
|
import net.minecraft.world.level.block.Blocks;
|
||||||
@@ -27,9 +42,22 @@ import org.junit.Test;
|
|||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import net.minecraft.SharedConstants;
|
import net.minecraft.SharedConstants;
|
||||||
import net.minecraft.server.Bootstrap;
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class ModdedBlockBreakHandlerTest {
|
public class ModdedBlockBreakHandlerTest {
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
@@ -48,4 +76,97 @@ public class ModdedBlockBreakHandlerTest {
|
|||||||
assertTrue(ModdedBlockBreakHandler.matchesState(north, east, false));
|
assertTrue(ModdedBlockBreakHandler.matchesState(north, east, false));
|
||||||
assertFalse(ModdedBlockBreakHandler.matchesState(north, ModdedBlockState.of(Blocks.COBBLESTONE.defaultBlockState(), null), false));
|
assertFalse(ModdedBlockBreakHandler.matchesState(north, ModdedBlockState.of(Blocks.COBBLESTONE.defaultBlockState(), null), false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void historicalEnvironmentSuppliesAllRulesAndScopesMatchingAndLoot() {
|
||||||
|
try (DropFixture fixture = new DropFixture()) {
|
||||||
|
fixture.dimensionRule.setReplaceVanillaDrops(true);
|
||||||
|
|
||||||
|
ModdedBlockBreakHandler.Result result = fixture.evaluate();
|
||||||
|
|
||||||
|
assertTrue(result.replaceVanillaDrops());
|
||||||
|
assertEquals(3, result.drops().size());
|
||||||
|
assertSame(fixture.item, result.drops().getFirst());
|
||||||
|
assertFalse(fixture.scoped.get());
|
||||||
|
verify(fixture.engine).getBiomeEnvironment(16, 136, 32);
|
||||||
|
verify(fixture.engine).openBiomeEnvironmentScope(fixture.environment);
|
||||||
|
verify(fixture.engine, never()).getData();
|
||||||
|
verify(fixture.engine, never()).getDimension();
|
||||||
|
verify(fixture.engine, never()).getBiome(anyInt(), anyInt(), anyInt());
|
||||||
|
verify(fixture.engine, never()).getRegion(anyInt(), anyInt(), anyInt());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void historicalBiomeCanSkipRegionAndDimensionDrops() {
|
||||||
|
try (DropFixture fixture = new DropFixture()) {
|
||||||
|
fixture.biomeRule.setSkipParents(true);
|
||||||
|
fixture.dimensionRule.setReplaceVanillaDrops(true);
|
||||||
|
|
||||||
|
ModdedBlockBreakHandler.Result result = fixture.evaluate();
|
||||||
|
|
||||||
|
assertFalse(result.replaceVanillaDrops());
|
||||||
|
assertEquals(1, result.drops().size());
|
||||||
|
assertFalse(fixture.scoped.get());
|
||||||
|
verify(fixture.region, never()).getBlockDrops();
|
||||||
|
verify(fixture.dimension, never()).getBlockDrops();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class DropFixture implements AutoCloseable {
|
||||||
|
private final NativeWorld world = mock(NativeWorld.class);
|
||||||
|
private final Engine engine = mock(Engine.class);
|
||||||
|
private final IrisBiome biome = mock(IrisBiome.class);
|
||||||
|
private final IrisRegion region = mock(IrisRegion.class);
|
||||||
|
private final IrisDimension dimension = mock(IrisDimension.class);
|
||||||
|
private final IrisData data = mock(IrisData.class);
|
||||||
|
private final BiomeEnvironment environment = new BiomeEnvironment(7L, biome, region, dimension, data);
|
||||||
|
private final ModdedBlockState state = ModdedBlockState.of(Blocks.COBBLESTONE.defaultBlockState(), null);
|
||||||
|
private final NativeItemStack item = mock(NativeItemStack.class);
|
||||||
|
private final IrisLoot loot = new IrisLoot().setRarity(1);
|
||||||
|
private final IrisBlockData configuredBlock = mock(IrisBlockData.class);
|
||||||
|
private final IrisBlockDrops biomeRule = rule();
|
||||||
|
private final IrisBlockDrops regionRule = rule();
|
||||||
|
private final IrisBlockDrops dimensionRule = rule();
|
||||||
|
private final AtomicBoolean scoped = new AtomicBoolean();
|
||||||
|
private final MockedStatic<NativeWorldGenerators> generators = mockStatic(NativeWorldGenerators.class);
|
||||||
|
private final MockedStatic<ModdedItemTranslator> translator = mockStatic(ModdedItemTranslator.class);
|
||||||
|
|
||||||
|
private DropFixture() {
|
||||||
|
IrisModdedChunkGenerator generator = mock(IrisModdedChunkGenerator.class);
|
||||||
|
generators.when(() -> NativeWorldGenerators.find(world, IrisModdedChunkGenerator.class)).thenReturn(generator);
|
||||||
|
when(generator.engineIfBound()).thenReturn(engine);
|
||||||
|
when(engine.getMinHeight()).thenReturn(-64);
|
||||||
|
when(engine.getBiomeEnvironment(16, 136, 32)).thenReturn(environment);
|
||||||
|
when(engine.openBiomeEnvironmentScope(environment)).thenAnswer(invocation -> {
|
||||||
|
assertTrue(scoped.compareAndSet(false, true));
|
||||||
|
return (BiomeEnvironment.Scope) () -> scoped.set(false);
|
||||||
|
});
|
||||||
|
when(biome.getBlockDrops()).thenReturn(new KList<>(biomeRule));
|
||||||
|
when(region.getBlockDrops()).thenReturn(new KList<>(regionRule));
|
||||||
|
when(dimension.getBlockDrops()).thenReturn(new KList<>(dimensionRule));
|
||||||
|
when(configuredBlock.getBlockData(data)).thenAnswer(invocation -> {
|
||||||
|
assertTrue(scoped.get());
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
translator.when(() -> ModdedItemTranslator.stack(eq(loot), eq(RNG.r), any())).thenAnswer(invocation -> {
|
||||||
|
assertTrue(scoped.get());
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private IrisBlockDrops rule() {
|
||||||
|
return new IrisBlockDrops().setBlocks(new KList<>(configuredBlock)).setDrops(new KList<>(loot));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModdedBlockBreakHandler.Result evaluate() {
|
||||||
|
return ModdedBlockBreakHandler.evaluateManagedDrops(world, new NativeBlockPoint(16, 72, 32), state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
translator.close();
|
||||||
|
generators.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+31
@@ -39,6 +39,37 @@ public class ModdedClientPackageIsolationTest {
|
|||||||
"art/arcane/iris/nativegen");
|
"art/arcane/iris/nativegen");
|
||||||
private static final int MINIMUM_SCANNED = 50;
|
private static final int MINIMUM_SCANNED = 50;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void clientPresentationCannotAccessPackOrWorldPersistence() throws IOException, URISyntaxException {
|
||||||
|
Path root = classesRoot();
|
||||||
|
Path directory = root.resolve(CLIENT_IRIS);
|
||||||
|
List<String> forbidden = List.of(
|
||||||
|
"art/arcane/iris/pack/",
|
||||||
|
"art/arcane/iris/world/storage/",
|
||||||
|
"art/arcane/iris/world/history/",
|
||||||
|
"art/arcane/iris/generation/runtime/",
|
||||||
|
"art/arcane/iris/configuration/",
|
||||||
|
"java/nio/file/",
|
||||||
|
"java/nio/channels/FileChannel",
|
||||||
|
"java/io/File",
|
||||||
|
"java/io/RandomAccessFile");
|
||||||
|
List<String> violations = new ArrayList<>();
|
||||||
|
int scanned = 0;
|
||||||
|
try (Stream<Path> walk = Files.walk(directory)) {
|
||||||
|
for (Path classFile : walk.filter(ModdedClientPackageIsolationTest::isClassFile).toList()) {
|
||||||
|
scanned++;
|
||||||
|
String bytecode = readAsLatin1(classFile);
|
||||||
|
for (String dependency : forbidden) {
|
||||||
|
if (bytecode.contains(dependency)) {
|
||||||
|
violations.add(root.relativize(classFile) + " -> " + dependency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue("client presentation classes were not scanned", scanned >= 20);
|
||||||
|
assertEquals("client presentation can access persistent generation data: " + violations, List.of(), violations);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void serverSidePackagesNeverReferenceClientOnlyTypes() throws IOException, URISyntaxException {
|
public void serverSidePackagesNeverReferenceClientOnlyTypes() throws IOException, URISyntaxException {
|
||||||
Path root = classesRoot();
|
Path root = classesRoot();
|
||||||
|
|||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockFaces;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
|
import art.arcane.volmlib.util.hunk.Hunk;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class ModdedDecoratorHooksTest {
|
||||||
|
@Test
|
||||||
|
public void faceFixingExcludesTheUpperBoundaryWhileKeepingTheLastRowAccessible() {
|
||||||
|
NativeBlockState state = mock(NativeBlockState.class);
|
||||||
|
NativeBlockState lastRow = mock(NativeBlockState.class);
|
||||||
|
Hunk<NativeBlockState> hunk = mock(Hunk.class);
|
||||||
|
when(hunk.getHeight()).thenReturn(4);
|
||||||
|
when(hunk.get(1, 3, 2)).thenReturn(lastRow);
|
||||||
|
when(hunk.get(1, 4, 2)).thenThrow(new IndexOutOfBoundsException("upper boundary"));
|
||||||
|
try (MockedStatic<NativeBlockFaces> faces = mockStatic(NativeBlockFaces.class)) {
|
||||||
|
faces.when(() -> NativeBlockFaces.fixFaces(eq(state), any(NativeBlockFaces.Neighbors.class)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
NativeBlockFaces.Neighbors neighbors = invocation.getArgument(1);
|
||||||
|
assertNull(neighbors.secondary(0, 1, 0));
|
||||||
|
assertSame(lastRow, neighbors.secondary(0, 0, 0));
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSame(state, new ModdedDecoratorHooks().fixFaces(state, hunk, 1, 2, 99, 3, 108, null));
|
||||||
|
|
||||||
|
faces.verify(() -> NativeBlockFaces.fixFaces(eq(state), any(NativeBlockFaces.Neighbors.class)));
|
||||||
|
}
|
||||||
|
verify(hunk, never()).get(1, 4, 2);
|
||||||
|
verify(hunk).get(1, 3, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loaded 100 of 463 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user