feat(pocket-base): add persistent pocket dimensions
Release / release (push) Successful in 2m37s
CI / build (push) Successful in 1m14s

This commit is contained in:
dmg
2026-08-21 23:16:04 -04:00
parent 8a9640d992
commit 11e3ad1720
32 changed files with 1597 additions and 13 deletions
@@ -10,6 +10,7 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.time.Instant;
import java.util.List;
import java.util.Map;
@@ -22,6 +23,7 @@ import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
final class BaseSettingsCommandTest {
@Test
@@ -89,6 +91,10 @@ final class BaseSettingsCommandTest {
List.of("allowed"),
command.onTabComplete(null, null, "basesettings", new String[] {"visitors", "a"})
);
assertEquals(
List.of("upgrade"),
command.onTabComplete(null, null, "basesettings", new String[] {"pocket", "u"})
);
for (String setting : List.of("navigation", "flight", "border", "spawnable", "bossbar")) {
assertEquals(
List.of("disable"),
@@ -107,7 +113,7 @@ final class BaseSettingsCommandTest {
assertEquals(
List.of(
"status", "upgrade", "visitors", "navigation", "flight", "border",
"status", "upgrade", "pocket", "visitors", "navigation", "flight", "border",
"spawnable", "bossbar"
),
command.onTabComplete(null, null, "basesettings", new String[] {""})
@@ -155,6 +161,81 @@ final class BaseSettingsCommandTest {
assertTrue(updated.get().visitorsEnabled());
}
@Test
void pocketUpgradePurchasesFirstLevelAfterBaseIv() throws Exception {
UUID playerId = UUID.randomUUID();
Player player = mock(Player.class);
when(player.getUniqueId()).thenReturn(playerId);
when(player.getName()).thenReturn("Builder");
PlayerInventory inventory = mock(PlayerInventory.class);
when(player.getInventory()).thenReturn(inventory);
when(inventory.getStorageContents()).thenReturn(new ItemStack[] {
new ItemStack(Material.DIAMOND_BLOCK, 64)
});
PlayerState owner = PlayerState.newPlayer(playerId, "Builder")
.withAdministrativeLevels(4, 0, 0, 0, 0, false, false, true)
.withBase(
new BaseLocation(UUID.randomUUID(), "world", 0, 64, 0, 0, 0),
Instant.EPOCH
);
BaseStateManager stateManager = mock(BaseStateManager.class);
when(stateManager.player(playerId, "Builder")).thenReturn(owner);
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
when(pocketBases.state(playerId)).thenReturn(PocketBaseState.locked(playerId));
when(pocketBases.upgrade(playerId)).thenReturn(
new PocketBaseState(playerId, 1, java.util.Optional.empty())
);
BaseSettingsCommand command = new BaseSettingsCommand(
stateManager,
new PluginSettingsProvider(PluginSettings.from(Map.of())),
mock(BaseFlightController.class),
pocketBases
);
command.onCommand(player, null, "basesettings", new String[] {"pocket", "upgrade"});
verify(pocketBases).upgrade(playerId);
verify(inventory).setStorageContents(any(ItemStack[].class));
}
@Test
void failedPocketUpgradeRestoresPayment() throws Exception {
UUID playerId = UUID.randomUUID();
Player player = mock(Player.class);
when(player.getUniqueId()).thenReturn(playerId);
when(player.getName()).thenReturn("Builder");
PlayerInventory inventory = mock(PlayerInventory.class);
when(player.getInventory()).thenReturn(inventory);
when(inventory.getStorageContents()).thenReturn(new ItemStack[] {
new ItemStack(Material.DIAMOND_BLOCK, 64)
});
PlayerState owner = PlayerState.newPlayer(playerId, "Builder")
.withAdministrativeLevels(4, 0, 0, 0, 0, false, false, true)
.withBase(
new BaseLocation(UUID.randomUUID(), "world", 0, 64, 0, 0, 0),
Instant.EPOCH
);
BaseStateManager stateManager = mock(BaseStateManager.class);
when(stateManager.player(playerId, "Builder")).thenReturn(owner);
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
when(pocketBases.state(playerId)).thenReturn(PocketBaseState.locked(playerId));
when(pocketBases.upgrade(playerId)).thenThrow(new IOException("save failed"));
BaseSettingsCommand command = new BaseSettingsCommand(
stateManager,
new PluginSettingsProvider(PluginSettings.from(Map.of())),
mock(BaseFlightController.class),
pocketBases
);
command.onCommand(player, null, "basesettings", new String[] {"pocket", "upgrade"});
ArgumentCaptor<ItemStack[]> contents = ArgumentCaptor.forClass(ItemStack[].class);
verify(inventory, times(2)).setStorageContents(contents.capture());
ItemStack[] restored = contents.getAllValues().get(1);
assertEquals(Material.DIAMOND_BLOCK, restored[0].getType());
assertEquals(64, restored[0].getAmount());
}
@Test
void bossbarEnableIsIdempotent() {
UUID playerId = UUID.randomUUID();
@@ -21,6 +21,25 @@ final class PluginSettingsTest {
assertEquals(250, settings.spawnableOverlayUnlockPlacements());
assertEquals(10_800L, settings.initialTeleportCooldownSeconds());
assertEquals(128, settings.visitorUnlockDiamondCost());
assertEquals(64, settings.pocketBaseUnlockCost());
assertEquals(16, settings.pocketBaseUpgradeCost());
assertEquals("DIAMOND_BLOCK", settings.pocketBaseCurrencyMaterial());
assertEquals("DIAMOND_BLOCK", settings.pocketBasePortalFrameMaterial());
}
@Test
void pocketBaseCostsAndMaterialsAreConfigurable() {
PluginSettings settings = PluginSettings.from(Map.of(
"pocket-base-unlock-cost", 32,
"pocket-base-upgrade-cost", 8,
"pocket-base-currency-material", "EMERALD_BLOCK",
"pocket-base-portal-frame-material", "GOLD_BLOCK"
));
assertEquals(32, settings.pocketBaseUnlockCost());
assertEquals(8, settings.pocketBaseUpgradeCost());
assertEquals("EMERALD_BLOCK", settings.pocketBaseCurrencyMaterial());
assertEquals("GOLD_BLOCK", settings.pocketBasePortalFrameMaterial());
}
@Test
@@ -0,0 +1,136 @@
package games.dmg.spigotbase;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.time.Instant;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Logger;
import org.bukkit.Material;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.block.Action;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.ItemStack;
import org.junit.jupiter.api.Test;
final class PocketBaseControllerTest {
@Test
void ownerCanActivateCompleteFrameInsideNormalBase() throws Exception {
UUID ownerId = UUID.randomUUID();
UUID worldId = UUID.randomUUID();
PocketPortalLocation portal = new PocketPortalLocation(
worldId, "world", -2, 64, 0, PocketPortalAxis.X
);
Set<BlockPosition> frame = new HashSet<>(PocketPortalGeometry.frameBlocks(portal));
Player owner = mock(Player.class);
World world = mock(World.class);
Block clicked = block(world, -2, 66, 0, Material.DIAMOND_BLOCK);
PlayerInteractEvent event = mock(PlayerInteractEvent.class);
BaseStateManager baseStates = mock(BaseStateManager.class);
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
PluginSettings settings = PluginSettings.from(Map.of());
PlayerState ownerState = PlayerState.newPlayer(ownerId, "Owner")
.withAdministrativeLevels(4, 0, 0, 0, 0, false, false, true)
.withBase(new BaseLocation(worldId, "world", 0, 64, 0, 0, 0), Instant.EPOCH);
when(owner.getUniqueId()).thenReturn(ownerId);
when(owner.getName()).thenReturn("Owner");
when(event.getPlayer()).thenReturn(owner);
when(event.getAction()).thenReturn(Action.RIGHT_CLICK_BLOCK);
when(event.getClickedBlock()).thenReturn(clicked);
when(event.getItem()).thenReturn(new ItemStack(Material.FLINT_AND_STEEL));
when(world.getUID()).thenReturn(worldId);
when(world.getName()).thenReturn("world");
when(world.getBlockAt(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
BlockPosition position = new BlockPosition(
invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2)
);
return block(
world,
position.x(),
position.y(),
position.z(),
frame.contains(position) ? Material.DIAMOND_BLOCK : Material.AIR
);
});
when(baseStates.player(ownerId, "Owner")).thenReturn(ownerState);
when(pocketBases.state(ownerId)).thenReturn(
new PocketBaseState(ownerId, 1, Optional.empty())
);
when(pocketBases.activateEntrance(ownerId, portal)).thenReturn(
new PocketBaseState(ownerId, 1, Optional.of(portal))
);
PocketBaseController controller = new PocketBaseController(
mock(Server.class),
baseStates,
new BaseBoundsService(settings),
pocketBases,
new PluginSettingsProvider(settings),
Logger.getAnonymousLogger()
);
controller.onActivate(event);
verify(pocketBases).activateEntrance(ownerId, portal);
verify(event).setCancelled(true);
}
@Test
void placementOutsideUnlockedPocketBoundaryIsCancelled() {
UUID ownerId = UUID.randomUUID();
UUID worldId = UUID.randomUUID();
World world = mock(World.class);
Block placed = block(world, 32, 65, 0, Material.STONE);
Player player = mock(Player.class);
BlockPlaceEvent event = mock(BlockPlaceEvent.class);
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
PluginSettings settings = PluginSettings.from(Map.of());
when(world.getUID()).thenReturn(worldId);
when(event.getBlockPlaced()).thenReturn(placed);
when(event.getPlayer()).thenReturn(player);
when(pocketBases.ownerForPocketWorld(worldId)).thenReturn(Optional.of(ownerId));
when(pocketBases.state(ownerId)).thenReturn(
new PocketBaseState(ownerId, 1, Optional.empty())
);
when(pocketBases.policy()).thenReturn(new PocketBasePolicy(settings));
PocketBaseController controller = new PocketBaseController(
mock(Server.class),
mock(BaseStateManager.class),
new BaseBoundsService(settings),
pocketBases,
new PluginSettingsProvider(settings),
Logger.getAnonymousLogger()
);
controller.onPlace(event);
verify(event).setCancelled(true);
}
private static Block block(
World world,
int x,
int y,
int z,
Material material
) {
Block block = mock(Block.class);
when(block.getWorld()).thenReturn(world);
when(block.getX()).thenReturn(x);
when(block.getY()).thenReturn(y);
when(block.getZ()).thenReturn(z);
when(block.getType()).thenReturn(material);
return block;
}
}
@@ -0,0 +1,34 @@
package games.dmg.spigotbase;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Map;
import java.util.UUID;
import org.junit.jupiter.api.Test;
final class PocketBasePolicyTest {
private final PocketBasePolicy policy = new PocketBasePolicy(PluginSettings.from(Map.of()));
@Test
void levelsExpandBySixteenBlocksOnEverySide() {
assertEquals(64, policy.size(1));
assertEquals(96, policy.size(2));
assertEquals(128, policy.size(3));
}
@Test
void unlockAndLaterLevelsUseTheirConfiguredPrices() {
assertEquals(64, policy.price(PocketBaseState.locked(UUID.randomUUID())));
assertEquals(16, policy.price(new PocketBaseState(UUID.randomUUID(), 1, java.util.Optional.empty())));
}
@Test
void buildBoundaryMatchesUnlockedPlatform() {
assertTrue(policy.contains(1, -32, -32));
assertTrue(policy.contains(1, 31, 31));
assertFalse(policy.contains(1, -33, 0));
assertFalse(policy.contains(1, 32, 0));
}
}
@@ -0,0 +1,84 @@
package games.dmg.spigotbase;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.bukkit.Material;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.WorldCreator;
import org.bukkit.block.Block;
import org.junit.jupiter.api.Test;
final class PocketBaseWorldServiceTest {
@Test
void firstLevelGeneratesGrassPlatformAndReturnPortal() {
UUID ownerId = UUID.randomUUID();
UUID worldId = UUID.randomUUID();
Server server = mock(Server.class);
World world = mock(World.class);
Map<BlockPosition, Block> blocks = blocks(world);
when(server.createWorld(any(WorldCreator.class))).thenReturn(world);
when(world.getUID()).thenReturn(worldId);
when(world.getName()).thenReturn("pocket");
PocketBaseWorldService service = new PocketBaseWorldService(
server,
new PluginSettingsProvider(PluginSettings.from(Map.of()))
);
service.expand(ownerId, 0, 1);
verify(blocks.get(new BlockPosition(-32, 64, -32)))
.setType(Material.GRASS_BLOCK, false);
verify(blocks.get(new BlockPosition(31, 61, 31))).setType(Material.DIRT, false);
verify(blocks.get(new BlockPosition(-2, 65, 0)))
.setType(Material.DIAMOND_BLOCK, false);
verify(world).setSpawnLocation(0, 65, 5);
assertEquals(ownerId, service.ownerForWorld(worldId).orElseThrow());
}
@Test
void laterLevelAddsRingWithoutOverwritingExistingTerrain() {
UUID ownerId = UUID.randomUUID();
Server server = mock(Server.class);
World world = mock(World.class);
Map<BlockPosition, Block> blocks = blocks(world);
when(server.createWorld(any(WorldCreator.class))).thenReturn(world);
when(world.getUID()).thenReturn(UUID.randomUUID());
when(world.getName()).thenReturn("pocket");
PocketBaseWorldService service = new PocketBaseWorldService(
server,
new PluginSettingsProvider(PluginSettings.from(Map.of()))
);
Block existingCenter = world.getBlockAt(0, 64, 0);
service.expand(ownerId, 1, 2);
verify(blocks.get(new BlockPosition(47, 64, 47)))
.setType(Material.GRASS_BLOCK, false);
verify(existingCenter, never())
.setType(any(Material.class), org.mockito.ArgumentMatchers.anyBoolean());
}
private static Map<BlockPosition, Block> blocks(World world) {
Map<BlockPosition, Block> blocks = new HashMap<>();
when(world.getBlockAt(
org.mockito.ArgumentMatchers.anyInt(),
org.mockito.ArgumentMatchers.anyInt(),
org.mockito.ArgumentMatchers.anyInt()
)).thenAnswer(invocation -> {
BlockPosition position = new BlockPosition(
invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2)
);
return blocks.computeIfAbsent(position, ignored -> mock(Block.class));
});
return blocks;
}
}
@@ -0,0 +1,42 @@
package games.dmg.spigotbase;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import org.junit.jupiter.api.Test;
final class PocketPortalGeometryTest {
@Test
void findsStandardFrameContainingIgnitedBlock() {
UUID worldId = UUID.randomUUID();
PocketPortalLocation expected = new PocketPortalLocation(
worldId, "world", 10, 64, 20, PocketPortalAxis.X
);
Set<BlockPosition> frame = new HashSet<>(PocketPortalGeometry.frameBlocks(expected));
Optional<PocketPortalLocation> found = PocketPortalGeometry.findFrame(
new BlockPosition(10, 66, 20),
worldId,
"world",
frame::contains,
ignored -> true
);
assertEquals(expected, found.orElseThrow());
}
@Test
void identifiesPortalInteriorAndRequiredFrameBlocks() {
PocketPortalLocation portal = new PocketPortalLocation(
UUID.randomUUID(), "world", 0, 64, 0, PocketPortalAxis.Z
);
assertTrue(PocketPortalGeometry.isInterior(portal, new BlockPosition(0, 65, 1)));
assertTrue(PocketPortalGeometry.isFrame(portal, new BlockPosition(0, 68, 3)));
assertEquals(14, PocketPortalGeometry.frameBlocks(portal).size());
}
}
@@ -0,0 +1,35 @@
package games.dmg.spigotbase;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
final class YamlPocketBaseRepositoryTest {
@TempDir
Path temporaryDirectory;
@Test
void roundTripsLevelsAndActiveEntrances() throws Exception {
UUID ownerId = UUID.randomUUID();
UUID worldId = UUID.randomUUID();
PocketBaseState expected = new PocketBaseState(
ownerId,
7,
Optional.of(new PocketPortalLocation(
worldId, "world", 10, 65, -4, PocketPortalAxis.X
))
);
YamlPocketBaseRepository repository = new YamlPocketBaseRepository(
temporaryDirectory.resolve("pocket-bases.yml")
);
repository.save(Map.of(ownerId, expected));
assertEquals(expected, repository.load().get(ownerId));
}
}