diff --git a/README.md b/README.md index 90e5c4a..e41c5a1 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ The plugin JAR is written to `build/libs/`. /basesettings status /basesettings upgrade /basesettings pocket upgrade +/basesettings pocket mobs /basesettings visitors /basesettings navigation /basesettings flight @@ -43,7 +44,7 @@ After 250 Survival-mode block placements anywhere by default, the spawnable over `/base` has a stationary warm-up. Looking around is allowed, while movement between blocks, damage, teleportation, world changes, death, logout, and conflicting teleport commands cancel it without consuming the cooldown. -Base IV owners can purchase and expand a persistent Pocket Base with `/basesettings pocket upgrade`. Activating a complete diamond-block portal frame inside the normal base with flint and steel opens one public entrance to the owner's grass platform in a private void world. +Base IV owners can purchase and expand a persistent Pocket Base with `/basesettings pocket upgrade`. Activating a complete diamond-block portal frame inside the normal base with flint and steel opens one public entrance to the owner's grass platform in a private void world. Natural hostile and passive mob spawning is disabled by default and can be controlled by the owner with `/basesettings pocket mobs `. ## Administration diff --git a/design/log.md b/design/log.md index af96eca..160a286 100644 --- a/design/log.md +++ b/design/log.md @@ -91,3 +91,10 @@ description: Chronological record of material decisions affecting the Spigot Bas - Used the live size-tier radius so expansion and relocation immediately update the visibility threshold. - Added exact-boundary and controller integration coverage. - Verified the change with `./gradlew clean check jar`. + +## 2026-08-24 — Pocket Base mob-spawning control + +- Disabled natural hostile and passive mob spawning by default in new and existing Pocket Bases without a saved preference. +- Added the persisted owner setting `/basesettings pocket mobs enable|disable`, contextual autocomplete, and status reporting. +- Reapplied saved spawn flags when Pocket Base worlds load and restored the prior world setting when persistence fails. +- Verified the feature with `./gradlew clean check jar`. diff --git a/design/user-stories/index.md b/design/user-stories/index.md index df41ba7..3de5763 100644 --- a/design/user-stories/index.md +++ b/design/user-stories/index.md @@ -23,3 +23,4 @@ description: Catalog of user stories for the Spigot Base plugin. 15. [US-015: Unlock a Pocket Base](us-015-unlock-a-pocket-base.md) 16. [US-016: Build and use Pocket Base portals](us-016-build-and-use-pocket-base-portals.md) 17. [US-017: Expand a Pocket Base](us-017-expand-a-pocket-base.md) +18. [US-018: Control Pocket Base mob spawning](us-018-control-pocket-base-mob-spawning.md) diff --git a/design/user-stories/us-018-control-pocket-base-mob-spawning.md b/design/user-stories/us-018-control-pocket-base-mob-spawning.md new file mode 100644 index 0000000..7c1d4d2 --- /dev/null +++ b/design/user-stories/us-018-control-pocket-base-mob-spawning.md @@ -0,0 +1,28 @@ +--- +type: User Story +title: "US-018: Control Pocket Base mob spawning" +description: Let Pocket Base owners control natural mob spawning in their private world, with spawning disabled by default. +status: done +--- + +# US-018: Control Pocket Base mob spawning + +As a **Pocket Base owner**, I want to enable or disable natural mob spawning in my Pocket Base so that I can choose whether the private world supports normal mob activity. + +## Acceptance criteria + +- [x] Natural mob spawning is disabled by default in every new Pocket Base and in existing Pocket Bases that have no saved preference. +- [x] A Pocket Base owner can use `/basesettings pocket mobs enable|disable` to control natural mob spawning in their own Pocket Base. +- [x] The setting controls both hostile and passive natural spawning only in the owner's Pocket Base. +- [x] The command is unavailable until Pocket Base I is unlocked. +- [x] The command and its modes are offered through contextual autocomplete. +- [x] `/basesettings status` displays the current Pocket Base mob-spawning setting. +- [x] The preference persists across restarts and is reapplied when the Pocket Base world loads. +- [x] Disabling natural spawning does not remove existing mobs or prevent explicitly spawned or summoned mobs. +- [x] Persistence or world-application failures produce a clear failure message without reporting a successful change. + +## Related + +- [US-015: Unlock a Pocket Base](us-015-unlock-a-pocket-base.md) +- [US-016: Build and use Pocket Base portals](us-016-build-and-use-pocket-base-portals.md) +- [US-017: Expand a Pocket Base](us-017-expand-a-pocket-base.md) diff --git a/src/main/java/games/dmg/spigotbase/BaseSettingsCommand.java b/src/main/java/games/dmg/spigotbase/BaseSettingsCommand.java index 53767ad..35f3b96 100644 --- a/src/main/java/games/dmg/spigotbase/BaseSettingsCommand.java +++ b/src/main/java/games/dmg/spigotbase/BaseSettingsCommand.java @@ -66,6 +66,10 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter { && arguments[1].equalsIgnoreCase("upgrade")) { return purchasePocketUpgrade(player, state); } + if (arguments.length == 3 && arguments[0].equalsIgnoreCase("pocket") + && arguments[1].equalsIgnoreCase("mobs")) { + return updatePocketMobSpawning(player, arguments[2]); + } if (arguments.length == 2 && arguments[0].equalsIgnoreCase("visitors")) { return updateVisitors(player, state, arguments[1]); } @@ -176,6 +180,37 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter { return true; } + private boolean updatePocketMobSpawning(Player player, String mode) { + if (pocketBases == null) { + player.sendMessage(ChatColor.RED + "Pocket Bases are currently unavailable."); + return true; + } + PocketBaseState current = pocketBases.state(player.getUniqueId()); + if (current.level() < 1) { + player.sendMessage(ChatColor.RED + "Pocket Base I is still locked."); + return true; + } + Boolean enabled = enabledMode(mode); + if (enabled == null) { + sendUsage(player); + return true; + } + final PocketBaseState updated; + try { + updated = pocketBases.setMobSpawning(player.getUniqueId(), enabled); + } catch (IOException | RuntimeException exception) { + player.sendMessage(ChatColor.RED + + "The Pocket Base mob-spawning setting could not be changed."); + return true; + } + player.sendMessage(ChatColor.YELLOW + "Pocket Base mob spawning is now " + + (updated.mobSpawningEnabled() + ? ChatColor.GREEN + "enabled" + : ChatColor.RED + "disabled") + + ChatColor.YELLOW + "."); + return true; + } + private boolean updateVisitors(Player player, PlayerState state, String mode) { if (state.baseLevel() < 4) { player.sendMessage(ChatColor.RED + "Base IV visitor access is still locked."); @@ -408,14 +443,17 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter { } private void showPocketPath(Player player, PlayerState owner) { - int level = pocketBases == null ? 0 : pocketBases.state(owner.playerId()).level(); - int price = level == 0 + PocketBaseState pocket = pocketBases == null + ? PocketBaseState.locked(owner.playerId()) + : pocketBases.state(owner.playerId()); + int price = pocket.level() == 0 ? settings.current().pocketBaseUnlockCost() : settings.current().pocketBaseUpgradeCost(); player.sendMessage(colorForPrerequisite(owner.baseLevel() >= 4 && owner.base().isPresent()) - + "Pocket Base " + level + ": " + ChatColor.GRAY + price + " " + + "Pocket Base " + pocket.level() + ": " + ChatColor.GRAY + price + " " + settings.current().pocketBaseCurrencyMaterial().toLowerCase(Locale.ROOT) - + " → /basesettings pocket upgrade"); + + " → /basesettings pocket upgrade; mob spawning=" + + (pocket.mobSpawningEnabled() ? "enabled" : "disabled")); } private void showCooldownPath(Player player, PlayerState state) { @@ -447,13 +485,18 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter { List modes = arguments[0].equalsIgnoreCase("visitors") ? VISITOR_MODES : switch (arguments[0].toLowerCase(Locale.ROOT)) { - case "pocket" -> List.of("upgrade"); + case "pocket" -> List.of("upgrade", "mobs"); case "navigation", "flight", "border", "spawnable", "bossbar" -> ENABLE_MODES; default -> List.of(); }; String prefix = arguments[1].toLowerCase(Locale.ROOT); return modes.stream().filter(mode -> mode.startsWith(prefix)).toList(); } + if (arguments.length == 3 && arguments[0].equalsIgnoreCase("pocket") + && arguments[1].equalsIgnoreCase("mobs")) { + String prefix = arguments[2].toLowerCase(Locale.ROOT); + return ENABLE_MODES.stream().filter(mode -> mode.startsWith(prefix)).toList(); + } return List.of(); } @@ -513,8 +556,8 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter { private static void sendUsage(Player player) { player.sendMessage(ChatColor.RED + "Usage: /basesettings " - + "[status|upgrade|pocket upgrade|visitors " - + "|navigation " + + "[status|upgrade|pocket upgrade|pocket mobs " + + "|visitors |navigation " + "|flight |border " + "|spawnable |bossbar ]"); } diff --git a/src/main/java/games/dmg/spigotbase/PocketBaseManager.java b/src/main/java/games/dmg/spigotbase/PocketBaseManager.java index 653773d..f254145 100644 --- a/src/main/java/games/dmg/spigotbase/PocketBaseManager.java +++ b/src/main/java/games/dmg/spigotbase/PocketBaseManager.java @@ -60,6 +60,27 @@ final class PocketBaseManager { } } + PocketBaseState setMobSpawning(UUID ownerId, boolean enabled) throws IOException { + PocketBaseState current = states.state(ownerId); + if (current.level() < 1) { + throw new IllegalStateException("Pocket Base I is still locked"); + } + worlds.setMobSpawning(ownerId, enabled); + try { + return states.updateAndSave( + ownerId, + state -> state.withMobSpawningEnabled(enabled) + ); + } catch (IOException | RuntimeException exception) { + try { + worlds.setMobSpawning(ownerId, current.mobSpawningEnabled()); + } catch (RuntimeException rollbackFailure) { + exception.addSuppressed(rollbackFailure); + } + throw exception; + } + } + Optional ownerForPocketWorld(UUID worldId) { return worlds.ownerForWorld(worldId); } diff --git a/src/main/java/games/dmg/spigotbase/PocketBaseState.java b/src/main/java/games/dmg/spigotbase/PocketBaseState.java index 12525ae..59d93ff 100644 --- a/src/main/java/games/dmg/spigotbase/PocketBaseState.java +++ b/src/main/java/games/dmg/spigotbase/PocketBaseState.java @@ -6,7 +6,8 @@ import java.util.UUID; public record PocketBaseState( UUID ownerId, int level, - Optional entrance + Optional entrance, + boolean mobSpawningEnabled ) { public PocketBaseState { if (ownerId == null) { @@ -21,19 +22,31 @@ public record PocketBaseState( } } + public PocketBaseState( + UUID ownerId, + int level, + Optional entrance + ) { + this(ownerId, level, entrance, false); + } + public static PocketBaseState locked(UUID ownerId) { - return new PocketBaseState(ownerId, 0, Optional.empty()); + return new PocketBaseState(ownerId, 0, Optional.empty(), false); } public PocketBaseState withLevel(int newLevel) { - return new PocketBaseState(ownerId, newLevel, entrance); + return new PocketBaseState(ownerId, newLevel, entrance, mobSpawningEnabled); } public PocketBaseState withEntrance(PocketPortalLocation portal) { - return new PocketBaseState(ownerId, level, Optional.of(portal)); + return new PocketBaseState(ownerId, level, Optional.of(portal), mobSpawningEnabled); } public PocketBaseState withoutEntrance() { - return new PocketBaseState(ownerId, level, Optional.empty()); + return new PocketBaseState(ownerId, level, Optional.empty(), mobSpawningEnabled); + } + + public PocketBaseState withMobSpawningEnabled(boolean enabled) { + return new PocketBaseState(ownerId, level, entrance, enabled); } } diff --git a/src/main/java/games/dmg/spigotbase/PocketBaseWorldService.java b/src/main/java/games/dmg/spigotbase/PocketBaseWorldService.java index 806f20f..58e4ef5 100644 --- a/src/main/java/games/dmg/spigotbase/PocketBaseWorldService.java +++ b/src/main/java/games/dmg/spigotbase/PocketBaseWorldService.java @@ -27,7 +27,7 @@ final class PocketBaseWorldService { void loadExisting(Map states) { for (PocketBaseState state : states.values()) { if (state.level() > 0) { - ensureWorld(state.ownerId()); + setMobSpawning(state.ownerId(), state.mobSpawningEnabled()); } } } @@ -66,6 +66,10 @@ final class PocketBaseWorldService { return ensureWorld(ownerId); } + void setMobSpawning(UUID ownerId, boolean enabled) { + ensureWorld(ownerId).setSpawnFlags(enabled, enabled); + } + Location arrival(UUID ownerId) { return new Location(ensureWorld(ownerId), 0.5, PLATFORM_Y + 1, 5.5, 180.0f, 0.0f); } @@ -90,7 +94,8 @@ final class PocketBaseWorldService { private World ensureWorld(UUID ownerId) { String name = worldName(ownerId); World world = server.getWorld(name); - if (world == null) { + boolean created = world == null; + if (created) { WorldCreator creator = new WorldCreator(name) .environment(World.Environment.NORMAL) .generator(new VoidPocketChunkGenerator()) @@ -100,6 +105,9 @@ final class PocketBaseWorldService { if (world == null) { throw new IllegalStateException("Could not create Pocket Base world " + name); } + if (created) { + world.setSpawnFlags(false, false); + } ownersByWorld.put(world.getUID(), ownerId); return world; } diff --git a/src/main/java/games/dmg/spigotbase/YamlPocketBaseRepository.java b/src/main/java/games/dmg/spigotbase/YamlPocketBaseRepository.java index 97ae722..54c0ace 100644 --- a/src/main/java/games/dmg/spigotbase/YamlPocketBaseRepository.java +++ b/src/main/java/games/dmg/spigotbase/YamlPocketBaseRepository.java @@ -42,7 +42,8 @@ public final class YamlPocketBaseRepository { PocketBaseState state = new PocketBaseState( ownerId, level, - loadPortal(yaml, path + ".entrance") + loadPortal(yaml, path + ".entrance"), + yaml.getBoolean(path + ".mob-spawning-enabled", false) ); states.put(ownerId, state); } catch (IllegalArgumentException ignored) { @@ -61,6 +62,7 @@ public final class YamlPocketBaseRepository { for (PocketBaseState state : states.values()) { String path = "owners." + state.ownerId(); yaml.set(path + ".level", state.level()); + yaml.set(path + ".mob-spawning-enabled", state.mobSpawningEnabled()); state.entrance().ifPresent(portal -> savePortal(yaml, path + ".entrance", portal)); } Path temporary = Files.createTempFile(parent, "spigot-base-pocket-", ".yml"); diff --git a/src/test/java/games/dmg/spigotbase/BaseSettingsCommandTest.java b/src/test/java/games/dmg/spigotbase/BaseSettingsCommandTest.java index 88d7374..d82a44a 100644 --- a/src/test/java/games/dmg/spigotbase/BaseSettingsCommandTest.java +++ b/src/test/java/games/dmg/spigotbase/BaseSettingsCommandTest.java @@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -95,6 +96,16 @@ final class BaseSettingsCommandTest { List.of("upgrade"), command.onTabComplete(null, null, "basesettings", new String[] {"pocket", "u"}) ); + assertEquals( + List.of("mobs"), + command.onTabComplete(null, null, "basesettings", new String[] {"pocket", "m"}) + ); + assertEquals( + List.of("enable"), + command.onTabComplete( + null, null, "basesettings", new String[] {"pocket", "mobs", "e"} + ) + ); for (String setting : List.of("navigation", "flight", "border", "spawnable", "bossbar")) { assertEquals( List.of("disable"), @@ -198,6 +209,101 @@ final class BaseSettingsCommandTest { verify(inventory).setStorageContents(any(ItemStack[].class)); } + @Test + void pocketOwnerCanEnableNaturalMobSpawning() throws Exception { + UUID playerId = UUID.randomUUID(); + Player player = mock(Player.class); + when(player.getUniqueId()).thenReturn(playerId); + when(player.getName()).thenReturn("Builder"); + BaseStateManager stateManager = mock(BaseStateManager.class); + when(stateManager.player(playerId, "Builder")) + .thenReturn(PlayerState.newPlayer(playerId, "Builder")); + PocketBaseManager pocketBases = mock(PocketBaseManager.class); + when(pocketBases.state(playerId)).thenReturn( + new PocketBaseState(playerId, 1, java.util.Optional.empty(), false) + ); + when(pocketBases.setMobSpawning(playerId, true)).thenReturn( + new PocketBaseState(playerId, 1, java.util.Optional.empty(), true) + ); + BaseSettingsCommand command = new BaseSettingsCommand( + stateManager, + new PluginSettingsProvider(PluginSettings.from(Map.of())), + mock(BaseFlightController.class), + pocketBases + ); + + command.onCommand( + player, null, "basesettings", new String[] {"pocket", "mobs", "enable"} + ); + + verify(pocketBases).setMobSpawning(playerId, true); + verify(player).sendMessage(org.mockito.ArgumentMatchers.argThat( + message -> message.contains("mob spawning") && message.contains("enabled") + )); + } + + @Test + void failedPocketMobSpawningChangeReportsOnlyFailure() throws Exception { + UUID playerId = UUID.randomUUID(); + Player player = mock(Player.class); + when(player.getUniqueId()).thenReturn(playerId); + when(player.getName()).thenReturn("Builder"); + BaseStateManager stateManager = mock(BaseStateManager.class); + when(stateManager.player(playerId, "Builder")) + .thenReturn(PlayerState.newPlayer(playerId, "Builder")); + PocketBaseManager pocketBases = mock(PocketBaseManager.class); + when(pocketBases.state(playerId)).thenReturn( + new PocketBaseState(playerId, 1, java.util.Optional.empty(), false) + ); + when(pocketBases.setMobSpawning(playerId, true)) + .thenThrow(new IOException("save failed")); + BaseSettingsCommand command = new BaseSettingsCommand( + stateManager, + new PluginSettingsProvider(PluginSettings.from(Map.of())), + mock(BaseFlightController.class), + pocketBases + ); + + command.onCommand( + player, null, "basesettings", new String[] {"pocket", "mobs", "enable"} + ); + + verify(player).sendMessage(org.mockito.ArgumentMatchers.argThat( + message -> message.contains("could not be changed") + )); + verify(player, never()).sendMessage(org.mockito.ArgumentMatchers.argThat( + message -> message.contains("is now") + )); + } + + @Test + void lockedPlayerCannotEnablePocketMobSpawning() throws Exception { + UUID playerId = UUID.randomUUID(); + Player player = mock(Player.class); + when(player.getUniqueId()).thenReturn(playerId); + when(player.getName()).thenReturn("Builder"); + BaseStateManager stateManager = mock(BaseStateManager.class); + when(stateManager.player(playerId, "Builder")) + .thenReturn(PlayerState.newPlayer(playerId, "Builder")); + PocketBaseManager pocketBases = mock(PocketBaseManager.class); + when(pocketBases.state(playerId)).thenReturn(PocketBaseState.locked(playerId)); + BaseSettingsCommand command = new BaseSettingsCommand( + stateManager, + new PluginSettingsProvider(PluginSettings.from(Map.of())), + mock(BaseFlightController.class), + pocketBases + ); + + command.onCommand( + player, null, "basesettings", new String[] {"pocket", "mobs", "enable"} + ); + + verify(pocketBases, never()).setMobSpawning(playerId, true); + verify(player).sendMessage(org.mockito.ArgumentMatchers.argThat( + message -> message.contains("Pocket Base I") && message.contains("locked") + )); + } + @Test void failedPocketUpgradeRestoresPayment() throws Exception { UUID playerId = UUID.randomUUID(); @@ -236,6 +342,34 @@ final class BaseSettingsCommandTest { assertEquals(64, restored[0].getAmount()); } + @Test + void statusDisplaysPocketMobSpawningPreference() { + UUID playerId = UUID.randomUUID(); + Player player = mock(Player.class); + when(player.getUniqueId()).thenReturn(playerId); + when(player.getName()).thenReturn("Builder"); + BaseStateManager stateManager = mock(BaseStateManager.class); + when(stateManager.player(playerId, "Builder")) + .thenReturn(PlayerState.newPlayer(playerId, "Builder")); + PocketBaseManager pocketBases = mock(PocketBaseManager.class); + when(pocketBases.state(playerId)).thenReturn( + new PocketBaseState(playerId, 1, java.util.Optional.empty(), false) + ); + BaseSettingsCommand command = new BaseSettingsCommand( + stateManager, + new PluginSettingsProvider(PluginSettings.from(Map.of())), + mock(BaseFlightController.class), + pocketBases + ); + + command.onCommand(player, null, "basesettings", new String[] {"status"}); + + verify(player).sendMessage(org.mockito.ArgumentMatchers.argThat( + message -> message.contains("Pocket Base 1") + && message.contains("mob spawning=disabled") + )); + } + @Test void bossbarEnableIsIdempotent() { UUID playerId = UUID.randomUUID(); diff --git a/src/test/java/games/dmg/spigotbase/PocketBaseManagerTest.java b/src/test/java/games/dmg/spigotbase/PocketBaseManagerTest.java new file mode 100644 index 0000000..ef60b0d --- /dev/null +++ b/src/test/java/games/dmg/spigotbase/PocketBaseManagerTest.java @@ -0,0 +1,66 @@ +package games.dmg.spigotbase; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.function.UnaryOperator; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +final class PocketBaseManagerTest { + @Test + void persistsAndAppliesMobSpawningPreference() throws Exception { + UUID ownerId = UUID.randomUUID(); + PocketBaseState current = new PocketBaseState(ownerId, 1, Optional.empty(), false); + PocketBaseStateManager states = mock(PocketBaseStateManager.class); + PocketBaseWorldService worlds = mock(PocketBaseWorldService.class); + when(states.state(ownerId)).thenReturn(current); + when(states.updateAndSave(org.mockito.ArgumentMatchers.eq(ownerId), + org.mockito.ArgumentMatchers.any())).thenAnswer(invocation -> { + @SuppressWarnings("unchecked") + UnaryOperator operation = invocation.getArgument(1); + return operation.apply(current); + }); + PocketBaseManager manager = new PocketBaseManager( + states, + worlds, + new PluginSettingsProvider(PluginSettings.from(Map.of())) + ); + + manager.setMobSpawning(ownerId, true); + + InOrder order = inOrder(worlds, states); + order.verify(worlds).setMobSpawning(ownerId, true); + order.verify(states).updateAndSave( + org.mockito.ArgumentMatchers.eq(ownerId), org.mockito.ArgumentMatchers.any() + ); + } + + @Test + void restoresWorldSettingWhenPersistenceFails() throws Exception { + UUID ownerId = UUID.randomUUID(); + PocketBaseState current = new PocketBaseState(ownerId, 1, Optional.empty(), false); + PocketBaseStateManager states = mock(PocketBaseStateManager.class); + PocketBaseWorldService worlds = mock(PocketBaseWorldService.class); + when(states.state(ownerId)).thenReturn(current); + when(states.updateAndSave(org.mockito.ArgumentMatchers.eq(ownerId), + org.mockito.ArgumentMatchers.any())).thenThrow(new IOException("save failed")); + PocketBaseManager manager = new PocketBaseManager( + states, + worlds, + new PluginSettingsProvider(PluginSettings.from(Map.of())) + ); + + assertThrows(IOException.class, () -> manager.setMobSpawning(ownerId, true)); + + verify(worlds).setMobSpawning(ownerId, true); + verify(worlds).setMobSpawning(ownerId, false); + } +} diff --git a/src/test/java/games/dmg/spigotbase/PocketBaseWorldServiceTest.java b/src/test/java/games/dmg/spigotbase/PocketBaseWorldServiceTest.java index 4ceb186..e0d7f9f 100644 --- a/src/test/java/games/dmg/spigotbase/PocketBaseWorldServiceTest.java +++ b/src/test/java/games/dmg/spigotbase/PocketBaseWorldServiceTest.java @@ -9,6 +9,7 @@ import static org.mockito.Mockito.when; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.UUID; import org.bukkit.Material; import org.bukkit.Server; @@ -41,9 +42,30 @@ final class PocketBaseWorldServiceTest { verify(blocks.get(new BlockPosition(-2, 65, 0))) .setType(Material.DIAMOND_BLOCK, false); verify(world).setSpawnLocation(0, 65, 5); + verify(world).setSpawnFlags(false, false); assertEquals(ownerId, service.ownerForWorld(worldId).orElseThrow()); } + @Test + void reappliesSavedMobSpawningPreferenceWhenWorldLoads() { + UUID ownerId = UUID.randomUUID(); + Server server = mock(Server.class); + World world = mock(World.class); + when(server.getWorld(org.mockito.ArgumentMatchers.anyString())).thenReturn(world); + when(world.getUID()).thenReturn(UUID.randomUUID()); + PocketBaseWorldService service = new PocketBaseWorldService( + server, + new PluginSettingsProvider(PluginSettings.from(Map.of())) + ); + + service.loadExisting(Map.of( + ownerId, + new PocketBaseState(ownerId, 1, Optional.empty(), true) + )); + + verify(world).setSpawnFlags(true, true); + } + @Test void laterLevelAddsRingWithoutOverwritingExistingTerrain() { UUID ownerId = UUID.randomUUID(); diff --git a/src/test/java/games/dmg/spigotbase/YamlPocketBaseRepositoryTest.java b/src/test/java/games/dmg/spigotbase/YamlPocketBaseRepositoryTest.java index f38039b..e2efc37 100644 --- a/src/test/java/games/dmg/spigotbase/YamlPocketBaseRepositoryTest.java +++ b/src/test/java/games/dmg/spigotbase/YamlPocketBaseRepositoryTest.java @@ -1,7 +1,9 @@ package games.dmg.spigotbase; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; import java.util.Optional; @@ -22,7 +24,8 @@ final class YamlPocketBaseRepositoryTest { 7, Optional.of(new PocketPortalLocation( worldId, "world", 10, 65, -4, PocketPortalAxis.X - )) + )), + true ); YamlPocketBaseRepository repository = new YamlPocketBaseRepository( temporaryDirectory.resolve("pocket-bases.yml") @@ -32,4 +35,14 @@ final class YamlPocketBaseRepositoryTest { assertEquals(expected, repository.load().get(ownerId)); } + + @Test + void existingRecordWithoutMobPreferenceDefaultsToDisabled() throws Exception { + UUID ownerId = UUID.randomUUID(); + Path stateFile = temporaryDirectory.resolve("pocket-bases.yml"); + Files.writeString(stateFile, "owners:\n " + ownerId + ":\n level: 1\n"); + YamlPocketBaseRepository repository = new YamlPocketBaseRepository(stateFile); + + assertFalse(repository.load().get(ownerId).mobSpawningEnabled()); + } }