feat(pocket-base): allow return portal relocation
This commit is contained in:
@@ -44,7 +44,7 @@ After 250 Survival-mode block placements anywhere by default, the spawnable over
|
|||||||
|
|
||||||
`/base` has a stationary warm-up. Looking around is allowed, while movement between blocks, damage, teleportation, world changes, death, logout, and conflicting teleport commands cancel it without consuming the cooldown.
|
`/base` 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. 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 and can be controlled by the owner with `/basesettings pocket mobs <enable|disable>`.
|
||||||
|
|
||||||
## Administration
|
## Administration
|
||||||
|
|
||||||
|
|||||||
@@ -98,3 +98,10 @@ description: Chronological record of material decisions affecting the Spigot Bas
|
|||||||
- Added the persisted owner setting `/basesettings pocket mobs enable|disable`, contextual autocomplete, and status reporting.
|
- 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.
|
- Reapplied saved spawn flags when Pocket Base worlds load and restored the prior world setting when persistence fails.
|
||||||
- Verified the feature with `./gradlew clean check jar`.
|
- Verified the feature with `./gradlew clean check jar`.
|
||||||
|
|
||||||
|
## 2026-08-24 — Relocatable Pocket Base return portals
|
||||||
|
|
||||||
|
- Allowed Pocket Base owners to activate a complete return portal frame with flint and steel anywhere inside their unlocked pocket boundary.
|
||||||
|
- Persisted one active return portal location while retaining the generated portal as the default for existing Pocket Bases.
|
||||||
|
- Made only the selected intact frame functional without removing old frame blocks, preserving the void-return safety fallback.
|
||||||
|
- Verified the feature with `./gradlew clean check jar`.
|
||||||
|
|||||||
@@ -21,7 +21,12 @@ As a **Pocket Base owner**, I want to connect my normal base to my Pocket Base w
|
|||||||
- [x] Any player can use an active entrance without a separate visitor setting, warm-up, or cooldown.
|
- [x] Any player can use an active entrance without a separate visitor setting, warm-up, or cooldown.
|
||||||
- [x] Visitors can build, break blocks, open containers, and otherwise interact normally inside the Pocket Base.
|
- [x] Visitors can build, break blocks, open containers, and otherwise interact normally inside the Pocket Base.
|
||||||
- [x] The generated return portal sends players safely to the owner's active entrance.
|
- [x] The generated return portal sends players safely to the owner's active entrance.
|
||||||
- [x] Return travel falls back to the server spawn when the owner has no valid active entrance.
|
- [x] Only the owner can activate a complete portal frame inside their own Pocket Base with flint and steel.
|
||||||
|
- [x] Every required return-frame block must be within the owner's unlocked Pocket Base boundary.
|
||||||
|
- [x] Each Pocket Base has at most one active return portal, and activating another valid frame moves the active return location without removing the old frame.
|
||||||
|
- [x] Relocated return portals persist through restarts, while existing Pocket Bases use the generated portal as their default.
|
||||||
|
- [x] Breaking the active return frame disables return travel through it.
|
||||||
|
- [x] Return travel falls back to the server spawn when the owner has no valid normal-world entrance.
|
||||||
- [x] Falling below Y -64 performs the same safe return without void damage.
|
- [x] Falling below Y -64 performs the same safe return without void damage.
|
||||||
- [x] Players who disconnect inside a Pocket Base remain there when they reconnect.
|
- [x] Players who disconnect inside a Pocket Base remain there when they reconnect.
|
||||||
- [x] Moving the normal base deactivates its entrance while preserving the Pocket Base world and its contents.
|
- [x] Moving the normal base deactivates its entrance while preserving the Pocket Base world and its contents.
|
||||||
|
|||||||
@@ -79,10 +79,6 @@ final class PocketBaseController implements Listener, Runnable {
|
|||||||
if (pocket.level() < 1) {
|
if (pocket.level() < 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
PlayerState owner = baseStates.player(player.getUniqueId(), player.getName());
|
|
||||||
if (owner.baseLevel() < 4 || owner.base().isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Block clicked = event.getClickedBlock();
|
Block clicked = event.getClickedBlock();
|
||||||
World world = clicked.getWorld();
|
World world = clicked.getWorld();
|
||||||
Material frameMaterial = Material.valueOf(
|
Material frameMaterial = Material.valueOf(
|
||||||
@@ -98,6 +94,37 @@ final class PocketBaseController implements Listener, Runnable {
|
|||||||
if (portal.isEmpty()) {
|
if (portal.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Optional<UUID> pocketOwner = pocketBases.ownerForPocketWorld(world.getUID());
|
||||||
|
if (pocketOwner.isPresent()) {
|
||||||
|
if (!pocketOwner.orElseThrow().equals(player.getUniqueId())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (PocketPortalGeometry.frameBlocks(portal.orElseThrow()).stream()
|
||||||
|
.anyMatch(candidate -> !pocketBases.policy().contains(
|
||||||
|
pocket.level(), candidate.x(), candidate.z()
|
||||||
|
))) {
|
||||||
|
player.sendMessage(ChatColor.RED
|
||||||
|
+ "The complete return portal must be inside your Pocket Base boundary.");
|
||||||
|
event.setCancelled(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
pocketBases.activateReturnPortal(player.getUniqueId(), portal.orElseThrow());
|
||||||
|
} catch (IOException | RuntimeException exception) {
|
||||||
|
logger.log(Level.SEVERE, "Could not activate Pocket Base return portal", exception);
|
||||||
|
player.sendMessage(ChatColor.RED
|
||||||
|
+ "The Pocket Base return portal could not be activated.");
|
||||||
|
event.setCancelled(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.setCancelled(true);
|
||||||
|
player.sendMessage(ChatColor.GREEN + "Pocket Base return portal activated.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PlayerState owner = baseStates.player(player.getUniqueId(), player.getName());
|
||||||
|
if (owner.baseLevel() < 4 || owner.base().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
BaseArea area = baseBounds.area(owner);
|
BaseArea area = baseBounds.area(owner);
|
||||||
if (PocketPortalGeometry.frameBlocks(portal.orElseThrow()).stream().anyMatch(candidate ->
|
if (PocketPortalGeometry.frameBlocks(portal.orElseThrow()).stream().anyMatch(candidate ->
|
||||||
!area.contains(world.getUID(), candidate.x(), candidate.y(), candidate.z()))) {
|
!area.contains(world.getUID(), candidate.x(), candidate.y(), candidate.z()))) {
|
||||||
|
|||||||
@@ -60,6 +60,17 @@ final class PocketBaseManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PocketBaseState activateReturnPortal(
|
||||||
|
UUID ownerId,
|
||||||
|
PocketPortalLocation portal
|
||||||
|
) throws IOException {
|
||||||
|
PocketBaseState current = states.state(ownerId);
|
||||||
|
if (current.level() < 1) {
|
||||||
|
throw new IllegalStateException("Pocket Base I is still locked");
|
||||||
|
}
|
||||||
|
return states.updateAndSave(ownerId, state -> state.withReturnPortal(portal));
|
||||||
|
}
|
||||||
|
|
||||||
PocketBaseState setMobSpawning(UUID ownerId, boolean enabled) throws IOException {
|
PocketBaseState setMobSpawning(UUID ownerId, boolean enabled) throws IOException {
|
||||||
PocketBaseState current = states.state(ownerId);
|
PocketBaseState current = states.state(ownerId);
|
||||||
if (current.level() < 1) {
|
if (current.level() < 1) {
|
||||||
@@ -90,11 +101,13 @@ final class PocketBaseManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PocketPortalLocation returnPortal(UUID ownerId) {
|
PocketPortalLocation returnPortal(UUID ownerId) {
|
||||||
return worlds.returnPortal(ownerId);
|
return states.state(ownerId).returnPortal().orElseGet(
|
||||||
|
() -> worlds.returnPortal(ownerId)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean returnPortalIsIntact(UUID ownerId) {
|
boolean returnPortalIsIntact(UUID ownerId) {
|
||||||
return worlds.returnPortalIsIntact(ownerId);
|
return worlds.returnPortalIsIntact(ownerId, returnPortal(ownerId));
|
||||||
}
|
}
|
||||||
|
|
||||||
World pocketWorld(UUID ownerId) {
|
World pocketWorld(UUID ownerId) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ public record PocketBaseState(
|
|||||||
UUID ownerId,
|
UUID ownerId,
|
||||||
int level,
|
int level,
|
||||||
Optional<PocketPortalLocation> entrance,
|
Optional<PocketPortalLocation> entrance,
|
||||||
|
Optional<PocketPortalLocation> returnPortal,
|
||||||
boolean mobSpawningEnabled
|
boolean mobSpawningEnabled
|
||||||
) {
|
) {
|
||||||
public PocketBaseState {
|
public PocketBaseState {
|
||||||
@@ -17,8 +18,9 @@ public record PocketBaseState(
|
|||||||
throw new IllegalArgumentException("Pocket Base level must not be negative");
|
throw new IllegalArgumentException("Pocket Base level must not be negative");
|
||||||
}
|
}
|
||||||
entrance = entrance == null ? Optional.empty() : entrance;
|
entrance = entrance == null ? Optional.empty() : entrance;
|
||||||
if (level == 0 && entrance.isPresent()) {
|
returnPortal = returnPortal == null ? Optional.empty() : returnPortal;
|
||||||
throw new IllegalArgumentException("a locked Pocket Base cannot have an entrance");
|
if (level == 0 && (entrance.isPresent() || returnPortal.isPresent())) {
|
||||||
|
throw new IllegalArgumentException("a locked Pocket Base cannot have a portal");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,26 +29,49 @@ public record PocketBaseState(
|
|||||||
int level,
|
int level,
|
||||||
Optional<PocketPortalLocation> entrance
|
Optional<PocketPortalLocation> entrance
|
||||||
) {
|
) {
|
||||||
this(ownerId, level, entrance, false);
|
this(ownerId, level, entrance, Optional.empty(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PocketBaseState(
|
||||||
|
UUID ownerId,
|
||||||
|
int level,
|
||||||
|
Optional<PocketPortalLocation> entrance,
|
||||||
|
boolean mobSpawningEnabled
|
||||||
|
) {
|
||||||
|
this(ownerId, level, entrance, Optional.empty(), mobSpawningEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PocketBaseState locked(UUID ownerId) {
|
public static PocketBaseState locked(UUID ownerId) {
|
||||||
return new PocketBaseState(ownerId, 0, Optional.empty(), false);
|
return new PocketBaseState(
|
||||||
|
ownerId, 0, Optional.empty(), Optional.empty(), false
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PocketBaseState withLevel(int newLevel) {
|
public PocketBaseState withLevel(int newLevel) {
|
||||||
return new PocketBaseState(ownerId, newLevel, entrance, mobSpawningEnabled);
|
return new PocketBaseState(
|
||||||
|
ownerId, newLevel, entrance, returnPortal, mobSpawningEnabled
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PocketBaseState withEntrance(PocketPortalLocation portal) {
|
public PocketBaseState withEntrance(PocketPortalLocation portal) {
|
||||||
return new PocketBaseState(ownerId, level, Optional.of(portal), mobSpawningEnabled);
|
return new PocketBaseState(
|
||||||
|
ownerId, level, Optional.of(portal), returnPortal, mobSpawningEnabled
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PocketBaseState withoutEntrance() {
|
public PocketBaseState withoutEntrance() {
|
||||||
return new PocketBaseState(ownerId, level, Optional.empty(), mobSpawningEnabled);
|
return new PocketBaseState(
|
||||||
|
ownerId, level, Optional.empty(), returnPortal, mobSpawningEnabled
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PocketBaseState withReturnPortal(PocketPortalLocation portal) {
|
||||||
|
return new PocketBaseState(
|
||||||
|
ownerId, level, entrance, Optional.of(portal), mobSpawningEnabled
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PocketBaseState withMobSpawningEnabled(boolean enabled) {
|
public PocketBaseState withMobSpawningEnabled(boolean enabled) {
|
||||||
return new PocketBaseState(ownerId, level, entrance, enabled);
|
return new PocketBaseState(ownerId, level, entrance, returnPortal, enabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,9 +81,11 @@ final class PocketBaseWorldService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean returnPortalIsIntact(UUID ownerId) {
|
boolean returnPortalIsIntact(UUID ownerId, PocketPortalLocation portal) {
|
||||||
PocketPortalLocation portal = returnPortal(ownerId);
|
|
||||||
World world = ensureWorld(ownerId);
|
World world = ensureWorld(ownerId);
|
||||||
|
if (!portal.worldId().equals(world.getUID())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
Material frame = Material.valueOf(settings.current().pocketBasePortalFrameMaterial());
|
Material frame = Material.valueOf(settings.current().pocketBasePortalFrameMaterial());
|
||||||
return PocketPortalGeometry.frameBlocks(portal).stream()
|
return PocketPortalGeometry.frameBlocks(portal).stream()
|
||||||
.allMatch(position -> world.getBlockAt(
|
.allMatch(position -> world.getBlockAt(
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ public final class YamlPocketBaseRepository {
|
|||||||
ownerId,
|
ownerId,
|
||||||
level,
|
level,
|
||||||
loadPortal(yaml, path + ".entrance"),
|
loadPortal(yaml, path + ".entrance"),
|
||||||
|
loadPortal(yaml, path + ".return-portal"),
|
||||||
yaml.getBoolean(path + ".mob-spawning-enabled", false)
|
yaml.getBoolean(path + ".mob-spawning-enabled", false)
|
||||||
);
|
);
|
||||||
states.put(ownerId, state);
|
states.put(ownerId, state);
|
||||||
@@ -64,6 +65,9 @@ public final class YamlPocketBaseRepository {
|
|||||||
yaml.set(path + ".level", state.level());
|
yaml.set(path + ".level", state.level());
|
||||||
yaml.set(path + ".mob-spawning-enabled", state.mobSpawningEnabled());
|
yaml.set(path + ".mob-spawning-enabled", state.mobSpawningEnabled());
|
||||||
state.entrance().ifPresent(portal -> savePortal(yaml, path + ".entrance", portal));
|
state.entrance().ifPresent(portal -> savePortal(yaml, path + ".entrance", portal));
|
||||||
|
state.returnPortal().ifPresent(portal ->
|
||||||
|
savePortal(yaml, path + ".return-portal", portal)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Path temporary = Files.createTempFile(parent, "spigot-base-pocket-", ".yml");
|
Path temporary = Files.createTempFile(parent, "spigot-base-pocket-", ".yml");
|
||||||
try {
|
try {
|
||||||
@@ -96,7 +100,7 @@ public final class YamlPocketBaseRepository {
|
|||||||
if (worldId == null || worldName == null || axis == null
|
if (worldId == null || worldName == null || axis == null
|
||||||
|| !yaml.isInt(path + ".x") || !yaml.isInt(path + ".y")
|
|| !yaml.isInt(path + ".x") || !yaml.isInt(path + ".y")
|
||||||
|| !yaml.isInt(path + ".z")) {
|
|| !yaml.isInt(path + ".z")) {
|
||||||
throw new IllegalArgumentException("invalid Pocket Base entrance");
|
throw new IllegalArgumentException("invalid Pocket Base portal");
|
||||||
}
|
}
|
||||||
return Optional.of(new PocketPortalLocation(
|
return Optional.of(new PocketPortalLocation(
|
||||||
UUID.fromString(worldId),
|
UUID.fromString(worldId),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package games.dmg.spigotbase;
|
|||||||
|
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@@ -85,6 +86,114 @@ final class PocketBaseControllerTest {
|
|||||||
verify(event).setCancelled(true);
|
verify(event).setCancelled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ownerCanActivateCompleteReturnFrameInsideOwnPocketBase() throws Exception {
|
||||||
|
UUID ownerId = UUID.randomUUID();
|
||||||
|
UUID worldId = UUID.randomUUID();
|
||||||
|
PocketPortalLocation portal = new PocketPortalLocation(
|
||||||
|
worldId, "pocket", 8, 65, 8, PocketPortalAxis.X
|
||||||
|
);
|
||||||
|
Set<BlockPosition> frame = new HashSet<>(PocketPortalGeometry.frameBlocks(portal));
|
||||||
|
Player owner = mock(Player.class);
|
||||||
|
World world = mock(World.class);
|
||||||
|
Block clicked = block(world, 8, 67, 8, Material.DIAMOND_BLOCK);
|
||||||
|
PlayerInteractEvent event = mock(PlayerInteractEvent.class);
|
||||||
|
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
|
||||||
|
PluginSettings settings = PluginSettings.from(Map.of());
|
||||||
|
|
||||||
|
when(owner.getUniqueId()).thenReturn(ownerId);
|
||||||
|
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("pocket");
|
||||||
|
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(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.onActivate(event);
|
||||||
|
|
||||||
|
verify(pocketBases).activateReturnPortal(ownerId, portal);
|
||||||
|
verify(event).setCancelled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnFrameOutsideUnlockedPocketBoundaryIsRejected() throws Exception {
|
||||||
|
UUID ownerId = UUID.randomUUID();
|
||||||
|
UUID worldId = UUID.randomUUID();
|
||||||
|
PocketPortalLocation portal = new PocketPortalLocation(
|
||||||
|
worldId, "pocket", 30, 65, 8, PocketPortalAxis.X
|
||||||
|
);
|
||||||
|
Set<BlockPosition> frame = new HashSet<>(PocketPortalGeometry.frameBlocks(portal));
|
||||||
|
Player owner = mock(Player.class);
|
||||||
|
World world = mock(World.class);
|
||||||
|
Block clicked = block(world, 30, 67, 8, Material.DIAMOND_BLOCK);
|
||||||
|
PlayerInteractEvent event = mock(PlayerInteractEvent.class);
|
||||||
|
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
|
||||||
|
PluginSettings settings = PluginSettings.from(Map.of());
|
||||||
|
|
||||||
|
when(owner.getUniqueId()).thenReturn(ownerId);
|
||||||
|
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("pocket");
|
||||||
|
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(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.onActivate(event);
|
||||||
|
|
||||||
|
verify(pocketBases, never()).activateReturnPortal(ownerId, portal);
|
||||||
|
verify(event).setCancelled(true);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void placementOutsideUnlockedPocketBoundaryIsCancelled() {
|
void placementOutsideUnlockedPocketBoundaryIsCancelled() {
|
||||||
UUID ownerId = UUID.randomUUID();
|
UUID ownerId = UUID.randomUUID();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package games.dmg.spigotbase;
|
package games.dmg.spigotbase;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.mockito.Mockito.inOrder;
|
import static org.mockito.Mockito.inOrder;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
@@ -15,6 +16,47 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.mockito.InOrder;
|
import org.mockito.InOrder;
|
||||||
|
|
||||||
final class PocketBaseManagerTest {
|
final class PocketBaseManagerTest {
|
||||||
|
@Test
|
||||||
|
void relocatedReturnPortalOverridesGeneratedDefault() {
|
||||||
|
UUID ownerId = UUID.randomUUID();
|
||||||
|
PocketPortalLocation relocated = new PocketPortalLocation(
|
||||||
|
UUID.randomUUID(), "pocket", 20, 65, 12, PocketPortalAxis.Z
|
||||||
|
);
|
||||||
|
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
|
||||||
|
));
|
||||||
|
PocketBaseManager manager = new PocketBaseManager(
|
||||||
|
states,
|
||||||
|
worlds,
|
||||||
|
new PluginSettingsProvider(PluginSettings.from(Map.of()))
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(relocated, manager.returnPortal(ownerId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void existingStateWithoutRelocationUsesGeneratedReturnPortal() {
|
||||||
|
UUID ownerId = UUID.randomUUID();
|
||||||
|
PocketPortalLocation generated = new PocketPortalLocation(
|
||||||
|
UUID.randomUUID(), "pocket", -2, 65, 0, PocketPortalAxis.X
|
||||||
|
);
|
||||||
|
PocketBaseStateManager states = mock(PocketBaseStateManager.class);
|
||||||
|
PocketBaseWorldService worlds = mock(PocketBaseWorldService.class);
|
||||||
|
when(states.state(ownerId)).thenReturn(
|
||||||
|
new PocketBaseState(ownerId, 1, Optional.empty())
|
||||||
|
);
|
||||||
|
when(worlds.returnPortal(ownerId)).thenReturn(generated);
|
||||||
|
PocketBaseManager manager = new PocketBaseManager(
|
||||||
|
states,
|
||||||
|
worlds,
|
||||||
|
new PluginSettingsProvider(PluginSettings.from(Map.of()))
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(generated, manager.returnPortal(ownerId));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void persistsAndAppliesMobSpawningPreference() throws Exception {
|
void persistsAndAppliesMobSpawningPreference() throws Exception {
|
||||||
UUID ownerId = UUID.randomUUID();
|
UUID ownerId = UUID.randomUUID();
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package games.dmg.spigotbase;
|
package games.dmg.spigotbase;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
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 static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
@@ -46,6 +48,33 @@ final class PocketBaseWorldServiceTest {
|
|||||||
assertEquals(ownerId, service.ownerForWorld(worldId).orElseThrow());
|
assertEquals(ownerId, service.ownerForWorld(worldId).orElseThrow());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void checksRelocatedReturnPortalFrameAtItsSavedLocation() {
|
||||||
|
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.getWorld(org.mockito.ArgumentMatchers.anyString())).thenReturn(world);
|
||||||
|
when(world.getUID()).thenReturn(worldId);
|
||||||
|
PocketPortalLocation portal = new PocketPortalLocation(
|
||||||
|
worldId, "pocket", 20, 65, 12, PocketPortalAxis.Z
|
||||||
|
);
|
||||||
|
for (BlockPosition position : PocketPortalGeometry.frameBlocks(portal)) {
|
||||||
|
when(world.getBlockAt(position.x(), position.y(), position.z()).getType())
|
||||||
|
.thenReturn(Material.DIAMOND_BLOCK);
|
||||||
|
}
|
||||||
|
PocketBaseWorldService service = new PocketBaseWorldService(
|
||||||
|
server,
|
||||||
|
new PluginSettingsProvider(PluginSettings.from(Map.of()))
|
||||||
|
);
|
||||||
|
|
||||||
|
assertTrue(service.returnPortalIsIntact(ownerId, portal));
|
||||||
|
|
||||||
|
when(world.getBlockAt(20, 65, 12).getType()).thenReturn(Material.AIR);
|
||||||
|
assertFalse(service.returnPortalIsIntact(ownerId, portal));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void reappliesSavedMobSpawningPreferenceWhenWorldLoads() {
|
void reappliesSavedMobSpawningPreferenceWhenWorldLoads() {
|
||||||
UUID ownerId = UUID.randomUUID();
|
UUID ownerId = UUID.randomUUID();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package games.dmg.spigotbase;
|
|||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -25,6 +26,9 @@ final class YamlPocketBaseRepositoryTest {
|
|||||||
Optional.of(new PocketPortalLocation(
|
Optional.of(new PocketPortalLocation(
|
||||||
worldId, "world", 10, 65, -4, PocketPortalAxis.X
|
worldId, "world", 10, 65, -4, PocketPortalAxis.X
|
||||||
)),
|
)),
|
||||||
|
Optional.of(new PocketPortalLocation(
|
||||||
|
worldId, "pocket", 20, 65, 12, PocketPortalAxis.Z
|
||||||
|
)),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
YamlPocketBaseRepository repository = new YamlPocketBaseRepository(
|
YamlPocketBaseRepository repository = new YamlPocketBaseRepository(
|
||||||
@@ -43,6 +47,9 @@ final class YamlPocketBaseRepositoryTest {
|
|||||||
Files.writeString(stateFile, "owners:\n " + ownerId + ":\n level: 1\n");
|
Files.writeString(stateFile, "owners:\n " + ownerId + ":\n level: 1\n");
|
||||||
YamlPocketBaseRepository repository = new YamlPocketBaseRepository(stateFile);
|
YamlPocketBaseRepository repository = new YamlPocketBaseRepository(stateFile);
|
||||||
|
|
||||||
assertFalse(repository.load().get(ownerId).mobSpawningEnabled());
|
PocketBaseState loaded = repository.load().get(ownerId);
|
||||||
|
|
||||||
|
assertFalse(loaded.mobSpawningEnabled());
|
||||||
|
assertTrue(loaded.returnPortal().isEmpty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user