diff --git a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGenerator.java b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGenerator.java index cd2bfddd8..8f2e93096 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGenerator.java +++ b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGenerator.java @@ -37,6 +37,7 @@ import net.minecraft.util.random.WeightedList; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.NaturalSpawner; import net.minecraft.world.level.NoiseColumn; import net.minecraft.world.level.StructureManager; import net.minecraft.world.level.WorldGenLevel; @@ -46,6 +47,7 @@ import net.minecraft.world.level.biome.BiomeSource; import net.minecraft.world.level.biome.MobSpawnSettings; import net.minecraft.world.level.levelgen.GenerationStep; import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.LegacyRandomSource; import net.minecraft.world.level.levelgen.RandomState; import net.minecraft.world.level.levelgen.RandomSupport; import net.minecraft.world.level.levelgen.WorldgenRandom; @@ -574,8 +576,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator { } @Override - public void spawnOriginalMobs(WorldGenRegion regionlimitedworldaccess) { - delegate.spawnOriginalMobs(regionlimitedworldaccess); + public void spawnOriginalMobs(WorldGenRegion region) { + ChunkPos center = region.getCenter(); + Holder visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY())); + Holder vanillaBiome = customBiomeSource.getVanillaSpawnBiome(visibleBiome); + WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed())); + random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ()); + NaturalSpawner.spawnMobsForChunkGeneration( + region, vanillaBiome == null ? visibleBiome : vanillaBiome, center, random); } private static WeightedList mergeSpawnTables( diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGeneratorFailureContractTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGeneratorFailureContractTest.java index 24888a18c..6fc59f789 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGeneratorFailureContractTest.java +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGeneratorFailureContractTest.java @@ -64,4 +64,19 @@ public class IrisChunkGeneratorFailureContractTest { assertTrue(source.contains("engine.acquireGenerationLease(\"bukkit_nms_base_column\")")); assertTrue(source.contains("catch (GenerationSessionException e)")); } + + @Test + public void vanillaChunkGenerationMobsUseTheVisibleBiomesVanillaDerivative() throws IOException { + String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))); + int spawnStart = source.indexOf("public void spawnOriginalMobs"); + int spawnEnd = source.indexOf("private static WeightedList", spawnStart); + String spawn = source.substring(spawnStart, spawnEnd); + + assertTrue(spawn.contains("customBiomeSource.getVanillaSpawnBiome(visibleBiome)")); + assertTrue(spawn.contains("NaturalSpawner.spawnMobsForChunkGeneration(")); + assertTrue(spawn.contains("region.getBiome(center.getWorldPosition().atY(region.getMaxY()))")); + assertTrue(spawn.contains("new LegacyRandomSource(RandomSupport.generateUniqueSeed())")); + assertTrue(spawn.contains("random.setDecorationSeed(region.getSeed()")); + assertFalse(spawn.contains("delegate.spawnOriginalMobs")); + } } diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java index 46e83cf07..514e50b39 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java @@ -971,7 +971,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { final Location spawn = w.getSpawnLocation(); for (Player i : getServer().getOnlinePlayers()) { final Runnable playerTask = () -> { - i.setGameMode(GameMode.SPECTATOR); + i.setGameMode(GameMode.CREATIVE); BukkitPlatform.teleportAsync(i, spawn); }; if (!J.runEntity(i, playerTask)) { diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStudio.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStudio.java index 1d4e64c83..ce4550d26 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStudio.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStudio.java @@ -657,7 +657,7 @@ public class CommandStudio implements DirectorExecutor { .getTarget() .getWorld(); BukkitPlatform.teleportAsync(player, BukkitWorldBinding.spawnLocation(studioWorld)) - .thenRun(() -> player.setGameMode(GameMode.SPECTATOR)); + .thenRun(() -> player.setGameMode(GameMode.CREATIVE)); } @Director(description = "Update your dimension projects VSCode workspace", descriptionKey = "iris.director.commandstudio.director.update_your_dimension_projects_vscode_workspace") diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/runtime/StudioPlayerModeContractTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/runtime/StudioPlayerModeContractTest.java new file mode 100644 index 000000000..6e316e110 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/runtime/StudioPlayerModeContractTest.java @@ -0,0 +1,24 @@ +package art.arcane.iris.core.runtime; + +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class StudioPlayerModeContractTest { + @Test + public void studioEntryKeepsPlayersEligibleForNaturalSpawning() throws IOException { + String plugin = Files.readString(Path.of("src/main/java/art/arcane/iris/Iris.java")); + String commands = Files.readString(Path.of( + "src/main/java/art/arcane/iris/core/commands/CommandStudio.java")); + + assertFalse(plugin.contains("GameMode.SPECTATOR")); + assertFalse(commands.contains("GameMode.SPECTATOR")); + assertTrue(plugin.contains("GameMode.CREATIVE")); + assertTrue(commands.contains("GameMode.CREATIVE")); + } +} 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 e1de286f4..7ac11866c 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 @@ -63,6 +63,7 @@ import net.minecraft.world.entity.MobCategory; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.NaturalSpawner; import net.minecraft.world.level.NoiseColumn; import net.minecraft.world.level.StructureManager; import net.minecraft.world.level.WorldGenLevel; @@ -81,6 +82,7 @@ import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; import net.minecraft.world.level.chunk.LevelChunkSection; import net.minecraft.world.level.levelgen.GenerationStep; import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.LegacyRandomSource; import net.minecraft.world.level.levelgen.RandomState; import net.minecraft.world.level.levelgen.RandomSupport; import net.minecraft.world.level.levelgen.WorldgenRandom; @@ -1191,6 +1193,15 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { @Override public void spawnOriginalMobs(WorldGenRegion region) { + Registry registry = region.registryAccess().lookupOrThrow(Registries.BIOME); + initializeVanillaSpawnBiomes(registry); + ChunkPos center = region.getCenter(); + Holder visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY())); + Holder vanillaBiome = vanillaSpawnBiomes.get(visibleBiome.value()); + WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed())); + random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ()); + NaturalSpawner.spawnMobsForChunkGeneration( + region, vanillaBiome == null ? visibleBiome : vanillaBiome, center, random); } @Override diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldManager.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldManager.java index 64ee6630e..8f894e41f 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldManager.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldManager.java @@ -712,7 +712,7 @@ public final class ModdedWorldManager implements EngineWorldManager { } private boolean isEntitySpawningEnabledForCurrentWorld() { - return entitySpawningEnabled(engine.isStudio(), IrisSettings.get().getStudio().isEnableEntitySpawning()); + return entitySpawningEnabled(engine.isStudio(), IrisSettings.get().getStudio().isEntitySpawning()); } static boolean entitySpawningEnabled(boolean studio, boolean studioSetting) { diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedChunkGeneratorSpawnTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedChunkGeneratorSpawnTest.java index 9b2c34c5f..e2b5f5e46 100644 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedChunkGeneratorSpawnTest.java +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedChunkGeneratorSpawnTest.java @@ -9,9 +9,13 @@ import net.minecraft.world.level.biome.MobSpawnSettings; import org.junit.Test; import org.junit.BeforeClass; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; public class IrisModdedChunkGeneratorSpawnTest { @BeforeClass @@ -44,4 +48,20 @@ public class IrisModdedChunkGeneratorSpawnTest { assertEquals(explicitCow, entries.get(2).value()); assertEquals(3, entries.get(2).weight()); } + + @Test + public void vanillaChunkGenerationMobsUseTheVisibleBiomesVanillaDerivative() throws IOException { + Path sourcePath = Path.of(System.getProperty("iris.moddedCommonSources"), + "art/arcane/iris/modded/IrisModdedChunkGenerator.java"); + String source = Files.readString(sourcePath); + int spawnStart = source.indexOf("public void spawnOriginalMobs"); + int spawnEnd = source.indexOf("@Override", spawnStart + 1); + String spawn = source.substring(spawnStart, spawnEnd); + + assertTrue(spawn.contains("initializeVanillaSpawnBiomes(registry)")); + assertTrue(spawn.contains("vanillaSpawnBiomes.get(visibleBiome.value())")); + assertTrue(spawn.contains("NaturalSpawner.spawnMobsForChunkGeneration(")); + assertTrue(spawn.contains("new LegacyRandomSource(RandomSupport.generateUniqueSeed())")); + assertTrue(spawn.contains("random.setDecorationSeed(region.getSeed()")); + } } diff --git a/core/src/main/java/art/arcane/iris/core/IrisSettings.java b/core/src/main/java/art/arcane/iris/core/IrisSettings.java index 7a8cd28e9..338db3584 100644 --- a/core/src/main/java/art/arcane/iris/core/IrisSettings.java +++ b/core/src/main/java/art/arcane/iris/core/IrisSettings.java @@ -295,7 +295,7 @@ public class IrisSettings { public boolean studio = true; public boolean openVSCode = true; public boolean disableTimeAndWeather = true; - public boolean enableEntitySpawning = false; + public boolean entitySpawning = true; public boolean autoStartDefaultStudio = false; } diff --git a/core/src/main/java/art/arcane/iris/core/project/IrisProject.java b/core/src/main/java/art/arcane/iris/core/project/IrisProject.java index b6b5bfde0..d7ba2bc94 100644 --- a/core/src/main/java/art/arcane/iris/core/project/IrisProject.java +++ b/core/src/main/java/art/arcane/iris/core/project/IrisProject.java @@ -295,7 +295,7 @@ public class IrisProject { } if (sender.isPlayer() && sender.player() != null) { - J.runEntity(sender.player(), () -> sender.player().setGameMode(GameMode.SPECTATOR)); + J.runEntity(sender.player(), () -> sender.player().setGameMode(GameMode.CREATIVE)); } activeProvider = IrisToolbelt.access(result.world()); maintenanceWorld = result.world(); diff --git a/core/src/main/java/art/arcane/iris/core/runtime/WorldRuntimeControlService.java b/core/src/main/java/art/arcane/iris/core/runtime/WorldRuntimeControlService.java index 506173fee..5a47f3e1c 100644 --- a/core/src/main/java/art/arcane/iris/core/runtime/WorldRuntimeControlService.java +++ b/core/src/main/java/art/arcane/iris/core/runtime/WorldRuntimeControlService.java @@ -82,6 +82,7 @@ public final class WorldRuntimeControlService { IrisServices.get(art.arcane.iris.core.link.MultiverseCoreLink.class).removeFromConfig(world); setIntGameRule(world, 0, "SPAWN_CHUNK_RADIUS", "spawnChunkRadius"); + enableStudioEntitySpawning(world); if (!IrisSettings.get().getStudio().isDisableTimeAndWeather()) { return true; } @@ -100,17 +101,12 @@ public final class WorldRuntimeControlService { applyStudioWorldRules(world); setBooleanGameRule(world, false, "DO_FIRE_TICK", "doFireTick"); - setBooleanGameRule(world, false, "DO_MOB_SPAWNING", "doMobSpawning"); setBooleanGameRule(world, false, "DO_MOB_LOOT", "doMobLoot"); - setBooleanGameRule(world, false, "DO_TRADER_SPAWNING", "doTraderSpawning"); - setBooleanGameRule(world, false, "DO_PATROL_SPAWNING", "doPatrolSpawning"); - setBooleanGameRule(world, false, "DO_INSOMNIA", "doInsomnia"); setBooleanGameRule(world, true, "DO_IMMEDIATE_RESPAWN", "doImmediateRespawn"); setBooleanGameRule(world, false, "FALL_DAMAGE", "fallDamage"); setBooleanGameRule(world, false, "FIRE_DAMAGE", "fireDamage"); setBooleanGameRule(world, false, "DROWNING_DAMAGE", "drowningDamage"); setBooleanGameRule(world, false, "FREEZE_DAMAGE", "freezeDamage"); - setBooleanGameRule(world, false, "DO_WARDEN_SPAWNING", "doWardenSpawning"); setBooleanGameRule(world, false, "MOB_GRIEFING", "mobGriefing"); setBooleanGameRule(world, false, "DO_TILE_DROPS", "doTileDrops"); setBooleanGameRule(world, true, "KEEP_INVENTORY", "keepInventory"); @@ -121,6 +117,14 @@ public final class WorldRuntimeControlService { return true; } + static void enableStudioEntitySpawning(World world) { + setBooleanGameRule(world, true, "DO_MOB_SPAWNING", "doMobSpawning"); + setBooleanGameRule(world, true, "DO_TRADER_SPAWNING", "doTraderSpawning"); + setBooleanGameRule(world, true, "DO_PATROL_SPAWNING", "doPatrolSpawning"); + setBooleanGameRule(world, true, "DO_INSOMNIA", "doInsomnia"); + setBooleanGameRule(world, true, "DO_WARDEN_SPAWNING", "doWardenSpawning"); + } + public boolean applyNoonTimeLock(World world) { if (world == null) { return false; diff --git a/core/src/main/java/art/arcane/iris/core/service/ObjectStudioSaveService.java b/core/src/main/java/art/arcane/iris/core/service/ObjectStudioSaveService.java index f2c55f8a6..4d5b7fd4c 100644 --- a/core/src/main/java/art/arcane/iris/core/service/ObjectStudioSaveService.java +++ b/core/src/main/java/art/arcane/iris/core/service/ObjectStudioSaveService.java @@ -40,8 +40,6 @@ import org.bukkit.block.Block; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.block.Action; -import org.bukkit.event.entity.CreatureSpawnEvent; -import org.bukkit.event.entity.EntitySpawnEvent; import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.event.world.WorldUnloadEvent; import org.bukkit.inventory.EquipmentSlot; @@ -129,26 +127,6 @@ public class ObjectStudioSaveService implements IrisService { unregister(event.getWorld()); } - @EventHandler(ignoreCancelled = true) - public void onCreatureSpawn(CreatureSpawnEvent event) { - if (!studios.containsKey(event.getLocation().getWorld().getUID())) return; - CreatureSpawnEvent.SpawnReason reason = event.getSpawnReason(); - if (reason == CreatureSpawnEvent.SpawnReason.CUSTOM - || reason == CreatureSpawnEvent.SpawnReason.COMMAND - || reason == CreatureSpawnEvent.SpawnReason.SPAWNER_EGG) { - return; - } - event.setCancelled(true); - } - - @EventHandler(ignoreCancelled = true) - public void onEntitySpawn(EntitySpawnEvent event) { - if (event instanceof CreatureSpawnEvent) return; - if (!studios.containsKey(event.getLocation().getWorld().getUID())) return; - if (event.getEntity() instanceof org.bukkit.entity.Player) return; - event.setCancelled(true); - } - @EventHandler(ignoreCancelled = true) public void onPlayerInteract(PlayerInteractEvent event) { if (event.getHand() != EquipmentSlot.HAND) return; diff --git a/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java b/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java index 5d38baf46..b79adf5a3 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java @@ -979,7 +979,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager { return true; } - return IrisSettings.get().getStudio().isEnableEntitySpawning(); + return IrisSettings.get().getStudio().isEntitySpawning(); } private KList spawnRandomly(List types) { diff --git a/core/src/test/java/art/arcane/iris/core/runtime/StudioEntitySpawningTest.java b/core/src/test/java/art/arcane/iris/core/runtime/StudioEntitySpawningTest.java new file mode 100644 index 000000000..3d2a6de62 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/runtime/StudioEntitySpawningTest.java @@ -0,0 +1,55 @@ +package art.arcane.iris.core.runtime; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.service.ObjectStudioSaveService; +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class StudioEntitySpawningTest { + @Test + public void studioEntitySpawningDefaultsToEnabled() { + assertTrue(new IrisSettings.IrisSettingsStudio().isEntitySpawning()); + } + + @Test + public void studioWorldRulesEnableVanillaMobSpawning() throws IOException { + String source = Files.readString(Path.of( + "src/main/java/art/arcane/iris/core/runtime/WorldRuntimeControlService.java")); + int helperStart = source.indexOf("static void enableStudioEntitySpawning"); + int helperEnd = source.indexOf("public boolean applyNoonTimeLock", helperStart); + String helper = source.substring(helperStart, helperEnd); + + assertTrue(source.contains("enableStudioEntitySpawning(world);")); + assertTrue(helper.contains("setBooleanGameRule(world, true, \"DO_MOB_SPAWNING\"")); + assertTrue(helper.contains("setBooleanGameRule(world, true, \"DO_TRADER_SPAWNING\"")); + assertTrue(helper.contains("setBooleanGameRule(world, true, \"DO_PATROL_SPAWNING\"")); + assertTrue(helper.contains("setBooleanGameRule(world, true, \"DO_INSOMNIA\"")); + assertTrue(helper.contains("setBooleanGameRule(world, true, \"DO_WARDEN_SPAWNING\"")); + assertFalse(helper.contains("setBooleanGameRule(world, false, \"DO_MOB_SPAWNING\"")); + } + + @Test + public void generalStudioUsesAPlayerModeEligibleForNaturalSpawning() throws IOException { + String source = Files.readString(Path.of( + "src/main/java/art/arcane/iris/core/project/IrisProject.java")); + + assertFalse(source.contains("GameMode.SPECTATOR")); + assertTrue(source.contains("GameMode.CREATIVE")); + } + + @Test + public void objectStudioDoesNotCancelEntitySpawns() { + boolean hasSpawnCanceller = Arrays.stream(ObjectStudioSaveService.class.getDeclaredMethods()) + .anyMatch(method -> method.getName().equals("onCreatureSpawn") + || method.getName().equals("onEntitySpawn")); + + assertFalse(hasSpawnCanceller); + } +}