feat(board): add shared physical quest boards
Release / release (push) Successful in 4m19s
CI / build (push) Successful in 1m20s

This commit is contained in:
dmg
2026-09-04 23:33:18 -04:00
parent f024744440
commit 63779d1631
20 changed files with 645 additions and 12 deletions
@@ -0,0 +1,67 @@
package games.dmg.spigotquestboard;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.util.Set;
import java.util.UUID;
import org.junit.jupiter.api.Test;
final class BoardRegistryTest {
private static final RegisteredBoard BOARD = new RegisteredBoard(
new BoardId(UUID.fromString("00000000-0000-0000-0000-000000000001"), 1, 64, 2),
"world"
);
@Test
void registersAndPersistsBeforePublishingBoard() throws IOException {
RecordingRepository repository = new RecordingRepository(BoardState.empty());
BoardRegistry registry = new BoardRegistry(repository);
assertEquals(BoardRegistrationResult.CREATED, registry.register(BOARD));
assertTrue(registry.contains(BOARD.id()));
assertEquals(Set.of(BOARD), repository.saved.boards());
}
@Test
void duplicateDoesNotWrite() throws IOException {
RecordingRepository repository = new RecordingRepository(new BoardState(Set.of(BOARD)));
BoardRegistry registry = new BoardRegistry(repository);
assertEquals(BoardRegistrationResult.ALREADY_REGISTERED, registry.register(BOARD));
assertEquals(0, repository.saveCount);
}
@Test
void failedPersistenceDoesNotPublishBoard() throws Exception {
BoardRepository repository = new BoardRepository() {
@Override public BoardState load() { return BoardState.empty(); }
@Override public void save(BoardState state) throws IOException {
throw new IOException("disk failed");
}
};
BoardRegistry registry = new BoardRegistry(repository);
assertThrows(IOException.class, () -> registry.register(BOARD));
assertFalse(registry.contains(BOARD.id()));
}
private static final class RecordingRepository implements BoardRepository {
private final BoardState initial;
private BoardState saved;
private int saveCount;
private RecordingRepository(BoardState initial) {
this.initial = initial;
}
@Override public BoardState load() { return initial; }
@Override public void save(BoardState state) {
saved = state;
saveCount++;
}
}
}
@@ -0,0 +1,78 @@
package games.dmg.spigotquestboard;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
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.Set;
import java.util.UUID;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.junit.jupiter.api.Test;
final class QuestAdminCommandTest {
@Test
void unauthorizedSenderCannotCreateBoard() throws Exception {
MemoryBoardRepository repository = new MemoryBoardRepository();
QuestAdminCommand executor = new QuestAdminCommand(new BoardRegistry(repository));
Player player = mock(Player.class);
when(player.hasPermission("spigotquestboard.admin")).thenReturn(false);
assertTrue(executor.onCommand(player, mock(Command.class), "questadmin", new String[] {"createboard"}));
verify(player, never()).getTargetBlockExact(5);
assertTrue(repository.state.boards().isEmpty());
}
@Test
void registersTargetedPhysicalBlock() throws Exception {
MemoryBoardRepository repository = new MemoryBoardRepository();
QuestAdminCommand executor = new QuestAdminCommand(new BoardRegistry(repository));
Player player = mock(Player.class);
Block block = mock(Block.class);
World world = mock(World.class);
UUID worldId = UUID.randomUUID();
when(player.hasPermission("spigotquestboard.admin")).thenReturn(true);
when(player.getTargetBlockExact(5)).thenReturn(block);
when(block.getWorld()).thenReturn(world);
when(world.getUID()).thenReturn(worldId);
when(world.getName()).thenReturn("world");
when(block.getX()).thenReturn(10);
when(block.getY()).thenReturn(65);
when(block.getZ()).thenReturn(-4);
assertTrue(executor.onCommand(player, mock(Command.class), "questadmin", new String[] {"createboard"}));
assertTrue(repository.state.boards().contains(new RegisteredBoard(
new BoardId(worldId, 10, 65, -4), "world"
)));
}
@Test
void consoleAndMissingTargetDoNotChangeState() throws Exception {
MemoryBoardRepository repository = new MemoryBoardRepository();
QuestAdminCommand executor = new QuestAdminCommand(new BoardRegistry(repository));
CommandSender console = mock(CommandSender.class);
when(console.hasPermission("spigotquestboard.admin")).thenReturn(true);
assertTrue(executor.onCommand(console, mock(Command.class), "questadmin", new String[] {"createboard"}));
Player player = mock(Player.class);
when(player.hasPermission("spigotquestboard.admin")).thenReturn(true);
when(player.getTargetBlockExact(5)).thenReturn(null);
assertTrue(executor.onCommand(player, mock(Command.class), "questadmin", new String[] {"createboard"}));
assertTrue(repository.state.boards().isEmpty());
}
private static final class MemoryBoardRepository implements BoardRepository {
private BoardState state = new BoardState(Set.of());
@Override public BoardState load() { return state; }
@Override public void save(BoardState state) { this.state = state; }
}
}
@@ -0,0 +1,86 @@
package games.dmg.spigotquestboard;
import static org.junit.jupiter.api.Assertions.assertEquals;
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.Set;
import java.util.UUID;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.junit.jupiter.api.Test;
final class QuestBoardInteractionListenerTest {
@Test
void registeredBoardsOpenTheSameGlobalInterface() throws Exception {
UUID worldId = UUID.randomUUID();
RegisteredBoard first = new RegisteredBoard(new BoardId(worldId, 1, 64, 1), "world");
RegisteredBoard second = new RegisteredBoard(new BoardId(worldId, 2, 64, 2), "world");
BoardRepository repository = new FixedRepository(new BoardState(Set.of(first, second)));
BoardRegistry registry = new BoardRegistry(repository);
CountingUi ui = new CountingUi();
QuestBoardInteractionListener listener = new QuestBoardInteractionListener(registry, ui);
Player player = mock(Player.class);
PlayerInteractEvent firstEvent = eventFor(first, player);
PlayerInteractEvent secondEvent = eventFor(second, player);
listener.onPlayerInteract(firstEvent);
listener.onPlayerInteract(secondEvent);
verify(firstEvent).setCancelled(true);
verify(secondEvent).setCancelled(true);
assertEquals(2, ui.opens);
}
@Test
void unregisteredAndOffHandInteractionsPassThrough() throws Exception {
BoardRegistry registry = new BoardRegistry(new FixedRepository(BoardState.empty()));
CountingUi ui = new CountingUi();
QuestBoardInteractionListener listener = new QuestBoardInteractionListener(registry, ui);
PlayerInteractEvent event = eventFor(
new RegisteredBoard(new BoardId(UUID.randomUUID(), 1, 64, 1), "world"),
mock(Player.class)
);
listener.onPlayerInteract(event);
verify(event, never()).setCancelled(true);
when(event.getHand()).thenReturn(EquipmentSlot.OFF_HAND);
listener.onPlayerInteract(event);
assertEquals(0, ui.opens);
}
private static PlayerInteractEvent eventFor(RegisteredBoard board, Player player) {
PlayerInteractEvent event = mock(PlayerInteractEvent.class);
Block block = mock(Block.class);
World world = mock(World.class);
when(event.getAction()).thenReturn(Action.RIGHT_CLICK_BLOCK);
when(event.getHand()).thenReturn(EquipmentSlot.HAND);
when(event.getClickedBlock()).thenReturn(block);
when(event.getPlayer()).thenReturn(player);
when(block.getWorld()).thenReturn(world);
when(world.getUID()).thenReturn(board.id().worldId());
when(world.getName()).thenReturn(board.worldName());
when(block.getX()).thenReturn(board.id().x());
when(block.getY()).thenReturn(board.id().y());
when(block.getZ()).thenReturn(board.id().z());
return event;
}
private static final class FixedRepository implements BoardRepository {
private final BoardState state;
private FixedRepository(BoardState state) { this.state = state; }
@Override public BoardState load() { return state; }
@Override public void save(BoardState state) { throw new UnsupportedOperationException(); }
}
private static final class CountingUi implements QuestBoardUi {
private int opens;
@Override public void open(Player player) { opens++; }
}
}
@@ -0,0 +1,43 @@
package games.dmg.spigotquestboard;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.nio.file.Path;
import java.util.Set;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
final class YamlBoardRepositoryTest {
@TempDir Path temporaryDirectory;
@Test
void missingFileLoadsEmptyState() throws Exception {
YamlBoardRepository repository = new YamlBoardRepository(
temporaryDirectory.resolve("boards.yml")
);
assertTrue(repository.load().boards().isEmpty());
}
@Test
void roundTripsWorldIdentityAndCoordinates() throws Exception {
Path path = temporaryDirectory.resolve("boards.yml");
YamlBoardRepository repository = new YamlBoardRepository(path);
BoardState expected = new BoardState(Set.of(
new RegisteredBoard(
new BoardId(UUID.fromString("00000000-0000-0000-0000-000000000001"), 4, 70, -8),
"survival"
),
new RegisteredBoard(
new BoardId(UUID.fromString("00000000-0000-0000-0000-000000000002"), 4, 70, -8),
"resource"
)
));
repository.save(expected);
assertEquals(expected, repository.load());
}
}