feat(board): add readable physical board signs
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
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.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -8,6 +9,7 @@ import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
@@ -56,6 +58,76 @@ final class BukkitPhysicalBoardWorldTest {
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesOnlyAnExactDecorativeSignForLegacyIdentification() {
|
||||
UUID worldId = UUID.randomUUID();
|
||||
BoardId location = new BoardId(worldId, 1, 65, 2);
|
||||
World world = mock(World.class);
|
||||
Block block = mock(Block.class);
|
||||
WallSign wallSign = mock(WallSign.class);
|
||||
Sign sign = mock(Sign.class);
|
||||
SignSide signSide = mock(SignSide.class);
|
||||
when(world.getUID()).thenReturn(worldId);
|
||||
when(world.getBlockAt(1, 65, 2)).thenReturn(block);
|
||||
when(block.getType()).thenReturn(Material.OAK_WALL_SIGN);
|
||||
when(block.getBlockData()).thenReturn(wallSign);
|
||||
when(block.getState()).thenReturn(sign);
|
||||
when(wallSign.getFacing()).thenReturn(org.bukkit.block.BlockFace.NORTH);
|
||||
when(sign.getSide(Side.FRONT)).thenReturn(signSide);
|
||||
List<Component> decorative = BukkitPhysicalBoardWorld.signLines(
|
||||
PhysicalBoardSignKind.DECORATIVE
|
||||
);
|
||||
for (int line = 0; line < decorative.size(); line++) {
|
||||
when(signSide.line(line)).thenReturn(decorative.get(line));
|
||||
}
|
||||
BukkitPhysicalBoardWorld adapter = new BukkitPhysicalBoardWorld(world);
|
||||
PhysicalBoardPlan.Placement placement = new PhysicalBoardPlan.Placement(
|
||||
location, "OAK_WALL_SIGN", PhysicalBoardSignKind.DECORATIVE
|
||||
);
|
||||
|
||||
assertTrue(adapter.matches(placement, BoardFacing.NORTH));
|
||||
assertFalse(adapter.matches(placement, BoardFacing.SOUTH));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rendersReadableTitleAndInstructionText() {
|
||||
assertEquals(
|
||||
List.of(Component.text("Quest"), Component.text("Board"),
|
||||
Component.empty(), Component.empty()),
|
||||
placedLines(PhysicalBoardSignKind.TITLE)
|
||||
);
|
||||
assertEquals(
|
||||
List.of(Component.text("Right-click"), Component.text("a sign below"),
|
||||
Component.text("to browse"), Component.text("quests")),
|
||||
placedLines(PhysicalBoardSignKind.INSTRUCTION)
|
||||
);
|
||||
}
|
||||
|
||||
private static List<Component> placedLines(PhysicalBoardSignKind kind) {
|
||||
UUID worldId = UUID.randomUUID();
|
||||
BoardId location = new BoardId(worldId, 1, 65, 2);
|
||||
World world = mock(World.class);
|
||||
Block block = mock(Block.class);
|
||||
WallSign wallSign = mock(WallSign.class);
|
||||
Sign sign = mock(Sign.class);
|
||||
SignSide signSide = mock(SignSide.class);
|
||||
when(world.getUID()).thenReturn(worldId);
|
||||
when(world.getBlockAt(1, 65, 2)).thenReturn(block);
|
||||
when(block.getBlockData()).thenReturn(wallSign);
|
||||
when(block.getState()).thenReturn(sign);
|
||||
when(sign.getSide(Side.FRONT)).thenReturn(signSide);
|
||||
when(sign.update(true, false)).thenReturn(true);
|
||||
|
||||
new BukkitPhysicalBoardWorld(world).place(
|
||||
new PhysicalBoardPlan.Placement(location, "OAK_WALL_SIGN", kind),
|
||||
BoardFacing.NORTH
|
||||
);
|
||||
|
||||
ArgumentCaptor<Component> text = ArgumentCaptor.forClass(Component.class);
|
||||
verify(signSide, times(4)).line(any(Integer.class), text.capture());
|
||||
return text.getAllValues();
|
||||
}
|
||||
|
||||
@Test
|
||||
void restoresCapturedBukkitBlockState() {
|
||||
UUID worldId = UUID.randomUUID();
|
||||
|
||||
@@ -33,10 +33,10 @@ final class PhysicalBoardCreatorTest {
|
||||
creator.create(anchorBlock(), BoardFacing.NORTH)
|
||||
);
|
||||
|
||||
assertEquals(23, world.placed.size());
|
||||
assertEquals(25, world.placed.size());
|
||||
assertEquals("AIR", world.materials.getOrDefault(ANCHOR, "AIR"));
|
||||
assertEquals(15, registry.size());
|
||||
assertEquals(15, repository.state.boards().size());
|
||||
assertEquals(17, registry.size());
|
||||
assertEquals(17, repository.state.boards().size());
|
||||
assertEquals(1, repository.saves);
|
||||
assertTrue(repository.state.boards().stream().allMatch(
|
||||
board -> board.worldName().equals("survival")
|
||||
@@ -49,7 +49,9 @@ final class PhysicalBoardCreatorTest {
|
||||
BoardRegistry registry = new BoardRegistry(repository);
|
||||
FakeWorld world = new FakeWorld();
|
||||
BoardId obstruction = PhysicalBoardPlan.create(ANCHOR, BoardFacing.EAST)
|
||||
.placements().get(7).location();
|
||||
.placements().stream()
|
||||
.filter(placement -> placement.signKind() == PhysicalBoardSignKind.TITLE)
|
||||
.findFirst().orElseThrow().location();
|
||||
world.materials.put(obstruction, "STONE");
|
||||
|
||||
assertEquals(
|
||||
|
||||
@@ -19,9 +19,9 @@ final class PhysicalBoardPlanTest {
|
||||
.filter(block -> block.material().equals("OAK_LOG")).count());
|
||||
assertEquals(12, plan.placements().stream()
|
||||
.filter(block -> block.material().equals("OAK_PLANKS")).count());
|
||||
assertEquals(3, plan.placements().stream()
|
||||
assertEquals(5, plan.placements().stream()
|
||||
.filter(block -> block.material().equals("OAK_WALL_SIGN")).count());
|
||||
assertEquals(15, plan.interactionLocations().size());
|
||||
assertEquals(17, plan.interactionLocations().size());
|
||||
assertFalse(plan.placements().stream().anyMatch(block -> block.location().equals(anchor)));
|
||||
assertEquals(-2, plan.placements().stream().mapToInt(block -> block.location().x()).min().orElseThrow());
|
||||
assertEquals(2, plan.placements().stream().mapToInt(block -> block.location().x()).max().orElseThrow());
|
||||
@@ -35,8 +35,8 @@ final class PhysicalBoardPlanTest {
|
||||
|
||||
for (BoardFacing facing : BoardFacing.values()) {
|
||||
PhysicalBoardPlan plan = PhysicalBoardPlan.create(anchor, facing);
|
||||
assertEquals(23, plan.placements().size());
|
||||
assertEquals(23, new HashSet<>(plan.placements().stream()
|
||||
assertEquals(25, plan.placements().size());
|
||||
assertEquals(25, new HashSet<>(plan.placements().stream()
|
||||
.map(PhysicalBoardPlan.Placement::location).toList()).size());
|
||||
|
||||
var backing = plan.placements().stream().filter(
|
||||
@@ -53,13 +53,20 @@ final class PhysicalBoardPlanTest {
|
||||
var signs = plan.placements().stream().filter(
|
||||
PhysicalBoardPlan.Placement::sign
|
||||
).toList();
|
||||
assertEquals(3, signs.size());
|
||||
assertEquals(5, signs.size());
|
||||
assertTrue(signs.stream().allMatch(sign ->
|
||||
forward(anchor, sign.location(), facing) == 1
|
||||
&& Math.abs(lateral(anchor, sign.location(), facing)) <= 1
|
||||
&& sign.location().y() == anchor.y() + 2
|
||||
&& sign.location().y() >= anchor.y() + 2
|
||||
&& sign.location().y() <= anchor.y() + 4
|
||||
));
|
||||
assertEquals(15, plan.interactionLocations().size());
|
||||
assertEquals(1, signs.stream()
|
||||
.filter(sign -> sign.signKind() == PhysicalBoardSignKind.TITLE).count());
|
||||
assertEquals(1, signs.stream()
|
||||
.filter(sign -> sign.signKind() == PhysicalBoardSignKind.INSTRUCTION).count());
|
||||
assertEquals(3, signs.stream()
|
||||
.filter(sign -> sign.signKind() == PhysicalBoardSignKind.DECORATIVE).count());
|
||||
assertEquals(17, plan.interactionLocations().size());
|
||||
assertTrue(plan.interactionLocations().containsAll(signs.stream()
|
||||
.map(PhysicalBoardPlan.Placement::location).toList()));
|
||||
assertTrue(backing.stream()
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
package games.dmg.spigotquestboard;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class PhysicalBoardUpgraderTest {
|
||||
private static final UUID WORLD_ID = UUID.fromString(
|
||||
"00000000-0000-0000-0000-000000000011"
|
||||
);
|
||||
private static final BoardId ANCHOR = new BoardId(WORLD_ID, 10, 64, 20);
|
||||
private static final BoardFacing FACING = BoardFacing.NORTH;
|
||||
|
||||
@Test
|
||||
void upgradesOnlyAnExactRegisteredLegacyStructure() throws Exception {
|
||||
RecordingRepository repository = new RecordingRepository(legacyRegistrations(), false);
|
||||
BoardRegistry registry = new BoardRegistry(repository);
|
||||
FakeWorld world = legacyWorld();
|
||||
|
||||
assertEquals(1, upgrader(registry, world).upgrade());
|
||||
|
||||
assertEquals(17, registry.size());
|
||||
assertEquals(1, repository.saves);
|
||||
assertEquals(Set.of(PhysicalBoardSignKind.TITLE, PhysicalBoardSignKind.INSTRUCTION),
|
||||
world.placed.values().stream().map(PhysicalBoardPlan.Placement::signKind)
|
||||
.collect(Collectors.toSet()));
|
||||
assertEquals(2, world.placed.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotAlterCustomBoardsOrStructuresThatDoNotExactlyMatch() throws Exception {
|
||||
RegisteredBoard custom = new RegisteredBoard(
|
||||
new BoardId(WORLD_ID, 100, 70, 100), "survival"
|
||||
);
|
||||
RecordingRepository customRepository = new RecordingRepository(Set.of(custom), false);
|
||||
FakeWorld customWorld = new FakeWorld();
|
||||
assertEquals(0, upgrader(new BoardRegistry(customRepository), customWorld).upgrade());
|
||||
assertTrue(customWorld.placed.isEmpty());
|
||||
|
||||
RecordingRepository changedRepository = new RecordingRepository(
|
||||
legacyRegistrations(), false
|
||||
);
|
||||
FakeWorld changedWorld = legacyWorld();
|
||||
PhysicalBoardPlan.Placement log = plan().placements().stream()
|
||||
.filter(placement -> "OAK_LOG".equals(placement.material())).findFirst().orElseThrow();
|
||||
changedWorld.existing.put(log.location(), new PhysicalBoardPlan.Placement(
|
||||
log.location(), "STONE"
|
||||
));
|
||||
assertEquals(0, upgrader(new BoardRegistry(changedRepository), changedWorld).upgrade());
|
||||
assertTrue(changedWorld.placed.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotOverwriteAnOccupiedNewSignCell() throws Exception {
|
||||
RecordingRepository repository = new RecordingRepository(legacyRegistrations(), false);
|
||||
BoardRegistry registry = new BoardRegistry(repository);
|
||||
FakeWorld world = legacyWorld();
|
||||
BoardId title = newSigns().stream()
|
||||
.filter(sign -> sign.signKind() == PhysicalBoardSignKind.TITLE)
|
||||
.findFirst().orElseThrow().location();
|
||||
world.existing.put(title, new PhysicalBoardPlan.Placement(title, "STONE"));
|
||||
|
||||
assertEquals(0, upgrader(registry, world).upgrade());
|
||||
|
||||
assertTrue(world.placed.isEmpty());
|
||||
assertEquals(15, registry.size());
|
||||
assertEquals(0, repository.saves);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rollsBackBothCellsWhenSignConstructionFails() throws Exception {
|
||||
RecordingRepository repository = new RecordingRepository(legacyRegistrations(), false);
|
||||
BoardRegistry registry = new BoardRegistry(repository);
|
||||
FakeWorld world = legacyWorld();
|
||||
world.failPlacement = 1;
|
||||
|
||||
assertThrows(IOException.class, () -> upgrader(registry, world).upgrade());
|
||||
|
||||
assertTrue(newSigns().stream().allMatch(sign -> !world.existing.containsKey(sign.location())));
|
||||
assertEquals(15, registry.size());
|
||||
assertEquals(0, repository.saves);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rollsBackBothCellsWhenAtomicPersistenceFails() throws Exception {
|
||||
RecordingRepository repository = new RecordingRepository(legacyRegistrations(), true);
|
||||
BoardRegistry registry = new BoardRegistry(repository);
|
||||
FakeWorld world = legacyWorld();
|
||||
|
||||
assertThrows(IOException.class, () -> upgrader(registry, world).upgrade());
|
||||
|
||||
assertTrue(newSigns().stream().allMatch(sign -> !world.existing.containsKey(sign.location())));
|
||||
assertEquals(15, registry.size());
|
||||
assertEquals(1, repository.saves);
|
||||
}
|
||||
|
||||
private static PhysicalBoardUpgrader upgrader(BoardRegistry registry, FakeWorld world) {
|
||||
return new PhysicalBoardUpgrader(
|
||||
registry, worldId -> WORLD_ID.equals(worldId) ? world : null
|
||||
);
|
||||
}
|
||||
|
||||
private static PhysicalBoardPlan plan() {
|
||||
return PhysicalBoardPlan.create(ANCHOR, FACING);
|
||||
}
|
||||
|
||||
private static Set<RegisteredBoard> legacyRegistrations() {
|
||||
return plan().placements().stream().filter(placement ->
|
||||
"OAK_PLANKS".equals(placement.material())
|
||||
|| placement.signKind() == PhysicalBoardSignKind.DECORATIVE
|
||||
).map(placement -> new RegisteredBoard(placement.location(), "survival"))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private static FakeWorld legacyWorld() {
|
||||
FakeWorld world = new FakeWorld();
|
||||
plan().placements().stream().filter(placement ->
|
||||
placement.signKind() != PhysicalBoardSignKind.TITLE
|
||||
&& placement.signKind() != PhysicalBoardSignKind.INSTRUCTION
|
||||
).forEach(placement -> {
|
||||
world.existing.put(placement.location(), placement);
|
||||
world.facings.put(placement.location(), FACING);
|
||||
});
|
||||
return world;
|
||||
}
|
||||
|
||||
private static java.util.List<PhysicalBoardPlan.Placement> newSigns() {
|
||||
return plan().placements().stream().filter(placement ->
|
||||
placement.signKind() == PhysicalBoardSignKind.TITLE
|
||||
|| placement.signKind() == PhysicalBoardSignKind.INSTRUCTION
|
||||
).toList();
|
||||
}
|
||||
|
||||
private static final class FakeWorld implements PhysicalBoardWorld {
|
||||
private final Map<BoardId, PhysicalBoardPlan.Placement> existing = new HashMap<>();
|
||||
private final Map<BoardId, BoardFacing> facings = new HashMap<>();
|
||||
private final Map<BoardId, PhysicalBoardPlan.Placement> placed = new HashMap<>();
|
||||
private int placements;
|
||||
private int failPlacement = -1;
|
||||
|
||||
@Override public boolean isEmpty(BoardId location) {
|
||||
return !existing.containsKey(location);
|
||||
}
|
||||
|
||||
@Override public Object snapshot(BoardId location) {
|
||||
return existing.get(location);
|
||||
}
|
||||
|
||||
@Override public void place(PhysicalBoardPlan.Placement placement, BoardFacing facing) {
|
||||
if (placements++ == failPlacement) {
|
||||
throw new IllegalStateException("simulated sign build failure");
|
||||
}
|
||||
existing.put(placement.location(), placement);
|
||||
facings.put(placement.location(), facing);
|
||||
placed.put(placement.location(), placement);
|
||||
}
|
||||
|
||||
@Override public boolean matches(
|
||||
PhysicalBoardPlan.Placement placement, BoardFacing facing
|
||||
) {
|
||||
return placement.equals(existing.get(placement.location()))
|
||||
&& (!placement.sign() || facing == facings.get(placement.location()));
|
||||
}
|
||||
|
||||
@Override public void restore(BoardId location, Object snapshot) {
|
||||
if (snapshot == null) {
|
||||
existing.remove(location);
|
||||
facings.remove(location);
|
||||
} else {
|
||||
PhysicalBoardPlan.Placement placement = (PhysicalBoardPlan.Placement) snapshot;
|
||||
existing.put(location, placement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RecordingRepository implements BoardRepository {
|
||||
private BoardState state;
|
||||
private final boolean failSave;
|
||||
private int saves;
|
||||
|
||||
private RecordingRepository(Set<RegisteredBoard> boards, boolean failSave) {
|
||||
state = new BoardState(boards);
|
||||
this.failSave = failSave;
|
||||
}
|
||||
|
||||
@Override public BoardState load() {
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override public void save(BoardState state) throws IOException {
|
||||
saves++;
|
||||
if (failSave) {
|
||||
throw new IOException("disk full");
|
||||
}
|
||||
this.state = state;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package games.dmg.spigotquestboard;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -8,12 +9,41 @@ import static org.mockito.Mockito.when;
|
||||
import java.time.Clock;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class PluginCommandWiringTest {
|
||||
@Test
|
||||
void startupPhysicalBoardUpgradeDelegatesToLoadedWorlds() throws Exception {
|
||||
UUID worldId = UUID.randomUUID();
|
||||
PhysicalBoardPlan plan = PhysicalBoardPlan.create(
|
||||
new BoardId(worldId, 1, 64, 1), BoardFacing.NORTH
|
||||
);
|
||||
Set<RegisteredBoard> legacy = plan.placements().stream()
|
||||
.filter(placement -> "OAK_PLANKS".equals(placement.material())
|
||||
|| placement.signKind() == PhysicalBoardSignKind.DECORATIVE)
|
||||
.map(placement -> new RegisteredBoard(placement.location(), "survival"))
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
BoardRegistry registry = new BoardRegistry(new BoardRepository() {
|
||||
@Override public BoardState load() { return new BoardState(legacy); }
|
||||
@Override public void save(BoardState state) { }
|
||||
});
|
||||
AtomicInteger worldLookups = new AtomicInteger();
|
||||
|
||||
int upgraded = SpigotQuestBoardPlugin.upgradePhysicalBoards(registry, id -> {
|
||||
assertEquals(worldId, id);
|
||||
worldLookups.incrementAndGet();
|
||||
return null;
|
||||
});
|
||||
|
||||
assertEquals(0, upgraded);
|
||||
assertTrue(worldLookups.get() > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void adminAndPlayerCommandsShareThePersistedSetting() throws Exception {
|
||||
PlayerCommandSettings settings = new PlayerCommandSettings(
|
||||
|
||||
@@ -206,8 +206,8 @@ final class QuestAdminCommandTest {
|
||||
new String[] {"createboard", "physical"}
|
||||
));
|
||||
|
||||
assertEquals(23, placements.get());
|
||||
assertEquals(15, repository.state.boards().size());
|
||||
assertEquals(25, placements.get());
|
||||
assertEquals(17, repository.state.boards().size());
|
||||
verify(player).sendMessage("Physical quest board created.");
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ final class YamlBoardRepositoryTest {
|
||||
assertEquals(BoardRegistrationResult.CREATED, first.registerAll(generated));
|
||||
BoardRegistry restarted = new BoardRegistry(new YamlBoardRepository(path));
|
||||
|
||||
assertEquals(15, restarted.size());
|
||||
assertEquals(17, restarted.size());
|
||||
assertTrue(generated.stream().allMatch(board -> restarted.contains(board.id())));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user