fix(board): make readable signs glow
This commit is contained in:
@@ -6,7 +6,7 @@ The approved behavior is specified in the [OKF knowledge bundle](knowledge/index
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Administrators can register persistent shared quest boards by targeting a block within five blocks and running `/questadmin createboard`. They can instead run `/questadmin createboard physical` to generate a five-wide oak board above the targeted ground anchor, with centered title and browsing-instruction signs above three decorative signs; its visible panel blocks and all five signs are registered. Right-clicking any registered board opens a compact native dashboard with dedicated browsing, creation, and claim screens. A player can request a block and quantity while escrowing the exact reward stack held in their main hand. Every board and `/quests list` show the same active quests with requested blocks, rewards, issuers, and time remaining. Players can complete quests at any board or with `/quests complete <quest>` by delivering the required blocks. Exact escrowed rewards are granted immediately, and delivered blocks are held for the issuer. Issuers can cancel their own active quests at any board or with `/quests cancel <quest>`. Completed deliveries and rewards from cancelled or seven-day-expired quests are held durably and can be collected at any board or with `/quests claim`; inventory overflow drops at the claimant's feet.
|
Administrators can register persistent shared quest boards by targeting a block within five blocks and running `/questadmin createboard`. They can instead run `/questadmin createboard physical` to generate a five-wide oak board above the targeted ground anchor, with glowing centered title and browsing-instruction signs above three decorative signs; its visible panel blocks and all five signs are registered. Right-clicking any registered board opens a compact native dashboard with dedicated browsing, creation, and claim screens. A player can request a block and quantity while escrowing the exact reward stack held in their main hand. Every board and `/quests list` show the same active quests with requested blocks, rewards, issuers, and time remaining. Players can complete quests at any board or with `/quests complete <quest>` by delivering the required blocks. Exact escrowed rewards are granted immediately, and delivered blocks are held for the issuer. Issuers can cancel their own active quests at any board or with `/quests cancel <quest>`. Completed deliveries and rewards from cancelled or seven-day-expired quests are held durably and can be collected at any board or with `/quests claim`; inventory overflow drops at the claimant's feet.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
|||||||
@@ -98,3 +98,10 @@ description: Chronological record of material decisions affecting Spigot Quest B
|
|||||||
- Registered all five signs and the plank face as shared-board interaction locations.
|
- Registered all five signs and the plank face as shared-board interaction locations.
|
||||||
- Added exact legacy-structure detection and failure-safe startup upgrades for previously generated three-sign boards without touching custom or altered structures.
|
- Added exact legacy-structure detection and failure-safe startup upgrades for previously generated three-sign boards without touching custom or altered structures.
|
||||||
- Verified 111 tests and the plugin JAR with `./gradlew clean check jar`.
|
- Verified 111 tests and the plugin JAR with `./gradlew clean check jar`.
|
||||||
|
|
||||||
|
## 2026-09-05 — Glowing readable board signs
|
||||||
|
|
||||||
|
- Enabled glowing front-side text on generated title and instruction signs while retaining non-glowing decorative signs.
|
||||||
|
- Added exact full-structure detection to refresh existing generated boards once without modifying custom or altered signs.
|
||||||
|
- Added two-sign snapshot rollback when a refresh cannot complete safely.
|
||||||
|
- Verified 116 tests and the plugin JAR with `./gradlew clean check jar`.
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ As a **player**, I want readable signs on a generated quest board so that I know
|
|||||||
- [x] Existing custom single-block boards and unrelated structures are not modified.
|
- [x] Existing custom single-block boards and unrelated structures are not modified.
|
||||||
- [x] Existing-board upgrades never overwrite occupied sign locations and persist new interaction locations failure-safely.
|
- [x] Existing-board upgrades never overwrite occupied sign locations and persist new interaction locations failure-safely.
|
||||||
- [x] Automated tests verify sign placement, readable text, decorative text, interaction registration, obstruction handling, and prior-layout migration.
|
- [x] Automated tests verify sign placement, readable text, decorative text, interaction registration, obstruction handling, and prior-layout migration.
|
||||||
|
- [x] The title and instruction signs use glowing front-side text for readability while decorative signs remain non-glowing.
|
||||||
|
- [x] New boards receive glowing readable signs, and exact existing generated five-sign boards are refreshed on startup without modifying custom signs.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import org.bukkit.block.Sign;
|
|||||||
import org.bukkit.block.data.BlockData;
|
import org.bukkit.block.data.BlockData;
|
||||||
import org.bukkit.block.data.type.WallSign;
|
import org.bukkit.block.data.type.WallSign;
|
||||||
import org.bukkit.block.sign.Side;
|
import org.bukkit.block.sign.Side;
|
||||||
|
import org.bukkit.block.sign.SignSide;
|
||||||
|
|
||||||
final class BukkitPhysicalBoardWorld implements PhysicalBoardWorld {
|
final class BukkitPhysicalBoardWorld implements PhysicalBoardWorld {
|
||||||
private static final Component DECORATIVE_TEXT = Component.text("xxxxxxxx")
|
private static final Component DECORATIVE_TEXT = Component.text("xxxxxxxx")
|
||||||
@@ -64,9 +65,11 @@ final class BukkitPhysicalBoardWorld implements PhysicalBoardWorld {
|
|||||||
throw new IllegalStateException("Oak wall sign did not create sign state");
|
throw new IllegalStateException("Oak wall sign did not create sign state");
|
||||||
}
|
}
|
||||||
List<Component> lines = signLines(placement.signKind());
|
List<Component> lines = signLines(placement.signKind());
|
||||||
|
SignSide front = sign.getSide(Side.FRONT);
|
||||||
for (int line = 0; line < lines.size(); line++) {
|
for (int line = 0; line < lines.size(); line++) {
|
||||||
sign.getSide(Side.FRONT).line(line, lines.get(line));
|
front.line(line, lines.get(line));
|
||||||
}
|
}
|
||||||
|
front.setGlowingText(placement.signKind() != PhysicalBoardSignKind.DECORATIVE);
|
||||||
if (!sign.update(true, false)) {
|
if (!sign.update(true, false)) {
|
||||||
throw new IllegalStateException("Could not configure physical quest-board sign");
|
throw new IllegalStateException("Could not configure physical quest-board sign");
|
||||||
}
|
}
|
||||||
@@ -74,6 +77,14 @@ final class BukkitPhysicalBoardWorld implements PhysicalBoardWorld {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean matches(PhysicalBoardPlan.Placement placement, BoardFacing facing) {
|
public boolean matches(PhysicalBoardPlan.Placement placement, BoardFacing facing) {
|
||||||
|
return matches(placement, facing,
|
||||||
|
placement.signKind() != PhysicalBoardSignKind.DECORATIVE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(
|
||||||
|
PhysicalBoardPlan.Placement placement, BoardFacing facing, boolean glowingText
|
||||||
|
) {
|
||||||
Block block = block(placement.location());
|
Block block = block(placement.location());
|
||||||
Material material = Material.matchMaterial(placement.material());
|
Material material = Material.matchMaterial(placement.material());
|
||||||
if (material == null || block.getType() != material) {
|
if (material == null || block.getType() != material) {
|
||||||
@@ -87,9 +98,13 @@ final class BukkitPhysicalBoardWorld implements PhysicalBoardWorld {
|
|||||||
|| !(block.getState() instanceof Sign sign)) {
|
|| !(block.getState() instanceof Sign sign)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
SignSide front = sign.getSide(Side.FRONT);
|
||||||
|
if (front.isGlowingText() != glowingText) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
List<Component> expected = signLines(placement.signKind());
|
List<Component> expected = signLines(placement.signKind());
|
||||||
for (int line = 0; line < expected.size(); line++) {
|
for (int line = 0; line < expected.size(); line++) {
|
||||||
if (!expected.get(line).equals(sign.getSide(Side.FRONT).line(line))) {
|
if (!expected.get(line).equals(front.line(line))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,14 +32,21 @@ final class PhysicalBoardUpgrader {
|
|||||||
int upgraded = 0;
|
int upgraded = 0;
|
||||||
Map<BoardId, RegisteredBoard> registered = registry.registeredBoards();
|
Map<BoardId, RegisteredBoard> registered = registry.registeredBoards();
|
||||||
for (Candidate candidate : candidates(registered)) {
|
for (Candidate candidate : candidates(registered)) {
|
||||||
if (!hasLegacyRegistrations(candidate, registered)) {
|
boolean legacy = hasLegacyRegistrations(candidate, registered);
|
||||||
|
boolean full = hasFullRegistrations(candidate, registered);
|
||||||
|
if (!legacy && !full) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
PhysicalBoardWorld world = worlds.open(candidate.anchor().worldId());
|
PhysicalBoardWorld world = worlds.open(candidate.anchor().worldId());
|
||||||
if (world != null && isLegacyBoard(candidate, world)
|
if (world == null) {
|
||||||
&& upgrade(candidate, world)) {
|
continue;
|
||||||
|
}
|
||||||
|
if (legacy && isLegacyBoard(candidate, world) && upgradeLegacy(candidate, world)) {
|
||||||
upgraded++;
|
upgraded++;
|
||||||
registered = registry.registeredBoards();
|
registered = registry.registeredBoards();
|
||||||
|
} else if (full && isNonGlowingFullBoard(candidate, world)) {
|
||||||
|
refreshReadableSigns(candidate, world);
|
||||||
|
upgraded++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return upgraded;
|
return upgraded;
|
||||||
@@ -60,6 +67,19 @@ final class PhysicalBoardUpgrader {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean hasFullRegistrations(
|
||||||
|
Candidate candidate, Map<BoardId, RegisteredBoard> registered
|
||||||
|
) {
|
||||||
|
PhysicalBoardPlan plan = PhysicalBoardPlan.create(candidate.anchor(), candidate.facing());
|
||||||
|
for (BoardId location : plan.interactionLocations()) {
|
||||||
|
RegisteredBoard board = registered.get(location);
|
||||||
|
if (board == null || !candidate.worldName().equals(board.worldName())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isLegacyBoard(Candidate candidate, PhysicalBoardWorld world) {
|
private boolean isLegacyBoard(Candidate candidate, PhysicalBoardWorld world) {
|
||||||
PhysicalBoardPlan plan = PhysicalBoardPlan.create(candidate.anchor(), candidate.facing());
|
PhysicalBoardPlan plan = PhysicalBoardPlan.create(candidate.anchor(), candidate.facing());
|
||||||
for (PhysicalBoardPlan.Placement placement : legacyPlacements(plan)) {
|
for (PhysicalBoardPlan.Placement placement : legacyPlacements(plan)) {
|
||||||
@@ -75,8 +95,21 @@ final class PhysicalBoardUpgrader {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean upgrade(Candidate candidate, PhysicalBoardWorld world) throws IOException {
|
private boolean isNonGlowingFullBoard(Candidate candidate, PhysicalBoardWorld world) {
|
||||||
List<PhysicalBoardPlan.Placement> signs = newSigns(
|
PhysicalBoardPlan plan = PhysicalBoardPlan.create(candidate.anchor(), candidate.facing());
|
||||||
|
for (PhysicalBoardPlan.Placement placement : plan.placements()) {
|
||||||
|
boolean matches = placement.sign()
|
||||||
|
? world.matches(placement, candidate.facing(), false)
|
||||||
|
: world.matches(placement, candidate.facing());
|
||||||
|
if (!matches) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean upgradeLegacy(Candidate candidate, PhysicalBoardWorld world) throws IOException {
|
||||||
|
List<PhysicalBoardPlan.Placement> signs = readableSigns(
|
||||||
PhysicalBoardPlan.create(candidate.anchor(), candidate.facing())
|
PhysicalBoardPlan.create(candidate.anchor(), candidate.facing())
|
||||||
);
|
);
|
||||||
Map<BoardId, Object> snapshots = new LinkedHashMap<>();
|
Map<BoardId, Object> snapshots = new LinkedHashMap<>();
|
||||||
@@ -153,7 +186,30 @@ final class PhysicalBoardUpgrader {
|
|||||||
return Set.copyOf(locations);
|
return Set.copyOf(locations);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void refreshReadableSigns(Candidate candidate, PhysicalBoardWorld world)
|
||||||
|
throws IOException {
|
||||||
|
List<PhysicalBoardPlan.Placement> signs = readableSigns(
|
||||||
|
PhysicalBoardPlan.create(candidate.anchor(), candidate.facing())
|
||||||
|
);
|
||||||
|
Map<BoardId, Object> snapshots = new LinkedHashMap<>();
|
||||||
|
try {
|
||||||
|
for (PhysicalBoardPlan.Placement sign : signs) {
|
||||||
|
snapshots.put(sign.location(), world.snapshot(sign.location()));
|
||||||
|
}
|
||||||
|
for (PhysicalBoardPlan.Placement sign : signs) {
|
||||||
|
world.place(sign, candidate.facing());
|
||||||
|
}
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
rollback(world, snapshots, exception);
|
||||||
|
throw new IOException("Could not refresh physical quest-board signs", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static List<PhysicalBoardPlan.Placement> newSigns(PhysicalBoardPlan plan) {
|
private static List<PhysicalBoardPlan.Placement> newSigns(PhysicalBoardPlan plan) {
|
||||||
|
return readableSigns(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<PhysicalBoardPlan.Placement> readableSigns(PhysicalBoardPlan plan) {
|
||||||
return plan.placements().stream().filter(placement ->
|
return plan.placements().stream().filter(placement ->
|
||||||
placement.signKind() == PhysicalBoardSignKind.TITLE
|
placement.signKind() == PhysicalBoardSignKind.TITLE
|
||||||
|| placement.signKind() == PhysicalBoardSignKind.INSTRUCTION
|
|| placement.signKind() == PhysicalBoardSignKind.INSTRUCTION
|
||||||
|
|||||||
@@ -11,5 +11,11 @@ interface PhysicalBoardWorld {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default boolean matches(
|
||||||
|
PhysicalBoardPlan.Placement placement, BoardFacing facing, boolean glowingText
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void restore(BoardId location, Object snapshot);
|
void restore(BoardId location, Object snapshot);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,34 @@ final class BukkitPhysicalBoardWorldTest {
|
|||||||
assertFalse(adapter.matches(placement, BoardFacing.SOUTH));
|
assertFalse(adapter.matches(placement, BoardFacing.SOUTH));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void readableSignsGlowWhileDecorativeSignsDoNot() {
|
||||||
|
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);
|
||||||
|
BukkitPhysicalBoardWorld adapter = new BukkitPhysicalBoardWorld(world);
|
||||||
|
|
||||||
|
adapter.place(new PhysicalBoardPlan.Placement(
|
||||||
|
location, "OAK_WALL_SIGN", PhysicalBoardSignKind.TITLE
|
||||||
|
), BoardFacing.NORTH);
|
||||||
|
verify(signSide).setGlowingText(true);
|
||||||
|
|
||||||
|
adapter.place(new PhysicalBoardPlan.Placement(
|
||||||
|
location, "OAK_WALL_SIGN", PhysicalBoardSignKind.DECORATIVE
|
||||||
|
), BoardFacing.NORTH);
|
||||||
|
verify(signSide).setGlowingText(false);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void rendersReadableTitleAndInstructionText() {
|
void rendersReadableTitleAndInstructionText() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
|||||||
@@ -35,6 +35,33 @@ final class PhysicalBoardUpgraderTest {
|
|||||||
assertEquals(2, world.placed.size());
|
assertEquals(2, world.placed.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void refreshesBothReadableSignsOnAnExactRegisteredFullBoard() throws Exception {
|
||||||
|
RecordingRepository repository = new RecordingRepository(fullRegistrations(), false);
|
||||||
|
BoardRegistry registry = new BoardRegistry(repository);
|
||||||
|
FakeWorld world = fullWorld(false);
|
||||||
|
|
||||||
|
assertEquals(1, upgrader(registry, world).upgrade());
|
||||||
|
|
||||||
|
assertEquals(17, registry.size());
|
||||||
|
assertEquals(0, repository.saves);
|
||||||
|
assertEquals(Set.of(PhysicalBoardSignKind.TITLE, PhysicalBoardSignKind.INSTRUCTION),
|
||||||
|
world.placed.values().stream().map(PhysicalBoardPlan.Placement::signKind)
|
||||||
|
.collect(Collectors.toSet()));
|
||||||
|
assertTrue(newSigns().stream().allMatch(sign -> world.glowing.get(sign.location())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void doesNotReportOrRefreshAnAlreadyGlowingFullBoard() throws Exception {
|
||||||
|
RecordingRepository repository = new RecordingRepository(fullRegistrations(), false);
|
||||||
|
FakeWorld world = fullWorld(true);
|
||||||
|
|
||||||
|
assertEquals(0, upgrader(new BoardRegistry(repository), world).upgrade());
|
||||||
|
|
||||||
|
assertTrue(world.placed.isEmpty());
|
||||||
|
assertEquals(0, repository.saves);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void doesNotAlterCustomBoardsOrStructuresThatDoNotExactlyMatch() throws Exception {
|
void doesNotAlterCustomBoardsOrStructuresThatDoNotExactlyMatch() throws Exception {
|
||||||
RegisteredBoard custom = new RegisteredBoard(
|
RegisteredBoard custom = new RegisteredBoard(
|
||||||
@@ -58,6 +85,23 @@ final class PhysicalBoardUpgraderTest {
|
|||||||
assertTrue(changedWorld.placed.isEmpty());
|
assertTrue(changedWorld.placed.isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void doesNotRefreshAFullBoardWithCustomSignText() throws Exception {
|
||||||
|
RecordingRepository repository = new RecordingRepository(fullRegistrations(), false);
|
||||||
|
FakeWorld world = fullWorld(false);
|
||||||
|
PhysicalBoardPlan.Placement title = newSigns().stream()
|
||||||
|
.filter(sign -> sign.signKind() == PhysicalBoardSignKind.TITLE)
|
||||||
|
.findFirst().orElseThrow();
|
||||||
|
world.existing.put(title.location(), new PhysicalBoardPlan.Placement(
|
||||||
|
title.location(), title.material(), PhysicalBoardSignKind.INSTRUCTION
|
||||||
|
));
|
||||||
|
|
||||||
|
assertEquals(0, upgrader(new BoardRegistry(repository), world).upgrade());
|
||||||
|
|
||||||
|
assertTrue(world.placed.isEmpty());
|
||||||
|
assertTrue(newSigns().stream().noneMatch(sign -> world.glowing.get(sign.location())));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void doesNotOverwriteAnOccupiedNewSignCell() throws Exception {
|
void doesNotOverwriteAnOccupiedNewSignCell() throws Exception {
|
||||||
RecordingRepository repository = new RecordingRepository(legacyRegistrations(), false);
|
RecordingRepository repository = new RecordingRepository(legacyRegistrations(), false);
|
||||||
@@ -89,6 +133,19 @@ final class PhysicalBoardUpgraderTest {
|
|||||||
assertEquals(0, repository.saves);
|
assertEquals(0, repository.saves);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rollsBackBothReadableSignsWhenFullBoardRefreshFails() throws Exception {
|
||||||
|
RecordingRepository repository = new RecordingRepository(fullRegistrations(), false);
|
||||||
|
FakeWorld world = fullWorld(false);
|
||||||
|
world.failPlacement = 1;
|
||||||
|
|
||||||
|
assertThrows(IOException.class,
|
||||||
|
() -> upgrader(new BoardRegistry(repository), world).upgrade());
|
||||||
|
|
||||||
|
assertTrue(newSigns().stream().noneMatch(sign -> world.glowing.get(sign.location())));
|
||||||
|
assertEquals(0, repository.saves);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void rollsBackBothCellsWhenAtomicPersistenceFails() throws Exception {
|
void rollsBackBothCellsWhenAtomicPersistenceFails() throws Exception {
|
||||||
RecordingRepository repository = new RecordingRepository(legacyRegistrations(), true);
|
RecordingRepository repository = new RecordingRepository(legacyRegistrations(), true);
|
||||||
@@ -120,6 +177,12 @@ final class PhysicalBoardUpgraderTest {
|
|||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Set<RegisteredBoard> fullRegistrations() {
|
||||||
|
return plan().interactionLocations().stream()
|
||||||
|
.map(location -> new RegisteredBoard(location, "survival"))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
private static FakeWorld legacyWorld() {
|
private static FakeWorld legacyWorld() {
|
||||||
FakeWorld world = new FakeWorld();
|
FakeWorld world = new FakeWorld();
|
||||||
plan().placements().stream().filter(placement ->
|
plan().placements().stream().filter(placement ->
|
||||||
@@ -132,6 +195,19 @@ final class PhysicalBoardUpgraderTest {
|
|||||||
return world;
|
return world;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static FakeWorld fullWorld(boolean readableGlowing) {
|
||||||
|
FakeWorld world = new FakeWorld();
|
||||||
|
plan().placements().forEach(placement -> {
|
||||||
|
world.existing.put(placement.location(), placement);
|
||||||
|
if (placement.sign()) {
|
||||||
|
world.facings.put(placement.location(), FACING);
|
||||||
|
world.glowing.put(placement.location(),
|
||||||
|
placement.signKind() != PhysicalBoardSignKind.DECORATIVE && readableGlowing);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return world;
|
||||||
|
}
|
||||||
|
|
||||||
private static java.util.List<PhysicalBoardPlan.Placement> newSigns() {
|
private static java.util.List<PhysicalBoardPlan.Placement> newSigns() {
|
||||||
return plan().placements().stream().filter(placement ->
|
return plan().placements().stream().filter(placement ->
|
||||||
placement.signKind() == PhysicalBoardSignKind.TITLE
|
placement.signKind() == PhysicalBoardSignKind.TITLE
|
||||||
@@ -140,8 +216,13 @@ final class PhysicalBoardUpgraderTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final class FakeWorld implements PhysicalBoardWorld {
|
private static final class FakeWorld implements PhysicalBoardWorld {
|
||||||
|
private record Snapshot(
|
||||||
|
PhysicalBoardPlan.Placement placement, BoardFacing facing, Boolean glowing
|
||||||
|
) { }
|
||||||
|
|
||||||
private final Map<BoardId, PhysicalBoardPlan.Placement> existing = new HashMap<>();
|
private final Map<BoardId, PhysicalBoardPlan.Placement> existing = new HashMap<>();
|
||||||
private final Map<BoardId, BoardFacing> facings = new HashMap<>();
|
private final Map<BoardId, BoardFacing> facings = new HashMap<>();
|
||||||
|
private final Map<BoardId, Boolean> glowing = new HashMap<>();
|
||||||
private final Map<BoardId, PhysicalBoardPlan.Placement> placed = new HashMap<>();
|
private final Map<BoardId, PhysicalBoardPlan.Placement> placed = new HashMap<>();
|
||||||
private int placements;
|
private int placements;
|
||||||
private int failPlacement = -1;
|
private int failPlacement = -1;
|
||||||
@@ -151,7 +232,9 @@ final class PhysicalBoardUpgraderTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override public Object snapshot(BoardId location) {
|
@Override public Object snapshot(BoardId location) {
|
||||||
return existing.get(location);
|
return new Snapshot(
|
||||||
|
existing.get(location), facings.get(location), glowing.get(location)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public void place(PhysicalBoardPlan.Placement placement, BoardFacing facing) {
|
@Override public void place(PhysicalBoardPlan.Placement placement, BoardFacing facing) {
|
||||||
@@ -160,6 +243,8 @@ final class PhysicalBoardUpgraderTest {
|
|||||||
}
|
}
|
||||||
existing.put(placement.location(), placement);
|
existing.put(placement.location(), placement);
|
||||||
facings.put(placement.location(), facing);
|
facings.put(placement.location(), facing);
|
||||||
|
glowing.put(placement.location(),
|
||||||
|
placement.signKind() != PhysicalBoardSignKind.DECORATIVE);
|
||||||
placed.put(placement.location(), placement);
|
placed.put(placement.location(), placement);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,13 +255,25 @@ final class PhysicalBoardUpgraderTest {
|
|||||||
&& (!placement.sign() || facing == facings.get(placement.location()));
|
&& (!placement.sign() || facing == facings.get(placement.location()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override public boolean matches(
|
||||||
|
PhysicalBoardPlan.Placement placement, BoardFacing facing, boolean glowingText
|
||||||
|
) {
|
||||||
|
return matches(placement, facing)
|
||||||
|
&& glowingText == glowing.getOrDefault(placement.location(), false);
|
||||||
|
}
|
||||||
|
|
||||||
@Override public void restore(BoardId location, Object snapshot) {
|
@Override public void restore(BoardId location, Object snapshot) {
|
||||||
if (snapshot == null) {
|
Snapshot captured = (Snapshot) snapshot;
|
||||||
existing.remove(location);
|
restore(existing, location, captured.placement());
|
||||||
facings.remove(location);
|
restore(facings, location, captured.facing());
|
||||||
|
restore(glowing, location, captured.glowing());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> void restore(Map<BoardId, T> values, BoardId location, T value) {
|
||||||
|
if (value == null) {
|
||||||
|
values.remove(location);
|
||||||
} else {
|
} else {
|
||||||
PhysicalBoardPlan.Placement placement = (PhysicalBoardPlan.Placement) snapshot;
|
values.put(location, value);
|
||||||
existing.put(location, placement);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user