From 4da9661885db700d9441f23d7ad67b3acc01d149 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Tue, 29 Sep 2026 03:56:40 -0400 Subject: [PATCH] Loader spawn position check - The modded chunk generator hands VolmLib the bound loader for chunk-generation creature checks - Each loader build tests its spawn position check: Forge and NeoForge fire MobSpawnEvent.PositionCheck and honor a denial, Fabric applies the vanilla rule and obstruction checks --- .../fabric/NativeFabricSpawnPositionTest.java | 43 ++++++++++++++ .../forge/NativeForgeSpawnPositionTest.java | 52 +++++++++++++++++ .../iris/modded/IrisModdedChunkGenerator.java | 6 ++ .../iris/modded/ModdedPlatformPathsTest.java | 8 +++ .../NativeNeoForgeSpawnPositionTest.java | 58 +++++++++++++++++++ 5 files changed, 167 insertions(+) create mode 100644 adapters/fabric/src/test/java/art/arcane/volmlib/nativelib/minecraft26_2/fabric/NativeFabricSpawnPositionTest.java create mode 100644 adapters/forge/src/test/java/art/arcane/volmlib/nativelib/minecraft26_2/forge/NativeForgeSpawnPositionTest.java create mode 100644 adapters/neoforge/src/test/java/art/arcane/volmlib/nativelib/minecraft26_2/neoforge/NativeNeoForgeSpawnPositionTest.java diff --git a/adapters/fabric/src/test/java/art/arcane/volmlib/nativelib/minecraft26_2/fabric/NativeFabricSpawnPositionTest.java b/adapters/fabric/src/test/java/art/arcane/volmlib/nativelib/minecraft26_2/fabric/NativeFabricSpawnPositionTest.java new file mode 100644 index 000000000..482f0354a --- /dev/null +++ b/adapters/fabric/src/test/java/art/arcane/volmlib/nativelib/minecraft26_2/fabric/NativeFabricSpawnPositionTest.java @@ -0,0 +1,43 @@ +package art.arcane.volmlib.nativelib.minecraft26_2.fabric; + +import art.arcane.volmlib.nativelib.modded.NativeLoaderOptions; +import net.minecraft.SharedConstants; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.level.ServerLevelAccessor; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.function.BooleanSupplier; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class NativeFabricSpawnPositionTest { + @BeforeClass + public static void bootstrap() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void chunkGenerationSpawnsUseTheVanillaRulesAndObstructionChecks() { + NativeFabricLoader loader = new NativeFabricLoader( + new NativeLoaderOptions("irisworldgen", "treefeller", BooleanSupplier::getAsBoolean)); + ServerLevelAccessor level = mock(ServerLevelAccessor.class); + Mob mob = mock(Mob.class); + when(mob.checkSpawnRules(level, EntitySpawnReason.CHUNK_GENERATION)).thenReturn(true); + when(mob.checkSpawnObstruction(level)).thenReturn(true); + assertTrue(loader.checkSpawnPosition(mob, level, EntitySpawnReason.CHUNK_GENERATION)); + + when(mob.checkSpawnObstruction(level)).thenReturn(false); + assertFalse(loader.checkSpawnPosition(mob, level, EntitySpawnReason.CHUNK_GENERATION)); + + when(mob.checkSpawnObstruction(level)).thenReturn(true); + when(mob.checkSpawnRules(level, EntitySpawnReason.CHUNK_GENERATION)).thenReturn(false); + assertFalse(loader.checkSpawnPosition(mob, level, EntitySpawnReason.CHUNK_GENERATION)); + } +} diff --git a/adapters/forge/src/test/java/art/arcane/volmlib/nativelib/minecraft26_2/forge/NativeForgeSpawnPositionTest.java b/adapters/forge/src/test/java/art/arcane/volmlib/nativelib/minecraft26_2/forge/NativeForgeSpawnPositionTest.java new file mode 100644 index 000000000..13ccce2db --- /dev/null +++ b/adapters/forge/src/test/java/art/arcane/volmlib/nativelib/minecraft26_2/forge/NativeForgeSpawnPositionTest.java @@ -0,0 +1,52 @@ +package art.arcane.volmlib.nativelib.minecraft26_2.forge; + +import art.arcane.volmlib.nativelib.modded.NativeLoaderOptions; +import net.minecraft.SharedConstants; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraftforge.common.util.Result; +import net.minecraftforge.event.entity.living.MobSpawnEvent; +import net.minecraftforge.eventbus.api.listener.EventListener; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BooleanSupplier; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class NativeForgeSpawnPositionTest { + @BeforeClass + public static void bootstrap() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void chunkGenerationSpawnsFireThePositionCheckEventAndHonorItsDenial() { + NativeForgeLoader loader = new NativeForgeLoader( + new NativeLoaderOptions("irisworldgen", "treefeller", BooleanSupplier::getAsBoolean)); + ServerLevelAccessor level = mock(ServerLevelAccessor.class); + Mob mob = mock(Mob.class); + when(mob.checkSpawnRules(level, EntitySpawnReason.CHUNK_GENERATION)).thenReturn(true); + when(mob.checkSpawnObstruction(level)).thenReturn(true); + AtomicReference observed = new AtomicReference<>(); + EventListener listener = MobSpawnEvent.PositionCheck.BUS.addListener((MobSpawnEvent.PositionCheck event) -> { + observed.set(event.getSpawnReason()); + event.setResult(Result.DENY); + }); + try { + assertFalse(loader.checkSpawnPosition(mob, level, EntitySpawnReason.CHUNK_GENERATION)); + } finally { + MobSpawnEvent.PositionCheck.BUS.removeListener(listener); + } + assertEquals(EntitySpawnReason.CHUNK_GENERATION, observed.get()); + assertTrue(loader.checkSpawnPosition(mob, level, EntitySpawnReason.CHUNK_GENERATION)); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java index 1bc6e2250..33d77d171 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java @@ -44,6 +44,7 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeFeatureBiomeSourc import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeDimensionRuntime; import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedLoader; import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeGeneratorOwner; import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeGeneratorHandle; @@ -1011,6 +1012,11 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat return ModdedEngineBootstrap.currentServer(); } + @Override + public NativeModdedLoader loader() { + return ModdedEngineBootstrap.loader(); + } + @Override public Engine current() { return engine(); diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedPlatformPathsTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedPlatformPathsTest.java index 00009e7b1..c46775ab3 100644 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedPlatformPathsTest.java +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedPlatformPathsTest.java @@ -9,6 +9,9 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedServerAccess; import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedServerLevels; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.level.ServerLevelAccessor; import net.minecraft.world.level.block.state.BlockState; import org.junit.Rule; import org.junit.Test; @@ -83,6 +86,11 @@ public class ModdedPlatformPathsTest { public boolean canBreakBlock(ServerLevel level, ServerPlayer player, BlockPos position, BlockState state) { return false; } + + @Override + public boolean checkSpawnPosition(Mob mob, ServerLevelAccessor level, EntitySpawnReason reason) { + return false; + } }); } diff --git a/adapters/neoforge/src/test/java/art/arcane/volmlib/nativelib/minecraft26_2/neoforge/NativeNeoForgeSpawnPositionTest.java b/adapters/neoforge/src/test/java/art/arcane/volmlib/nativelib/minecraft26_2/neoforge/NativeNeoForgeSpawnPositionTest.java new file mode 100644 index 000000000..25667b89c --- /dev/null +++ b/adapters/neoforge/src/test/java/art/arcane/volmlib/nativelib/minecraft26_2/neoforge/NativeNeoForgeSpawnPositionTest.java @@ -0,0 +1,58 @@ +package art.arcane.volmlib.nativelib.minecraft26_2.neoforge; + +import art.arcane.volmlib.nativelib.modded.NativeLoaderOptions; +import net.minecraft.SharedConstants; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.level.ServerLevelAccessor; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.entity.living.MobSpawnEvent; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class NativeNeoForgeSpawnPositionTest { + @BeforeClass + public static void bootstrap() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void chunkGenerationSpawnsFireThePositionCheckEventAndHonorItsDenial() { + NativeNeoForgeLoader loader = new NativeNeoForgeLoader( + new NativeLoaderOptions("irisworldgen", "treefeller", BooleanSupplier::getAsBoolean)); + ServerLevelAccessor level = mock(ServerLevelAccessor.class); + Mob mob = mock(Mob.class); + when(mob.checkSpawnRules(level, EntitySpawnReason.CHUNK_GENERATION)).thenReturn(true); + when(mob.checkSpawnObstruction(level)).thenReturn(true); + AtomicBoolean deny = new AtomicBoolean(true); + AtomicReference observed = new AtomicReference<>(); + Consumer listener = (MobSpawnEvent.PositionCheck event) -> { + if (event.getEntity() == mob && deny.get()) { + observed.set(event.getSpawnType()); + event.setResult(MobSpawnEvent.PositionCheck.Result.FAIL); + } + }; + NeoForge.EVENT_BUS.addListener(MobSpawnEvent.PositionCheck.class, listener); + try { + assertFalse(loader.checkSpawnPosition(mob, level, EntitySpawnReason.CHUNK_GENERATION)); + assertEquals(EntitySpawnReason.CHUNK_GENERATION, observed.get()); + deny.set(false); + assertTrue(loader.checkSpawnPosition(mob, level, EntitySpawnReason.CHUNK_GENERATION)); + } finally { + NeoForge.EVENT_BUS.unregister(listener); + } + } +}