From b2ba4d5821140b28d207588c83b9c31e668d0ed6 Mon Sep 17 00:00:00 2001 From: Dylan Garvis Date: Mon, 24 Aug 2026 10:41:44 -0400 Subject: [PATCH] feat(pocket-base): split hostile and passive mob spawning --- README.md | 4 +- design/log.md | 5 +- ...us-018-control-pocket-base-mob-spawning.md | 24 ++++--- .../dmg/spigotbase/BaseSettingsCommand.java | 70 +++++++++++++------ .../dmg/spigotbase/PocketBaseManager.java | 42 +++++++++-- .../games/dmg/spigotbase/PocketBaseState.java | 60 +++++++--------- .../spigotbase/PocketBaseWorldService.java | 14 +++- .../spigotbase/YamlPocketBaseRepository.java | 23 +++++- .../spigotbase/BaseSettingsCommandTest.java | 66 +++++++++++------ .../dmg/spigotbase/PocketBaseManagerTest.java | 30 +++++--- .../PocketBaseWorldServiceTest.java | 7 +- .../YamlPocketBaseRepositoryTest.java | 18 ++++- 12 files changed, 246 insertions(+), 117 deletions(-) diff --git a/README.md b/README.md index dbaea15..f8ab768 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ The plugin JAR is written to `build/libs/`. /basesettings status /basesettings upgrade /basesettings pocket upgrade -/basesettings pocket mobs +/basesettings pocket mobs /basesettings pocket type /basesettings visitors /basesettings navigation @@ -45,7 +45,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. The owner can move the return portal by building and igniting another complete frame inside the unlocked Pocket Base boundary; only the newly activated return portal remains functional. Natural hostile and passive mob spawning is disabled by default and can be controlled by the owner with `/basesettings pocket mobs `. +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. The owner can move the return portal by building and igniting another complete frame inside the unlocked Pocket Base boundary; only the newly activated return portal remains functional. Natural hostile and passive mob spawning is disabled by default. Owners can control each category independently with `/basesettings pocket mobs `. Owners can change biome metadata without altering Pocket Base blocks, entities, inventories, or portals by using `/basesettings pocket type `. Void uses `the_void`; Nether and Overworld accept their compatible vanilla biome names, such as `crimson_forest` or `plains`. Each change defaults to 16 netherite blocks, is configurable, and affects applicable natural mob selection when spawning is enabled. The Pocket Base remains a void-generated normal-environment world; dimension-specific mechanics do not change. diff --git a/design/log.md b/design/log.md index 22ea55d..ccb13b6 100644 --- a/design/log.md +++ b/design/log.md @@ -95,8 +95,9 @@ description: Chronological record of material decisions affecting the Spigot Bas ## 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. +- Added independent persisted owner settings through `/basesettings pocket mobs `, contextual autocomplete, and status reporting. +- Migrated each legacy combined mob-spawning preference to both categories to preserve existing behavior. +- Reapplied both saved spawn flags when Pocket Base worlds load and restored both prior world settings when persistence fails. - Verified the feature with `./gradlew clean check jar`. ## 2026-08-24 — Relocatable Pocket Base return portals 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 index 360220d..b60afda 100644 --- a/design/user-stories/us-018-control-pocket-base-mob-spawning.md +++ b/design/user-stories/us-018-control-pocket-base-mob-spawning.md @@ -1,25 +1,27 @@ --- 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. +description: Let Pocket Base owners control hostile and passive natural mob spawning independently in their private world, with both 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. +As a **Pocket Base owner**, I want to enable or disable hostile and passive natural mob spawning independently in my Pocket Base so that I can choose which kinds of normal mob activity its private world supports. ## 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. +- [x] Hostile and passive natural mob spawning are both 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 hostile enable|disable` to control hostile natural mob spawning in their own Pocket Base. +- [x] A Pocket Base owner can use `/basesettings pocket mobs passive enable|disable` to control passive natural mob spawning in their own Pocket Base. +- [x] Changing one mob category does not change the other category. +- [x] The commands are unavailable until Pocket Base I is unlocked. +- [x] The commands, categories, and modes are offered through contextual autocomplete. +- [x] `/basesettings status` displays the current hostile and passive Pocket Base mob-spawning settings. +- [x] Both preferences persist across restarts and are reapplied when the Pocket Base world loads. +- [x] Existing saved `mob-spawning-enabled` values migrate to both category preferences, preserving prior behavior. +- [x] Disabling either natural-spawning category does not remove existing mobs or prevent explicitly spawned or summoned mobs. +- [x] Persistence or world-application failures produce a clear failure message, restore both prior world flags, and do not report a successful change. ## Related diff --git a/src/main/java/games/dmg/spigotbase/BaseSettingsCommand.java b/src/main/java/games/dmg/spigotbase/BaseSettingsCommand.java index caa13f2..e121583 100644 --- a/src/main/java/games/dmg/spigotbase/BaseSettingsCommand.java +++ b/src/main/java/games/dmg/spigotbase/BaseSettingsCommand.java @@ -69,9 +69,9 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter { && arguments[1].equalsIgnoreCase("upgrade")) { return purchasePocketUpgrade(player, state); } - if (arguments.length == 3 && arguments[0].equalsIgnoreCase("pocket") + if (arguments.length == 4 && arguments[0].equalsIgnoreCase("pocket") && arguments[1].equalsIgnoreCase("mobs")) { - return updatePocketMobSpawning(player, arguments[2]); + return updatePocketMobSpawning(player, arguments[2], arguments[3]); } if (arguments.length == 4 && arguments[0].equalsIgnoreCase("pocket") && arguments[1].equalsIgnoreCase("type")) { @@ -240,7 +240,11 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter { return true; } - private boolean updatePocketMobSpawning(Player player, String mode) { + private boolean updatePocketMobSpawning( + Player player, + String category, + String mode + ) { if (pocketBases == null) { player.sendMessage(ChatColor.RED + "Pocket Bases are currently unavailable."); return true; @@ -256,17 +260,30 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter { return true; } final PocketBaseState updated; + final boolean settingEnabled; try { - updated = pocketBases.setMobSpawning(player.getUniqueId(), enabled); + if (category.equalsIgnoreCase("hostile")) { + updated = pocketBases.setHostileMobSpawning( + player.getUniqueId(), enabled + ); + settingEnabled = updated.hostileMobSpawningEnabled(); + } else if (category.equalsIgnoreCase("passive")) { + updated = pocketBases.setPassiveMobSpawning( + player.getUniqueId(), enabled + ); + settingEnabled = updated.passiveMobSpawningEnabled(); + } else { + sendUsage(player); + return true; + } } 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") + player.sendMessage(ChatColor.YELLOW + "Pocket Base " + + category.toLowerCase(Locale.ROOT) + " mob spawning is now " + + (settingEnabled ? ChatColor.GREEN + "enabled" : ChatColor.RED + "disabled") + ChatColor.YELLOW + "."); return true; } @@ -514,8 +531,10 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter { + settings.current().pocketBaseCurrencyMaterial().toLowerCase(Locale.ROOT) + " → /basesettings pocket upgrade; type=" + pocket.biome().worldType().commandName() + "/" + pocket.biome().commandName() - + "; mob spawning=" - + (pocket.mobSpawningEnabled() ? "enabled" : "disabled")); + + "; hostile spawning=" + + (pocket.hostileMobSpawningEnabled() ? "enabled" : "disabled") + + "; passive spawning=" + + (pocket.passiveMobSpawningEnabled() ? "enabled" : "disabled")); } private void showCooldownPath(Player player, PlayerState state) { @@ -557,7 +576,9 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter { if (arguments.length == 3 && arguments[0].equalsIgnoreCase("pocket")) { String prefix = arguments[2].toLowerCase(Locale.ROOT); if (arguments[1].equalsIgnoreCase("mobs")) { - return ENABLE_MODES.stream().filter(mode -> mode.startsWith(prefix)).toList(); + return List.of("hostile", "passive").stream() + .filter(category -> category.startsWith(prefix)) + .toList(); } if (arguments[1].equalsIgnoreCase("type")) { return POCKET_WORLD_TYPES.stream() @@ -565,15 +586,23 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter { .toList(); } } - if (arguments.length == 4 && arguments[0].equalsIgnoreCase("pocket") - && arguments[1].equalsIgnoreCase("type")) { + if (arguments.length == 4 && arguments[0].equalsIgnoreCase("pocket")) { String prefix = arguments[3].toLowerCase(Locale.ROOT); - return PocketBaseWorldType.fromCommand(arguments[2]) - .map(PocketBaseBiome::commandNames) - .orElse(List.of()) - .stream() - .filter(biome -> biome.startsWith(prefix)) - .toList(); + if (arguments[1].equalsIgnoreCase("mobs") + && (arguments[2].equalsIgnoreCase("hostile") + || arguments[2].equalsIgnoreCase("passive"))) { + return ENABLE_MODES.stream() + .filter(mode -> mode.startsWith(prefix)) + .toList(); + } + if (arguments[1].equalsIgnoreCase("type")) { + return PocketBaseWorldType.fromCommand(arguments[2]) + .map(PocketBaseBiome::commandNames) + .orElse(List.of()) + .stream() + .filter(biome -> biome.startsWith(prefix)) + .toList(); + } } return List.of(); } @@ -634,7 +663,8 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter { private static void sendUsage(Player player) { player.sendMessage(ChatColor.RED + "Usage: /basesettings " - + "[status|upgrade|pocket upgrade|pocket mobs " + + "[status|upgrade|pocket upgrade|pocket mobs " + + " " + "|pocket type " + "|visitors |navigation " + "|flight |border " diff --git a/src/main/java/games/dmg/spigotbase/PocketBaseManager.java b/src/main/java/games/dmg/spigotbase/PocketBaseManager.java index a915490..b3dc14f 100644 --- a/src/main/java/games/dmg/spigotbase/PocketBaseManager.java +++ b/src/main/java/games/dmg/spigotbase/PocketBaseManager.java @@ -92,20 +92,48 @@ final class PocketBaseManager { } } - PocketBaseState setMobSpawning(UUID ownerId, boolean enabled) throws IOException { + PocketBaseState setHostileMobSpawning(UUID ownerId, boolean enabled) + throws IOException { PocketBaseState current = states.state(ownerId); + return setMobSpawning( + current, + enabled, + current.passiveMobSpawningEnabled(), + state -> state.withHostileMobSpawningEnabled(enabled) + ); + } + + PocketBaseState setPassiveMobSpawning(UUID ownerId, boolean enabled) + throws IOException { + PocketBaseState current = states.state(ownerId); + return setMobSpawning( + current, + current.hostileMobSpawningEnabled(), + enabled, + state -> state.withPassiveMobSpawningEnabled(enabled) + ); + } + + private PocketBaseState setMobSpawning( + PocketBaseState current, + boolean hostileEnabled, + boolean passiveEnabled, + java.util.function.UnaryOperator update + ) throws IOException { if (current.level() < 1) { throw new IllegalStateException("Pocket Base I is still locked"); } - worlds.setMobSpawning(ownerId, enabled); + UUID ownerId = current.ownerId(); + worlds.setMobSpawning(ownerId, hostileEnabled, passiveEnabled); try { - return states.updateAndSave( - ownerId, - state -> state.withMobSpawningEnabled(enabled) - ); + return states.updateAndSave(ownerId, update); } catch (IOException | RuntimeException exception) { try { - worlds.setMobSpawning(ownerId, current.mobSpawningEnabled()); + worlds.setMobSpawning( + ownerId, + current.hostileMobSpawningEnabled(), + current.passiveMobSpawningEnabled() + ); } catch (RuntimeException rollbackFailure) { exception.addSuppressed(rollbackFailure); } diff --git a/src/main/java/games/dmg/spigotbase/PocketBaseState.java b/src/main/java/games/dmg/spigotbase/PocketBaseState.java index 1541379..568905c 100644 --- a/src/main/java/games/dmg/spigotbase/PocketBaseState.java +++ b/src/main/java/games/dmg/spigotbase/PocketBaseState.java @@ -8,7 +8,8 @@ public record PocketBaseState( int level, Optional entrance, Optional returnPortal, - boolean mobSpawningEnabled, + boolean hostileMobSpawningEnabled, + boolean passiveMobSpawningEnabled, PocketBaseBiome biome ) { public PocketBaseState { @@ -32,73 +33,64 @@ public record PocketBaseState( Optional entrance ) { this( - ownerId, level, entrance, Optional.empty(), false, PocketBaseBiome.THE_VOID - ); - } - - public PocketBaseState( - UUID ownerId, - int level, - Optional entrance, - boolean mobSpawningEnabled - ) { - this( - ownerId, level, entrance, Optional.empty(), mobSpawningEnabled, - PocketBaseBiome.THE_VOID - ); - } - - public PocketBaseState( - UUID ownerId, - int level, - Optional entrance, - Optional returnPortal, - boolean mobSpawningEnabled - ) { - this( - ownerId, level, entrance, returnPortal, mobSpawningEnabled, + ownerId, level, entrance, Optional.empty(), false, false, PocketBaseBiome.THE_VOID ); } public static PocketBaseState locked(UUID ownerId) { return new PocketBaseState( - ownerId, 0, Optional.empty(), Optional.empty(), false, + ownerId, 0, Optional.empty(), Optional.empty(), false, false, PocketBaseBiome.THE_VOID ); } public PocketBaseState withLevel(int newLevel) { return new PocketBaseState( - ownerId, newLevel, entrance, returnPortal, mobSpawningEnabled, biome + ownerId, newLevel, entrance, returnPortal, hostileMobSpawningEnabled, + passiveMobSpawningEnabled, biome ); } public PocketBaseState withEntrance(PocketPortalLocation portal) { return new PocketBaseState( - ownerId, level, Optional.of(portal), returnPortal, mobSpawningEnabled, biome + ownerId, level, Optional.of(portal), returnPortal, + hostileMobSpawningEnabled, passiveMobSpawningEnabled, biome ); } public PocketBaseState withoutEntrance() { return new PocketBaseState( - ownerId, level, Optional.empty(), returnPortal, mobSpawningEnabled, biome + ownerId, level, Optional.empty(), returnPortal, hostileMobSpawningEnabled, + passiveMobSpawningEnabled, biome ); } public PocketBaseState withReturnPortal(PocketPortalLocation portal) { return new PocketBaseState( - ownerId, level, entrance, Optional.of(portal), mobSpawningEnabled, biome + ownerId, level, entrance, Optional.of(portal), hostileMobSpawningEnabled, + passiveMobSpawningEnabled, biome ); } - public PocketBaseState withMobSpawningEnabled(boolean enabled) { - return new PocketBaseState(ownerId, level, entrance, returnPortal, enabled, biome); + public PocketBaseState withHostileMobSpawningEnabled(boolean enabled) { + return new PocketBaseState( + ownerId, level, entrance, returnPortal, enabled, + passiveMobSpawningEnabled, biome + ); + } + + public PocketBaseState withPassiveMobSpawningEnabled(boolean enabled) { + return new PocketBaseState( + ownerId, level, entrance, returnPortal, hostileMobSpawningEnabled, + enabled, biome + ); } public PocketBaseState withBiome(PocketBaseBiome selectedBiome) { return new PocketBaseState( - ownerId, level, entrance, returnPortal, mobSpawningEnabled, selectedBiome + ownerId, level, entrance, returnPortal, hostileMobSpawningEnabled, + passiveMobSpawningEnabled, selectedBiome ); } } diff --git a/src/main/java/games/dmg/spigotbase/PocketBaseWorldService.java b/src/main/java/games/dmg/spigotbase/PocketBaseWorldService.java index b294efe..90ce058 100644 --- a/src/main/java/games/dmg/spigotbase/PocketBaseWorldService.java +++ b/src/main/java/games/dmg/spigotbase/PocketBaseWorldService.java @@ -42,7 +42,11 @@ final class PocketBaseWorldService { for (PocketBaseState state : states.values()) { if (state.level() > 0) { setBiome(state.ownerId(), state.level(), state.biome()); - setMobSpawning(state.ownerId(), state.mobSpawningEnabled()); + setMobSpawning( + state.ownerId(), + state.hostileMobSpawningEnabled(), + state.passiveMobSpawningEnabled() + ); } } } @@ -91,8 +95,12 @@ final class PocketBaseWorldService { return ensureWorld(ownerId); } - void setMobSpawning(UUID ownerId, boolean enabled) { - ensureWorld(ownerId).setSpawnFlags(enabled, enabled); + void setMobSpawning( + UUID ownerId, + boolean hostileEnabled, + boolean passiveEnabled + ) { + ensureWorld(ownerId).setSpawnFlags(hostileEnabled, passiveEnabled); } void setBiome(UUID ownerId, int level, PocketBaseBiome biome) { diff --git a/src/main/java/games/dmg/spigotbase/YamlPocketBaseRepository.java b/src/main/java/games/dmg/spigotbase/YamlPocketBaseRepository.java index e805ffe..b0c47ca 100644 --- a/src/main/java/games/dmg/spigotbase/YamlPocketBaseRepository.java +++ b/src/main/java/games/dmg/spigotbase/YamlPocketBaseRepository.java @@ -44,7 +44,8 @@ public final class YamlPocketBaseRepository { level, loadPortal(yaml, path + ".entrance"), loadPortal(yaml, path + ".return-portal"), - yaml.getBoolean(path + ".mob-spawning-enabled", false), + loadMobPreference(yaml, path, "hostile-mob-spawning-enabled"), + loadMobPreference(yaml, path, "passive-mob-spawning-enabled"), loadBiome(yaml, path) ); states.put(ownerId, state); @@ -64,7 +65,14 @@ 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()); + yaml.set( + path + ".hostile-mob-spawning-enabled", + state.hostileMobSpawningEnabled() + ); + yaml.set( + path + ".passive-mob-spawning-enabled", + state.passiveMobSpawningEnabled() + ); yaml.set(path + ".world-type", state.biome().worldType().commandName()); yaml.set(path + ".biome", state.biome().commandName()); state.entrance().ifPresent(portal -> savePortal(yaml, path + ".entrance", portal)); @@ -90,6 +98,17 @@ public final class YamlPocketBaseRepository { } } + private static boolean loadMobPreference( + YamlConfiguration yaml, + String path, + String categoryKey + ) { + return yaml.getBoolean( + path + "." + categoryKey, + yaml.getBoolean(path + ".mob-spawning-enabled", false) + ); + } + private static PocketBaseBiome loadBiome(YamlConfiguration yaml, String path) { String worldTypeName = yaml.getString(path + ".world-type"); String biomeName = yaml.getString(path + ".biome"); diff --git a/src/test/java/games/dmg/spigotbase/BaseSettingsCommandTest.java b/src/test/java/games/dmg/spigotbase/BaseSettingsCommandTest.java index e710ec1..84453da 100644 --- a/src/test/java/games/dmg/spigotbase/BaseSettingsCommandTest.java +++ b/src/test/java/games/dmg/spigotbase/BaseSettingsCommandTest.java @@ -119,10 +119,23 @@ final class BaseSettingsCommandTest { new String[] {"pocket", "type", "nether", "c"} ) ); + assertEquals( + List.of("hostile"), + command.onTabComplete( + null, null, "basesettings", new String[] {"pocket", "mobs", "h"} + ) + ); + assertEquals( + List.of("passive"), + command.onTabComplete( + null, null, "basesettings", new String[] {"pocket", "mobs", "p"} + ) + ); assertEquals( List.of("enable"), command.onTabComplete( - null, null, "basesettings", new String[] {"pocket", "mobs", "e"} + null, null, "basesettings", + new String[] {"pocket", "mobs", "hostile", "e"} ) ); for (String setting : List.of("navigation", "flight", "border", "spawnable", "bossbar")) { @@ -296,7 +309,7 @@ final class BaseSettingsCommandTest { } @Test - void pocketOwnerCanEnableNaturalMobSpawning() throws Exception { + void pocketOwnerCanEnableHostileMobSpawningWithoutChangingPassive() throws Exception { UUID playerId = UUID.randomUUID(); Player player = mock(Player.class); when(player.getUniqueId()).thenReturn(playerId); @@ -305,11 +318,13 @@ final class BaseSettingsCommandTest { 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) + PocketBaseState current = new PocketBaseState( + playerId, 1, java.util.Optional.empty(), java.util.Optional.empty(), + false, true, PocketBaseBiome.THE_VOID ); - when(pocketBases.setMobSpawning(playerId, true)).thenReturn( - new PocketBaseState(playerId, 1, java.util.Optional.empty(), true) + when(pocketBases.state(playerId)).thenReturn(current); + when(pocketBases.setHostileMobSpawning(playerId, true)).thenReturn( + current.withHostileMobSpawningEnabled(true) ); BaseSettingsCommand command = new BaseSettingsCommand( stateManager, @@ -318,13 +333,14 @@ final class BaseSettingsCommandTest { pocketBases ); - command.onCommand( - player, null, "basesettings", new String[] {"pocket", "mobs", "enable"} - ); + command.onCommand(player, null, "basesettings", new String[] { + "pocket", "mobs", "hostile", "enable" + }); - verify(pocketBases).setMobSpawning(playerId, true); + verify(pocketBases).setHostileMobSpawning(playerId, true); + verify(pocketBases, never()).setPassiveMobSpawning(playerId, true); verify(player).sendMessage(org.mockito.ArgumentMatchers.argThat( - message -> message.contains("mob spawning") && message.contains("enabled") + message -> message.contains("hostile mob spawning") && message.contains("enabled") )); } @@ -339,9 +355,9 @@ final class BaseSettingsCommandTest { .thenReturn(PlayerState.newPlayer(playerId, "Builder")); PocketBaseManager pocketBases = mock(PocketBaseManager.class); when(pocketBases.state(playerId)).thenReturn( - new PocketBaseState(playerId, 1, java.util.Optional.empty(), false) + new PocketBaseState(playerId, 1, java.util.Optional.empty()) ); - when(pocketBases.setMobSpawning(playerId, true)) + when(pocketBases.setPassiveMobSpawning(playerId, true)) .thenThrow(new IOException("save failed")); BaseSettingsCommand command = new BaseSettingsCommand( stateManager, @@ -350,9 +366,9 @@ final class BaseSettingsCommandTest { pocketBases ); - command.onCommand( - player, null, "basesettings", new String[] {"pocket", "mobs", "enable"} - ); + command.onCommand(player, null, "basesettings", new String[] { + "pocket", "mobs", "passive", "enable" + }); verify(player).sendMessage(org.mockito.ArgumentMatchers.argThat( message -> message.contains("could not be changed") @@ -380,11 +396,11 @@ final class BaseSettingsCommandTest { pocketBases ); - command.onCommand( - player, null, "basesettings", new String[] {"pocket", "mobs", "enable"} - ); + command.onCommand(player, null, "basesettings", new String[] { + "pocket", "mobs", "hostile", "enable" + }); - verify(pocketBases, never()).setMobSpawning(playerId, true); + verify(pocketBases, never()).setHostileMobSpawning(playerId, true); verify(player).sendMessage(org.mockito.ArgumentMatchers.argThat( message -> message.contains("Pocket Base I") && message.contains("locked") )); @@ -429,7 +445,7 @@ final class BaseSettingsCommandTest { } @Test - void statusDisplaysPocketMobSpawningPreference() { + void statusDisplaysPocketMobSpawningPreferences() { UUID playerId = UUID.randomUUID(); Player player = mock(Player.class); when(player.getUniqueId()).thenReturn(playerId); @@ -439,7 +455,10 @@ final class BaseSettingsCommandTest { .thenReturn(PlayerState.newPlayer(playerId, "Builder")); PocketBaseManager pocketBases = mock(PocketBaseManager.class); when(pocketBases.state(playerId)).thenReturn( - new PocketBaseState(playerId, 1, java.util.Optional.empty(), false) + new PocketBaseState( + playerId, 1, java.util.Optional.empty(), java.util.Optional.empty(), + true, false, PocketBaseBiome.THE_VOID + ) ); BaseSettingsCommand command = new BaseSettingsCommand( stateManager, @@ -453,7 +472,8 @@ final class BaseSettingsCommandTest { verify(player).sendMessage(org.mockito.ArgumentMatchers.argThat( message -> message.contains("Pocket Base 1") && message.contains("type=void/the_void") - && message.contains("mob spawning=disabled") + && message.contains("hostile spawning=enabled") + && message.contains("passive spawning=disabled") )); } diff --git a/src/test/java/games/dmg/spigotbase/PocketBaseManagerTest.java b/src/test/java/games/dmg/spigotbase/PocketBaseManagerTest.java index b62f71f..9fdd42f 100644 --- a/src/test/java/games/dmg/spigotbase/PocketBaseManagerTest.java +++ b/src/test/java/games/dmg/spigotbase/PocketBaseManagerTest.java @@ -25,7 +25,8 @@ final class PocketBaseManagerTest { PocketBaseStateManager states = mock(PocketBaseStateManager.class); PocketBaseWorldService worlds = mock(PocketBaseWorldService.class); when(states.state(ownerId)).thenReturn(new PocketBaseState( - ownerId, 1, Optional.empty(), Optional.of(relocated), false + ownerId, 1, Optional.empty(), Optional.of(relocated), false, false, + PocketBaseBiome.THE_VOID )); PocketBaseManager manager = new PocketBaseManager( states, @@ -58,9 +59,12 @@ final class PocketBaseManagerTest { } @Test - void persistsAndAppliesMobSpawningPreference() throws Exception { + void persistsAndAppliesHostileMobSpawningWithoutChangingPassive() throws Exception { UUID ownerId = UUID.randomUUID(); - PocketBaseState current = new PocketBaseState(ownerId, 1, Optional.empty(), false); + PocketBaseState current = new PocketBaseState( + ownerId, 1, Optional.empty(), Optional.empty(), false, true, + PocketBaseBiome.THE_VOID + ); PocketBaseStateManager states = mock(PocketBaseStateManager.class); PocketBaseWorldService worlds = mock(PocketBaseWorldService.class); when(states.state(ownerId)).thenReturn(current); @@ -76,10 +80,12 @@ final class PocketBaseManagerTest { new PluginSettingsProvider(PluginSettings.from(Map.of())) ); - manager.setMobSpawning(ownerId, true); + PocketBaseState updated = manager.setHostileMobSpawning(ownerId, true); + assertEquals(true, updated.hostileMobSpawningEnabled()); + assertEquals(true, updated.passiveMobSpawningEnabled()); InOrder order = inOrder(worlds, states); - order.verify(worlds).setMobSpawning(ownerId, true); + order.verify(worlds).setMobSpawning(ownerId, true, true); order.verify(states).updateAndSave( org.mockito.ArgumentMatchers.eq(ownerId), org.mockito.ArgumentMatchers.any() ); @@ -137,9 +143,12 @@ final class PocketBaseManagerTest { } @Test - void restoresWorldSettingWhenPersistenceFails() throws Exception { + void restoresBothWorldSettingsWhenPersistenceFails() throws Exception { UUID ownerId = UUID.randomUUID(); - PocketBaseState current = new PocketBaseState(ownerId, 1, Optional.empty(), false); + PocketBaseState current = new PocketBaseState( + ownerId, 1, Optional.empty(), Optional.empty(), true, false, + PocketBaseBiome.THE_VOID + ); PocketBaseStateManager states = mock(PocketBaseStateManager.class); PocketBaseWorldService worlds = mock(PocketBaseWorldService.class); when(states.state(ownerId)).thenReturn(current); @@ -151,9 +160,10 @@ final class PocketBaseManagerTest { new PluginSettingsProvider(PluginSettings.from(Map.of())) ); - assertThrows(IOException.class, () -> manager.setMobSpawning(ownerId, true)); + assertThrows(IOException.class, + () -> manager.setPassiveMobSpawning(ownerId, true)); - verify(worlds).setMobSpawning(ownerId, true); - verify(worlds).setMobSpawning(ownerId, false); + verify(worlds).setMobSpawning(ownerId, true, true); + verify(worlds).setMobSpawning(ownerId, true, false); } } diff --git a/src/test/java/games/dmg/spigotbase/PocketBaseWorldServiceTest.java b/src/test/java/games/dmg/spigotbase/PocketBaseWorldServiceTest.java index 9122f74..e401e09 100644 --- a/src/test/java/games/dmg/spigotbase/PocketBaseWorldServiceTest.java +++ b/src/test/java/games/dmg/spigotbase/PocketBaseWorldServiceTest.java @@ -93,10 +93,13 @@ final class PocketBaseWorldServiceTest { service.loadExisting(Map.of( ownerId, - new PocketBaseState(ownerId, 1, Optional.empty(), true) + new PocketBaseState( + ownerId, 1, Optional.empty(), Optional.empty(), true, false, + PocketBaseBiome.THE_VOID + ) )); - verify(world).setSpawnFlags(true, true); + verify(world).setSpawnFlags(true, false); } @Test diff --git a/src/test/java/games/dmg/spigotbase/YamlPocketBaseRepositoryTest.java b/src/test/java/games/dmg/spigotbase/YamlPocketBaseRepositoryTest.java index d56030f..2956448 100644 --- a/src/test/java/games/dmg/spigotbase/YamlPocketBaseRepositoryTest.java +++ b/src/test/java/games/dmg/spigotbase/YamlPocketBaseRepositoryTest.java @@ -30,6 +30,7 @@ final class YamlPocketBaseRepositoryTest { worldId, "pocket", 20, 65, 12, PocketPortalAxis.Z )), true, + false, PocketBaseBiome.CRIMSON_FOREST ); YamlPocketBaseRepository repository = new YamlPocketBaseRepository( @@ -50,8 +51,23 @@ final class YamlPocketBaseRepositoryTest { PocketBaseState loaded = repository.load().get(ownerId); - assertFalse(loaded.mobSpawningEnabled()); + assertFalse(loaded.hostileMobSpawningEnabled()); + assertFalse(loaded.passiveMobSpawningEnabled()); assertTrue(loaded.returnPortal().isEmpty()); assertEquals(PocketBaseBiome.THE_VOID, loaded.biome()); } + + @Test + void migratesLegacyMobPreferenceToBothCategories() throws Exception { + UUID ownerId = UUID.randomUUID(); + Path stateFile = temporaryDirectory.resolve("pocket-bases.yml"); + Files.writeString(stateFile, "owners:\n " + ownerId + + ":\n level: 1\n mob-spawning-enabled: true\n"); + YamlPocketBaseRepository repository = new YamlPocketBaseRepository(stateFile); + + PocketBaseState loaded = repository.load().get(ownerId); + + assertTrue(loaded.hostileMobSpawningEnabled()); + assertTrue(loaded.passiveMobSpawningEnabled()); + } }