feat(pocket-base): split hostile and passive mob spawning
Release / release (push) Successful in 2m50s
CI / build (push) Successful in 1m13s

This commit is contained in:
dmg
2026-08-24 10:41:44 -04:00
parent 6ea5bc16f4
commit b2ba4d5821
12 changed files with 246 additions and 117 deletions
+2 -2
View File
@@ -29,7 +29,7 @@ The plugin JAR is written to `build/libs/`.
/basesettings status
/basesettings upgrade
/basesettings pocket upgrade
/basesettings pocket mobs <enable|disable>
/basesettings pocket mobs <hostile|passive> <enable|disable>
/basesettings pocket type <void|nether|overworld> <subtype>
/basesettings visitors <allowed|blocked>
/basesettings navigation <enable|disable>
@@ -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 <enable|disable>`.
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 <hostile|passive> <enable|disable>`.
Owners can change biome metadata without altering Pocket Base blocks, entities, inventories, or portals by using `/basesettings pocket type <void|nether|overworld> <subtype>`. 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.
+3 -2
View File
@@ -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 <hostile|passive> <enable|disable>`, 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
@@ -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
@@ -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,9 +586,16 @@ 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);
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())
@@ -575,6 +603,7 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
.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 <enable|disable>"
+ "[status|upgrade|pocket upgrade|pocket mobs "
+ "<hostile|passive> <enable|disable>"
+ "|pocket type <void|nether|overworld> <subtype>"
+ "|visitors <allowed|blocked>|navigation <enable|disable>"
+ "|flight <enable|disable>|border <enable|disable>"
@@ -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<PocketBaseState> 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);
}
@@ -8,7 +8,8 @@ public record PocketBaseState(
int level,
Optional<PocketPortalLocation> entrance,
Optional<PocketPortalLocation> returnPortal,
boolean mobSpawningEnabled,
boolean hostileMobSpawningEnabled,
boolean passiveMobSpawningEnabled,
PocketBaseBiome biome
) {
public PocketBaseState {
@@ -32,73 +33,64 @@ public record PocketBaseState(
Optional<PocketPortalLocation> entrance
) {
this(
ownerId, level, entrance, Optional.empty(), false, PocketBaseBiome.THE_VOID
);
}
public PocketBaseState(
UUID ownerId,
int level,
Optional<PocketPortalLocation> entrance,
boolean mobSpawningEnabled
) {
this(
ownerId, level, entrance, Optional.empty(), mobSpawningEnabled,
PocketBaseBiome.THE_VOID
);
}
public PocketBaseState(
UUID ownerId,
int level,
Optional<PocketPortalLocation> entrance,
Optional<PocketPortalLocation> 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
);
}
}
@@ -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) {
@@ -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");
@@ -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.<String>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.<String>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.<String>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.<String>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")
));
}
@@ -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);
}
}
@@ -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
@@ -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());
}
}