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 status
/basesettings upgrade /basesettings upgrade
/basesettings pocket upgrade /basesettings pocket upgrade
/basesettings pocket mobs <enable|disable> /basesettings pocket mobs <hostile|passive> <enable|disable>
/basesettings pocket type <void|nether|overworld> <subtype> /basesettings pocket type <void|nether|overworld> <subtype>
/basesettings visitors <allowed|blocked> /basesettings visitors <allowed|blocked>
/basesettings navigation <enable|disable> /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` 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. 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 ## 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. - 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. - Added independent persisted owner settings through `/basesettings pocket mobs <hostile|passive> <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. - 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`. - Verified the feature with `./gradlew clean check jar`.
## 2026-08-24 — Relocatable Pocket Base return portals ## 2026-08-24 — Relocatable Pocket Base return portals
@@ -1,25 +1,27 @@
--- ---
type: User Story type: User Story
title: "US-018: Control Pocket Base mob spawning" 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 status: done
--- ---
# US-018: Control Pocket Base mob spawning # 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 ## 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] 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 enable|disable` to control natural mob spawning in their own Pocket Base. - [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] The setting controls both hostile and passive natural spawning only in the owner's 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] The command is unavailable until Pocket Base I is unlocked. - [x] Changing one mob category does not change the other category.
- [x] The command and its modes are offered through contextual autocomplete. - [x] The commands are unavailable until Pocket Base I is unlocked.
- [x] `/basesettings status` displays the current Pocket Base mob-spawning setting. - [x] The commands, categories, and modes are offered through contextual autocomplete.
- [x] The preference persists across restarts and is reapplied when the Pocket Base world loads. - [x] `/basesettings status` displays the current hostile and passive Pocket Base mob-spawning settings.
- [x] Disabling natural spawning does not remove existing mobs or prevent explicitly spawned or summoned mobs. - [x] Both preferences persist across restarts and are reapplied when the Pocket Base world loads.
- [x] Persistence or world-application failures produce a clear failure message without reporting a successful change. - [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 ## Related
@@ -69,9 +69,9 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
&& arguments[1].equalsIgnoreCase("upgrade")) { && arguments[1].equalsIgnoreCase("upgrade")) {
return purchasePocketUpgrade(player, state); return purchasePocketUpgrade(player, state);
} }
if (arguments.length == 3 && arguments[0].equalsIgnoreCase("pocket") if (arguments.length == 4 && arguments[0].equalsIgnoreCase("pocket")
&& arguments[1].equalsIgnoreCase("mobs")) { && arguments[1].equalsIgnoreCase("mobs")) {
return updatePocketMobSpawning(player, arguments[2]); return updatePocketMobSpawning(player, arguments[2], arguments[3]);
} }
if (arguments.length == 4 && arguments[0].equalsIgnoreCase("pocket") if (arguments.length == 4 && arguments[0].equalsIgnoreCase("pocket")
&& arguments[1].equalsIgnoreCase("type")) { && arguments[1].equalsIgnoreCase("type")) {
@@ -240,7 +240,11 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
return true; return true;
} }
private boolean updatePocketMobSpawning(Player player, String mode) { private boolean updatePocketMobSpawning(
Player player,
String category,
String mode
) {
if (pocketBases == null) { if (pocketBases == null) {
player.sendMessage(ChatColor.RED + "Pocket Bases are currently unavailable."); player.sendMessage(ChatColor.RED + "Pocket Bases are currently unavailable.");
return true; return true;
@@ -256,17 +260,30 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
return true; return true;
} }
final PocketBaseState updated; final PocketBaseState updated;
final boolean settingEnabled;
try { 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) { } catch (IOException | RuntimeException exception) {
player.sendMessage(ChatColor.RED player.sendMessage(ChatColor.RED
+ "The Pocket Base mob-spawning setting could not be changed."); + "The Pocket Base mob-spawning setting could not be changed.");
return true; return true;
} }
player.sendMessage(ChatColor.YELLOW + "Pocket Base mob spawning is now " player.sendMessage(ChatColor.YELLOW + "Pocket Base "
+ (updated.mobSpawningEnabled() + category.toLowerCase(Locale.ROOT) + " mob spawning is now "
? ChatColor.GREEN + "enabled" + (settingEnabled ? ChatColor.GREEN + "enabled" : ChatColor.RED + "disabled")
: ChatColor.RED + "disabled")
+ ChatColor.YELLOW + "."); + ChatColor.YELLOW + ".");
return true; return true;
} }
@@ -514,8 +531,10 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
+ settings.current().pocketBaseCurrencyMaterial().toLowerCase(Locale.ROOT) + settings.current().pocketBaseCurrencyMaterial().toLowerCase(Locale.ROOT)
+ " → /basesettings pocket upgrade; type=" + " → /basesettings pocket upgrade; type="
+ pocket.biome().worldType().commandName() + "/" + pocket.biome().commandName() + pocket.biome().worldType().commandName() + "/" + pocket.biome().commandName()
+ "; mob spawning=" + "; hostile spawning="
+ (pocket.mobSpawningEnabled() ? "enabled" : "disabled")); + (pocket.hostileMobSpawningEnabled() ? "enabled" : "disabled")
+ "; passive spawning="
+ (pocket.passiveMobSpawningEnabled() ? "enabled" : "disabled"));
} }
private void showCooldownPath(Player player, PlayerState state) { 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")) { if (arguments.length == 3 && arguments[0].equalsIgnoreCase("pocket")) {
String prefix = arguments[2].toLowerCase(Locale.ROOT); String prefix = arguments[2].toLowerCase(Locale.ROOT);
if (arguments[1].equalsIgnoreCase("mobs")) { 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")) { if (arguments[1].equalsIgnoreCase("type")) {
return POCKET_WORLD_TYPES.stream() return POCKET_WORLD_TYPES.stream()
@@ -565,9 +586,16 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
.toList(); .toList();
} }
} }
if (arguments.length == 4 && arguments[0].equalsIgnoreCase("pocket") if (arguments.length == 4 && arguments[0].equalsIgnoreCase("pocket")) {
&& arguments[1].equalsIgnoreCase("type")) {
String prefix = arguments[3].toLowerCase(Locale.ROOT); 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]) return PocketBaseWorldType.fromCommand(arguments[2])
.map(PocketBaseBiome::commandNames) .map(PocketBaseBiome::commandNames)
.orElse(List.of()) .orElse(List.of())
@@ -575,6 +603,7 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
.filter(biome -> biome.startsWith(prefix)) .filter(biome -> biome.startsWith(prefix))
.toList(); .toList();
} }
}
return List.of(); return List.of();
} }
@@ -634,7 +663,8 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
private static void sendUsage(Player player) { private static void sendUsage(Player player) {
player.sendMessage(ChatColor.RED + "Usage: /basesettings " 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>" + "|pocket type <void|nether|overworld> <subtype>"
+ "|visitors <allowed|blocked>|navigation <enable|disable>" + "|visitors <allowed|blocked>|navigation <enable|disable>"
+ "|flight <enable|disable>|border <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); 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) { if (current.level() < 1) {
throw new IllegalStateException("Pocket Base I is still locked"); throw new IllegalStateException("Pocket Base I is still locked");
} }
worlds.setMobSpawning(ownerId, enabled); UUID ownerId = current.ownerId();
worlds.setMobSpawning(ownerId, hostileEnabled, passiveEnabled);
try { try {
return states.updateAndSave( return states.updateAndSave(ownerId, update);
ownerId,
state -> state.withMobSpawningEnabled(enabled)
);
} catch (IOException | RuntimeException exception) { } catch (IOException | RuntimeException exception) {
try { try {
worlds.setMobSpawning(ownerId, current.mobSpawningEnabled()); worlds.setMobSpawning(
ownerId,
current.hostileMobSpawningEnabled(),
current.passiveMobSpawningEnabled()
);
} catch (RuntimeException rollbackFailure) { } catch (RuntimeException rollbackFailure) {
exception.addSuppressed(rollbackFailure); exception.addSuppressed(rollbackFailure);
} }
@@ -8,7 +8,8 @@ public record PocketBaseState(
int level, int level,
Optional<PocketPortalLocation> entrance, Optional<PocketPortalLocation> entrance,
Optional<PocketPortalLocation> returnPortal, Optional<PocketPortalLocation> returnPortal,
boolean mobSpawningEnabled, boolean hostileMobSpawningEnabled,
boolean passiveMobSpawningEnabled,
PocketBaseBiome biome PocketBaseBiome biome
) { ) {
public PocketBaseState { public PocketBaseState {
@@ -32,73 +33,64 @@ public record PocketBaseState(
Optional<PocketPortalLocation> entrance Optional<PocketPortalLocation> entrance
) { ) {
this( this(
ownerId, level, entrance, Optional.empty(), false, PocketBaseBiome.THE_VOID ownerId, level, entrance, Optional.empty(), false, false,
);
}
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,
PocketBaseBiome.THE_VOID PocketBaseBiome.THE_VOID
); );
} }
public static PocketBaseState locked(UUID ownerId) { public static PocketBaseState locked(UUID ownerId) {
return new PocketBaseState( return new PocketBaseState(
ownerId, 0, Optional.empty(), Optional.empty(), false, ownerId, 0, Optional.empty(), Optional.empty(), false, false,
PocketBaseBiome.THE_VOID PocketBaseBiome.THE_VOID
); );
} }
public PocketBaseState withLevel(int newLevel) { public PocketBaseState withLevel(int newLevel) {
return new PocketBaseState( return new PocketBaseState(
ownerId, newLevel, entrance, returnPortal, mobSpawningEnabled, biome ownerId, newLevel, entrance, returnPortal, hostileMobSpawningEnabled,
passiveMobSpawningEnabled, biome
); );
} }
public PocketBaseState withEntrance(PocketPortalLocation portal) { public PocketBaseState withEntrance(PocketPortalLocation portal) {
return new PocketBaseState( return new PocketBaseState(
ownerId, level, Optional.of(portal), returnPortal, mobSpawningEnabled, biome ownerId, level, Optional.of(portal), returnPortal,
hostileMobSpawningEnabled, passiveMobSpawningEnabled, biome
); );
} }
public PocketBaseState withoutEntrance() { public PocketBaseState withoutEntrance() {
return new PocketBaseState( return new PocketBaseState(
ownerId, level, Optional.empty(), returnPortal, mobSpawningEnabled, biome ownerId, level, Optional.empty(), returnPortal, hostileMobSpawningEnabled,
passiveMobSpawningEnabled, biome
); );
} }
public PocketBaseState withReturnPortal(PocketPortalLocation portal) { public PocketBaseState withReturnPortal(PocketPortalLocation portal) {
return new PocketBaseState( 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) { public PocketBaseState withHostileMobSpawningEnabled(boolean enabled) {
return new PocketBaseState(ownerId, level, entrance, returnPortal, enabled, biome); 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) { public PocketBaseState withBiome(PocketBaseBiome selectedBiome) {
return new PocketBaseState( 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()) { for (PocketBaseState state : states.values()) {
if (state.level() > 0) { if (state.level() > 0) {
setBiome(state.ownerId(), state.level(), state.biome()); 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); return ensureWorld(ownerId);
} }
void setMobSpawning(UUID ownerId, boolean enabled) { void setMobSpawning(
ensureWorld(ownerId).setSpawnFlags(enabled, enabled); UUID ownerId,
boolean hostileEnabled,
boolean passiveEnabled
) {
ensureWorld(ownerId).setSpawnFlags(hostileEnabled, passiveEnabled);
} }
void setBiome(UUID ownerId, int level, PocketBaseBiome biome) { void setBiome(UUID ownerId, int level, PocketBaseBiome biome) {
@@ -44,7 +44,8 @@ public final class YamlPocketBaseRepository {
level, level,
loadPortal(yaml, path + ".entrance"), loadPortal(yaml, path + ".entrance"),
loadPortal(yaml, path + ".return-portal"), 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) loadBiome(yaml, path)
); );
states.put(ownerId, state); states.put(ownerId, state);
@@ -64,7 +65,14 @@ public final class YamlPocketBaseRepository {
for (PocketBaseState state : states.values()) { for (PocketBaseState state : states.values()) {
String path = "owners." + state.ownerId(); String path = "owners." + state.ownerId();
yaml.set(path + ".level", state.level()); 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 + ".world-type", state.biome().worldType().commandName());
yaml.set(path + ".biome", state.biome().commandName()); yaml.set(path + ".biome", state.biome().commandName());
state.entrance().ifPresent(portal -> savePortal(yaml, path + ".entrance", portal)); 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) { private static PocketBaseBiome loadBiome(YamlConfiguration yaml, String path) {
String worldTypeName = yaml.getString(path + ".world-type"); String worldTypeName = yaml.getString(path + ".world-type");
String biomeName = yaml.getString(path + ".biome"); String biomeName = yaml.getString(path + ".biome");
@@ -119,10 +119,23 @@ final class BaseSettingsCommandTest {
new String[] {"pocket", "type", "nether", "c"} 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( assertEquals(
List.of("enable"), List.of("enable"),
command.onTabComplete( 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")) { for (String setting : List.of("navigation", "flight", "border", "spawnable", "bossbar")) {
@@ -296,7 +309,7 @@ final class BaseSettingsCommandTest {
} }
@Test @Test
void pocketOwnerCanEnableNaturalMobSpawning() throws Exception { void pocketOwnerCanEnableHostileMobSpawningWithoutChangingPassive() throws Exception {
UUID playerId = UUID.randomUUID(); UUID playerId = UUID.randomUUID();
Player player = mock(Player.class); Player player = mock(Player.class);
when(player.getUniqueId()).thenReturn(playerId); when(player.getUniqueId()).thenReturn(playerId);
@@ -305,11 +318,13 @@ final class BaseSettingsCommandTest {
when(stateManager.player(playerId, "Builder")) when(stateManager.player(playerId, "Builder"))
.thenReturn(PlayerState.newPlayer(playerId, "Builder")); .thenReturn(PlayerState.newPlayer(playerId, "Builder"));
PocketBaseManager pocketBases = mock(PocketBaseManager.class); PocketBaseManager pocketBases = mock(PocketBaseManager.class);
when(pocketBases.state(playerId)).thenReturn( PocketBaseState current = new PocketBaseState(
new PocketBaseState(playerId, 1, java.util.Optional.empty(), false) playerId, 1, java.util.Optional.empty(), java.util.Optional.empty(),
false, true, PocketBaseBiome.THE_VOID
); );
when(pocketBases.setMobSpawning(playerId, true)).thenReturn( when(pocketBases.state(playerId)).thenReturn(current);
new PocketBaseState(playerId, 1, java.util.Optional.empty(), true) when(pocketBases.setHostileMobSpawning(playerId, true)).thenReturn(
current.withHostileMobSpawningEnabled(true)
); );
BaseSettingsCommand command = new BaseSettingsCommand( BaseSettingsCommand command = new BaseSettingsCommand(
stateManager, stateManager,
@@ -318,13 +333,14 @@ final class BaseSettingsCommandTest {
pocketBases pocketBases
); );
command.onCommand( command.onCommand(player, null, "basesettings", new String[] {
player, null, "basesettings", new String[] {"pocket", "mobs", "enable"} "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( 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")); .thenReturn(PlayerState.newPlayer(playerId, "Builder"));
PocketBaseManager pocketBases = mock(PocketBaseManager.class); PocketBaseManager pocketBases = mock(PocketBaseManager.class);
when(pocketBases.state(playerId)).thenReturn( 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")); .thenThrow(new IOException("save failed"));
BaseSettingsCommand command = new BaseSettingsCommand( BaseSettingsCommand command = new BaseSettingsCommand(
stateManager, stateManager,
@@ -350,9 +366,9 @@ final class BaseSettingsCommandTest {
pocketBases pocketBases
); );
command.onCommand( command.onCommand(player, null, "basesettings", new String[] {
player, null, "basesettings", new String[] {"pocket", "mobs", "enable"} "pocket", "mobs", "passive", "enable"
); });
verify(player).sendMessage(org.mockito.ArgumentMatchers.<String>argThat( verify(player).sendMessage(org.mockito.ArgumentMatchers.<String>argThat(
message -> message.contains("could not be changed") message -> message.contains("could not be changed")
@@ -380,11 +396,11 @@ final class BaseSettingsCommandTest {
pocketBases pocketBases
); );
command.onCommand( command.onCommand(player, null, "basesettings", new String[] {
player, null, "basesettings", new String[] {"pocket", "mobs", "enable"} "pocket", "mobs", "hostile", "enable"
); });
verify(pocketBases, never()).setMobSpawning(playerId, true); verify(pocketBases, never()).setHostileMobSpawning(playerId, true);
verify(player).sendMessage(org.mockito.ArgumentMatchers.<String>argThat( verify(player).sendMessage(org.mockito.ArgumentMatchers.<String>argThat(
message -> message.contains("Pocket Base I") && message.contains("locked") message -> message.contains("Pocket Base I") && message.contains("locked")
)); ));
@@ -429,7 +445,7 @@ final class BaseSettingsCommandTest {
} }
@Test @Test
void statusDisplaysPocketMobSpawningPreference() { void statusDisplaysPocketMobSpawningPreferences() {
UUID playerId = UUID.randomUUID(); UUID playerId = UUID.randomUUID();
Player player = mock(Player.class); Player player = mock(Player.class);
when(player.getUniqueId()).thenReturn(playerId); when(player.getUniqueId()).thenReturn(playerId);
@@ -439,7 +455,10 @@ final class BaseSettingsCommandTest {
.thenReturn(PlayerState.newPlayer(playerId, "Builder")); .thenReturn(PlayerState.newPlayer(playerId, "Builder"));
PocketBaseManager pocketBases = mock(PocketBaseManager.class); PocketBaseManager pocketBases = mock(PocketBaseManager.class);
when(pocketBases.state(playerId)).thenReturn( 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( BaseSettingsCommand command = new BaseSettingsCommand(
stateManager, stateManager,
@@ -453,7 +472,8 @@ final class BaseSettingsCommandTest {
verify(player).sendMessage(org.mockito.ArgumentMatchers.<String>argThat( verify(player).sendMessage(org.mockito.ArgumentMatchers.<String>argThat(
message -> message.contains("Pocket Base 1") message -> message.contains("Pocket Base 1")
&& message.contains("type=void/the_void") && 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); PocketBaseStateManager states = mock(PocketBaseStateManager.class);
PocketBaseWorldService worlds = mock(PocketBaseWorldService.class); PocketBaseWorldService worlds = mock(PocketBaseWorldService.class);
when(states.state(ownerId)).thenReturn(new PocketBaseState( 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( PocketBaseManager manager = new PocketBaseManager(
states, states,
@@ -58,9 +59,12 @@ final class PocketBaseManagerTest {
} }
@Test @Test
void persistsAndAppliesMobSpawningPreference() throws Exception { void persistsAndAppliesHostileMobSpawningWithoutChangingPassive() throws Exception {
UUID ownerId = UUID.randomUUID(); 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); PocketBaseStateManager states = mock(PocketBaseStateManager.class);
PocketBaseWorldService worlds = mock(PocketBaseWorldService.class); PocketBaseWorldService worlds = mock(PocketBaseWorldService.class);
when(states.state(ownerId)).thenReturn(current); when(states.state(ownerId)).thenReturn(current);
@@ -76,10 +80,12 @@ final class PocketBaseManagerTest {
new PluginSettingsProvider(PluginSettings.from(Map.of())) 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); InOrder order = inOrder(worlds, states);
order.verify(worlds).setMobSpawning(ownerId, true); order.verify(worlds).setMobSpawning(ownerId, true, true);
order.verify(states).updateAndSave( order.verify(states).updateAndSave(
org.mockito.ArgumentMatchers.eq(ownerId), org.mockito.ArgumentMatchers.any() org.mockito.ArgumentMatchers.eq(ownerId), org.mockito.ArgumentMatchers.any()
); );
@@ -137,9 +143,12 @@ final class PocketBaseManagerTest {
} }
@Test @Test
void restoresWorldSettingWhenPersistenceFails() throws Exception { void restoresBothWorldSettingsWhenPersistenceFails() throws Exception {
UUID ownerId = UUID.randomUUID(); 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); PocketBaseStateManager states = mock(PocketBaseStateManager.class);
PocketBaseWorldService worlds = mock(PocketBaseWorldService.class); PocketBaseWorldService worlds = mock(PocketBaseWorldService.class);
when(states.state(ownerId)).thenReturn(current); when(states.state(ownerId)).thenReturn(current);
@@ -151,9 +160,10 @@ final class PocketBaseManagerTest {
new PluginSettingsProvider(PluginSettings.from(Map.of())) 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, true, true);
verify(worlds).setMobSpawning(ownerId, false); verify(worlds).setMobSpawning(ownerId, true, false);
} }
} }
@@ -93,10 +93,13 @@ final class PocketBaseWorldServiceTest {
service.loadExisting(Map.of( service.loadExisting(Map.of(
ownerId, 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 @Test
@@ -30,6 +30,7 @@ final class YamlPocketBaseRepositoryTest {
worldId, "pocket", 20, 65, 12, PocketPortalAxis.Z worldId, "pocket", 20, 65, 12, PocketPortalAxis.Z
)), )),
true, true,
false,
PocketBaseBiome.CRIMSON_FOREST PocketBaseBiome.CRIMSON_FOREST
); );
YamlPocketBaseRepository repository = new YamlPocketBaseRepository( YamlPocketBaseRepository repository = new YamlPocketBaseRepository(
@@ -50,8 +51,23 @@ final class YamlPocketBaseRepositoryTest {
PocketBaseState loaded = repository.load().get(ownerId); PocketBaseState loaded = repository.load().get(ownerId);
assertFalse(loaded.mobSpawningEnabled()); assertFalse(loaded.hostileMobSpawningEnabled());
assertFalse(loaded.passiveMobSpawningEnabled());
assertTrue(loaded.returnPortal().isEmpty()); assertTrue(loaded.returnPortal().isEmpty());
assertEquals(PocketBaseBiome.THE_VOID, loaded.biome()); 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());
}
} }