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
+1 -1
View File
@@ -6,7 +6,7 @@ The approved behavior is specified in the [OKF knowledge bundle](knowledge/index
## Status ## Status
The project foundation and user stories are established. Quest-board behavior remains in the backlog until its stories are implemented and verified. Administrators can register persistent shared quest boards by targeting a block within five blocks and running `/questadmin createboard`. Right-clicking any registered board opens the native quest-board dialog. Quest creation, delivery, and claims remain under development.
## Requirements ## Requirements
+1
View File
@@ -28,6 +28,7 @@ dependencies {
testImplementation("org.purpurmc.purpur:purpur-api:26.2.build.2618-stable") testImplementation("org.purpurmc.purpur:purpur-api:26.2.build.2618-stable")
testImplementation(platform("org.junit:junit-bom:5.13.4")) testImplementation(platform("org.junit:junit-bom:5.13.4"))
testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.mockito:mockito-core:5.18.0")
testImplementation("org.yaml:snakeyaml:2.4") testImplementation("org.yaml:snakeyaml:2.4")
testRuntimeOnly("org.junit.platform:junit-platform-launcher") testRuntimeOnly("org.junit.platform:junit-platform-launcher")
} }
+7
View File
@@ -26,3 +26,10 @@ description: Chronological record of material decisions affecting Spigot Quest B
- Published the project to the public `dmg/spigot-quest-board` Gitea repository. - Published the project to the public `dmg/spigot-quest-board` Gitea repository.
- Added strict Java 25/Purpur 26.2 Gradle builds, plugin metadata, a metadata regression test, and CI/release workflows modeled on Spigot Base. - Added strict Java 25/Purpur 26.2 Gradle builds, plugin metadata, a metadata regression test, and CI/release workflows modeled on Spigot Base.
- Verified the plugin foundation and JAR with `./gradlew clean check jar`. - Verified the plugin foundation and JAR with `./gradlew clean check jar`.
## 2026-09-05 — Shared physical quest boards
- Added durable world-UUID and block-coordinate board registration through `/questadmin createboard`.
- Registered main-hand interactions open one shared native Purpur dialog while unregistered blocks remain untouched.
- Persistence publishes a board only after an atomic YAML save succeeds and rejects duplicate locations.
- Verified 11 tests and the plugin JAR with `./gradlew clean check jar`.
@@ -2,7 +2,7 @@
type: User Story type: User Story
title: "US-002: Create and use shared quest boards" title: "US-002: Create and use shared quest boards"
description: Let administrators establish persistent physical boards that expose one shared quest system. description: Let administrators establish persistent physical boards that expose one shared quest system.
status: backlog status: done
--- ---
# US-002: Create and use shared quest boards # US-002: Create and use shared quest boards
@@ -11,15 +11,15 @@ As an **administrator**, I want to turn a targeted block into a quest board so t
## Acceptance criteria ## Acceptance criteria
- [ ] An authorized administrator can use `/questadmin createboard` to register the block they are targeting. - [x] An authorized administrator can use `/questadmin createboard` to register the block they are targeting.
- [ ] Creation rejects a missing, invalid, or already registered target without changing state. - [x] Creation rejects a missing, invalid, or already registered target without changing state.
- [ ] Registered boards persist across server restarts with their world and block coordinates. - [x] Registered boards persist across server restarts with their world and block coordinates.
- [ ] Interacting with any registered board opens the quest-board interface. - [x] Interacting with any registered board opens the quest-board interface.
- [ ] Every registered board exposes the same global quests and claimable items. - [x] Every registered board exposes the same global quests and claimable items.
- [ ] The interface uses Purpur's supported native dialog API and follows the interaction style of Spigot Base. - [x] The interface uses Purpur's supported native dialog API and follows the interaction style of Spigot Base.
- [ ] Ordinary block interaction is not intercepted at unregistered locations. - [x] Ordinary block interaction is not intercepted at unregistered locations.
- [ ] Administrative actions require the `spigotquestboard.admin` permission, granted to server operators by default. - [x] Administrative actions require the `spigotquestboard.admin` permission, granted to server operators by default.
- [ ] Automated tests verify board registration, persistence, shared visibility, authorization, and interaction routing. - [x] Automated tests verify board registration, persistence, shared visibility, authorization, and interaction routing.
## Related ## Related
@@ -0,0 +1,17 @@
package games.dmg.spigotquestboard;
import java.util.Objects;
import java.util.UUID;
import org.bukkit.block.Block;
record BoardId(UUID worldId, int x, int y, int z) {
BoardId {
Objects.requireNonNull(worldId, "worldId");
}
static BoardId from(Block block) {
return new BoardId(
block.getWorld().getUID(), block.getX(), block.getY(), block.getZ()
);
}
}
@@ -0,0 +1,6 @@
package games.dmg.spigotquestboard;
enum BoardRegistrationResult {
CREATED,
ALREADY_REGISTERED
}
@@ -0,0 +1,47 @@
package games.dmg.spigotquestboard;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
final class BoardRegistry {
private final BoardRepository repository;
private Map<BoardId, RegisteredBoard> boards;
BoardRegistry(BoardRepository repository) throws IOException {
this.repository = Objects.requireNonNull(repository, "repository");
boards = index(repository.load());
}
synchronized BoardRegistrationResult register(RegisteredBoard board) throws IOException {
Objects.requireNonNull(board, "board");
if (boards.containsKey(board.id())) {
return BoardRegistrationResult.ALREADY_REGISTERED;
}
Map<BoardId, RegisteredBoard> candidate = new LinkedHashMap<>(boards);
candidate.put(board.id(), board);
repository.save(new BoardState(Set.copyOf(candidate.values())));
boards = Map.copyOf(candidate);
return BoardRegistrationResult.CREATED;
}
synchronized boolean contains(BoardId id) {
return boards.containsKey(id);
}
synchronized int size() {
return boards.size();
}
private static Map<BoardId, RegisteredBoard> index(BoardState state) throws IOException {
Map<BoardId, RegisteredBoard> indexed = new LinkedHashMap<>();
for (RegisteredBoard board : state.boards()) {
if (indexed.put(board.id(), board) != null) {
throw new IOException("Duplicate quest board location: " + board.id());
}
}
return Map.copyOf(indexed);
}
}
@@ -0,0 +1,8 @@
package games.dmg.spigotquestboard;
import java.io.IOException;
interface BoardRepository {
BoardState load() throws IOException;
void save(BoardState state) throws IOException;
}
@@ -0,0 +1,14 @@
package games.dmg.spigotquestboard;
import java.util.Objects;
import java.util.Set;
record BoardState(Set<RegisteredBoard> boards) {
BoardState {
boards = Set.copyOf(Objects.requireNonNull(boards, "boards"));
}
static BoardState empty() {
return new BoardState(Set.of());
}
}
@@ -0,0 +1,50 @@
package games.dmg.spigotquestboard;
import java.io.IOException;
import java.util.Objects;
import org.bukkit.block.Block;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
final class QuestAdminCommand implements CommandExecutor {
private static final String PERMISSION = "spigotquestboard.admin";
private final BoardRegistry registry;
QuestAdminCommand(BoardRegistry registry) {
this.registry = Objects.requireNonNull(registry, "registry");
}
@Override
public boolean onCommand(
CommandSender sender, Command command, String label, String[] arguments
) {
if (!sender.hasPermission(PERMISSION)) {
sender.sendMessage("You do not have permission to administer quest boards.");
return true;
}
if (arguments.length != 1 || !"createboard".equalsIgnoreCase(arguments[0])) {
sender.sendMessage("Usage: /questadmin createboard");
return true;
}
if (!(sender instanceof Player player)) {
sender.sendMessage("A player must target the quest board block.");
return true;
}
Block target = player.getTargetBlockExact(5);
if (target == null) {
sender.sendMessage("Target a physical block within five blocks.");
return true;
}
try {
BoardRegistrationResult result = registry.register(RegisteredBoard.from(target));
sender.sendMessage(result == BoardRegistrationResult.CREATED
? "Quest board created."
: "That block is already a quest board.");
} catch (IOException exception) {
sender.sendMessage("The quest board could not be saved. No board was created.");
}
return true;
}
}
@@ -0,0 +1,28 @@
package games.dmg.spigotquestboard;
import io.papermc.paper.dialog.Dialog;
import io.papermc.paper.registry.data.dialog.DialogBase;
import io.papermc.paper.registry.data.dialog.body.DialogBody;
import io.papermc.paper.registry.data.dialog.type.DialogType;
import java.util.List;
import net.kyori.adventure.text.Component;
import org.bukkit.entity.Player;
final class QuestBoardDialogUi implements QuestBoardUi {
@Override
public void open(Player player) {
DialogBase base = DialogBase.builder(Component.text("Quest Board"))
.externalTitle(Component.text("Quest Board"))
.body(List.of(DialogBody.plainMessage(
Component.text("No active quests or claimable items."), 420
)))
.canCloseWithEscape(true)
.pause(false)
.afterAction(DialogBase.DialogAfterAction.CLOSE)
.build();
Dialog dialog = Dialog.create(factory -> factory.empty()
.base(base)
.type(DialogType.notice()));
player.showDialog(dialog);
}
}
@@ -0,0 +1,31 @@
package games.dmg.spigotquestboard;
import java.util.Objects;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.EquipmentSlot;
final class QuestBoardInteractionListener implements Listener {
private final BoardRegistry registry;
private final QuestBoardUi ui;
QuestBoardInteractionListener(BoardRegistry registry, QuestBoardUi ui) {
this.registry = Objects.requireNonNull(registry, "registry");
this.ui = Objects.requireNonNull(ui, "ui");
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onPlayerInteract(PlayerInteractEvent event) {
if (event.getAction() != Action.RIGHT_CLICK_BLOCK
|| event.getHand() != EquipmentSlot.HAND
|| event.getClickedBlock() == null
|| !registry.contains(BoardId.from(event.getClickedBlock()))) {
return;
}
event.setCancelled(true);
ui.open(event.getPlayer());
}
}
@@ -0,0 +1,7 @@
package games.dmg.spigotquestboard;
import org.bukkit.entity.Player;
interface QuestBoardUi {
void open(Player player);
}
@@ -0,0 +1,15 @@
package games.dmg.spigotquestboard;
import java.util.Objects;
import org.bukkit.block.Block;
record RegisteredBoard(BoardId id, String worldName) {
RegisteredBoard {
Objects.requireNonNull(id, "id");
Objects.requireNonNull(worldName, "worldName");
}
static RegisteredBoard from(Block block) {
return new RegisteredBoard(BoardId.from(block), block.getWorld().getName());
}
}
@@ -1,10 +1,34 @@
package games.dmg.spigotquestboard; package games.dmg.spigotquestboard;
import java.io.IOException;
import java.util.Objects;
import java.util.logging.Level;
import org.bukkit.command.PluginCommand;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
public final class SpigotQuestBoardPlugin extends JavaPlugin { public final class SpigotQuestBoardPlugin extends JavaPlugin {
@Override @Override
public void onEnable() { public void onEnable() {
getLogger().info("Spigot Quest Board enabled."); final BoardRegistry boards;
try {
boards = new BoardRegistry(new YamlBoardRepository(
getDataFolder().toPath().resolve("boards.yml")
));
} catch (IOException exception) {
getLogger().log(Level.SEVERE, "Could not load quest boards", exception);
getServer().getPluginManager().disablePlugin(this);
return;
}
QuestAdminCommand admin = new QuestAdminCommand(boards);
command("questadmin").setExecutor(admin);
getServer().getPluginManager().registerEvents(
new QuestBoardInteractionListener(boards, new QuestBoardDialogUi()), this
);
getLogger().info("Spigot Quest Board enabled with " + boards.size() + " boards.");
}
private PluginCommand command(String name) {
return Objects.requireNonNull(getCommand(name), "Missing command metadata for " + name);
} }
} }
@@ -0,0 +1,104 @@
package games.dmg.spigotquestboard;
import java.io.IOException;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration;
final class YamlBoardRepository implements BoardRepository {
private final Path path;
YamlBoardRepository(Path path) {
this.path = path;
}
@Override
public BoardState load() throws IOException {
if (!Files.exists(path)) {
return BoardState.empty();
}
YamlConfiguration yaml = new YamlConfiguration();
try {
yaml.load(path.toFile());
} catch (InvalidConfigurationException exception) {
throw new IOException("Invalid quest board state", exception);
}
Set<RegisteredBoard> boards = new LinkedHashSet<>();
for (Map<?, ?> entry : yaml.getMapList("boards")) {
try {
UUID worldId = UUID.fromString(requiredString(entry, "world-id"));
String worldName = requiredString(entry, "world-name");
BoardId id = new BoardId(
worldId,
requiredInteger(entry, "x"),
requiredInteger(entry, "y"),
requiredInteger(entry, "z")
);
RegisteredBoard board = new RegisteredBoard(id, worldName);
boolean duplicate = boards.stream().anyMatch(existing -> existing.id().equals(id));
if (duplicate) {
throw new IOException("Duplicate quest board location: " + id);
}
boards.add(board);
} catch (IllegalArgumentException exception) {
throw new IOException("Invalid quest board record", exception);
}
}
return new BoardState(boards);
}
@Override
public void save(BoardState state) throws IOException {
YamlConfiguration yaml = new YamlConfiguration();
List<Map<String, Object>> serialized = new ArrayList<>();
for (RegisteredBoard board : state.boards()) {
serialized.add(Map.of(
"world-id", board.id().worldId().toString(),
"world-name", board.worldName(),
"x", board.id().x(),
"y", board.id().y(),
"z", board.id().z()
));
}
yaml.set("boards", serialized);
Path parent = path.toAbsolutePath().getParent();
Files.createDirectories(parent);
Path temporary = Files.createTempFile(parent, path.getFileName().toString(), ".tmp");
try {
yaml.save(temporary.toFile());
try {
Files.move(temporary, path, StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException exception) {
Files.move(temporary, path, StandardCopyOption.REPLACE_EXISTING);
}
} finally {
Files.deleteIfExists(temporary);
}
}
private static String requiredString(Map<?, ?> entry, String key) {
Object value = entry.get(key);
if (!(value instanceof String text) || text.isBlank()) {
throw new IllegalArgumentException("Missing " + key);
}
return text;
}
private static int requiredInteger(Map<?, ?> entry, String key) {
Object value = entry.get(key);
if (!(value instanceof Number number)) {
throw new IllegalArgumentException("Missing " + key);
}
return number.intValue();
}
}
@@ -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());
}
}