feat(board): add readable physical board signs
This commit is contained in:
@@ -46,6 +46,10 @@ final class BoardRegistry {
|
||||
return boards.size();
|
||||
}
|
||||
|
||||
synchronized Map<BoardId, RegisteredBoard> registeredBoards() {
|
||||
return Map.copyOf(boards);
|
||||
}
|
||||
|
||||
private static Map<BoardId, RegisteredBoard> index(BoardState state) throws IOException {
|
||||
Map<BoardId, RegisteredBoard> indexed = new LinkedHashMap<>();
|
||||
for (RegisteredBoard board : state.boards()) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package games.dmg.spigotquestboard;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
@@ -17,6 +18,13 @@ final class BukkitPhysicalBoardWorld implements PhysicalBoardWorld {
|
||||
private static final Component DECORATIVE_TEXT = Component.text("xxxxxxxx")
|
||||
.color(NamedTextColor.DARK_GREEN)
|
||||
.decorate(TextDecoration.OBFUSCATED);
|
||||
private static final List<Component> TITLE_TEXT = List.of(
|
||||
Component.text("Quest"), Component.text("Board"), Component.empty(), Component.empty()
|
||||
);
|
||||
private static final List<Component> INSTRUCTION_TEXT = List.of(
|
||||
Component.text("Right-click"), Component.text("a sign below"),
|
||||
Component.text("to browse"), Component.text("quests")
|
||||
);
|
||||
private final World world;
|
||||
|
||||
BukkitPhysicalBoardWorld(World world) {
|
||||
@@ -55,14 +63,39 @@ final class BukkitPhysicalBoardWorld implements PhysicalBoardWorld {
|
||||
if (!(state instanceof Sign sign)) {
|
||||
throw new IllegalStateException("Oak wall sign did not create sign state");
|
||||
}
|
||||
for (int line = 0; line < 4; line++) {
|
||||
sign.getSide(Side.FRONT).line(line, DECORATIVE_TEXT);
|
||||
List<Component> lines = signLines(placement.signKind());
|
||||
for (int line = 0; line < lines.size(); line++) {
|
||||
sign.getSide(Side.FRONT).line(line, lines.get(line));
|
||||
}
|
||||
if (!sign.update(true, false)) {
|
||||
throw new IllegalStateException("Could not configure physical quest-board sign");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(PhysicalBoardPlan.Placement placement, BoardFacing facing) {
|
||||
Block block = block(placement.location());
|
||||
Material material = Material.matchMaterial(placement.material());
|
||||
if (material == null || block.getType() != material) {
|
||||
return false;
|
||||
}
|
||||
if (!placement.sign()) {
|
||||
return true;
|
||||
}
|
||||
if (!(block.getBlockData() instanceof WallSign wallSign)
|
||||
|| wallSign.getFacing() != facing.blockFace()
|
||||
|| !(block.getState() instanceof Sign sign)) {
|
||||
return false;
|
||||
}
|
||||
List<Component> expected = signLines(placement.signKind());
|
||||
for (int line = 0; line < expected.size(); line++) {
|
||||
if (!expected.get(line).equals(sign.getSide(Side.FRONT).line(line))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restore(BoardId location, Object snapshot) {
|
||||
if (!(snapshot instanceof BlockState state) || !state.update(true, false)) {
|
||||
@@ -70,6 +103,16 @@ final class BukkitPhysicalBoardWorld implements PhysicalBoardWorld {
|
||||
}
|
||||
}
|
||||
|
||||
static List<Component> signLines(PhysicalBoardSignKind kind) {
|
||||
return switch (Objects.requireNonNull(kind, "kind")) {
|
||||
case TITLE -> TITLE_TEXT;
|
||||
case INSTRUCTION -> INSTRUCTION_TEXT;
|
||||
case DECORATIVE -> List.of(
|
||||
DECORATIVE_TEXT, DECORATIVE_TEXT, DECORATIVE_TEXT, DECORATIVE_TEXT
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private Block block(BoardId location) {
|
||||
if (!world.getUID().equals(location.worldId())) {
|
||||
throw new IllegalArgumentException("Physical board location belongs to another world");
|
||||
|
||||
@@ -22,8 +22,8 @@ record PhysicalBoardPlan(
|
||||
static PhysicalBoardPlan create(BoardId anchor, BoardFacing facing) {
|
||||
Objects.requireNonNull(anchor, "anchor");
|
||||
Objects.requireNonNull(facing, "facing");
|
||||
List<Placement> placements = new ArrayList<>(23);
|
||||
Set<BoardId> interactions = new LinkedHashSet<>(15);
|
||||
List<Placement> placements = new ArrayList<>(25);
|
||||
Set<BoardId> interactions = new LinkedHashSet<>(17);
|
||||
|
||||
for (int height = 1; height <= 4; height++) {
|
||||
for (int lateral = -2; lateral <= 2; lateral++) {
|
||||
@@ -37,12 +37,32 @@ record PhysicalBoardPlan(
|
||||
}
|
||||
for (int lateral = -1; lateral <= 1; lateral++) {
|
||||
BoardId location = offset(anchor, facing, lateral, 2, 1);
|
||||
placements.add(new Placement(location, "OAK_WALL_SIGN"));
|
||||
placements.add(new Placement(
|
||||
location, "OAK_WALL_SIGN", PhysicalBoardSignKind.DECORATIVE
|
||||
));
|
||||
interactions.add(location);
|
||||
}
|
||||
addSign(
|
||||
placements, interactions, offset(anchor, facing, 0, 3, 1),
|
||||
PhysicalBoardSignKind.INSTRUCTION
|
||||
);
|
||||
addSign(
|
||||
placements, interactions, offset(anchor, facing, 0, 4, 1),
|
||||
PhysicalBoardSignKind.TITLE
|
||||
);
|
||||
return new PhysicalBoardPlan(facing, placements, interactions);
|
||||
}
|
||||
|
||||
private static void addSign(
|
||||
List<Placement> placements,
|
||||
Set<BoardId> interactions,
|
||||
BoardId location,
|
||||
PhysicalBoardSignKind kind
|
||||
) {
|
||||
placements.add(new Placement(location, "OAK_WALL_SIGN", kind));
|
||||
interactions.add(location);
|
||||
}
|
||||
|
||||
private static BoardId offset(
|
||||
BoardId anchor, BoardFacing facing, int lateral, int vertical, int forward
|
||||
) {
|
||||
@@ -54,14 +74,29 @@ record PhysicalBoardPlan(
|
||||
);
|
||||
}
|
||||
|
||||
record Placement(BoardId location, String material) {
|
||||
record Placement(
|
||||
BoardId location,
|
||||
String material,
|
||||
PhysicalBoardSignKind signKind
|
||||
) {
|
||||
Placement {
|
||||
Objects.requireNonNull(location, "location");
|
||||
Objects.requireNonNull(material, "material");
|
||||
if ("OAK_WALL_SIGN".equals(material) != (signKind != null)) {
|
||||
throw new IllegalArgumentException("Only wall signs require sign metadata");
|
||||
}
|
||||
}
|
||||
|
||||
Placement(BoardId location, String material) {
|
||||
this(
|
||||
location,
|
||||
material,
|
||||
"OAK_WALL_SIGN".equals(material) ? PhysicalBoardSignKind.DECORATIVE : null
|
||||
);
|
||||
}
|
||||
|
||||
boolean sign() {
|
||||
return "OAK_WALL_SIGN".equals(material);
|
||||
return signKind != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package games.dmg.spigotquestboard;
|
||||
|
||||
enum PhysicalBoardSignKind {
|
||||
TITLE,
|
||||
INSTRUCTION,
|
||||
DECORATIVE
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package games.dmg.spigotquestboard;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
final class PhysicalBoardUpgrader {
|
||||
interface WorldFactory {
|
||||
PhysicalBoardWorld open(UUID worldId);
|
||||
}
|
||||
|
||||
private record Candidate(BoardId anchor, BoardFacing facing, String worldName) { }
|
||||
private record RelativeInteraction(int lateral, int height, int forward) { }
|
||||
|
||||
private static final List<RelativeInteraction> LEGACY_INTERACTIONS = legacyInteractions();
|
||||
|
||||
private final BoardRegistry registry;
|
||||
private final WorldFactory worlds;
|
||||
|
||||
PhysicalBoardUpgrader(BoardRegistry registry, WorldFactory worlds) {
|
||||
this.registry = Objects.requireNonNull(registry, "registry");
|
||||
this.worlds = Objects.requireNonNull(worlds, "worlds");
|
||||
}
|
||||
|
||||
int upgrade() throws IOException {
|
||||
int upgraded = 0;
|
||||
Map<BoardId, RegisteredBoard> registered = registry.registeredBoards();
|
||||
for (Candidate candidate : candidates(registered)) {
|
||||
if (!hasLegacyRegistrations(candidate, registered)) {
|
||||
continue;
|
||||
}
|
||||
PhysicalBoardWorld world = worlds.open(candidate.anchor().worldId());
|
||||
if (world != null && isLegacyBoard(candidate, world)
|
||||
&& upgrade(candidate, world)) {
|
||||
upgraded++;
|
||||
registered = registry.registeredBoards();
|
||||
}
|
||||
}
|
||||
return upgraded;
|
||||
}
|
||||
|
||||
private static boolean hasLegacyRegistrations(
|
||||
Candidate candidate, Map<BoardId, RegisteredBoard> registered
|
||||
) {
|
||||
PhysicalBoardPlan plan = PhysicalBoardPlan.create(candidate.anchor(), candidate.facing());
|
||||
for (BoardId location : legacyInteractionLocations(plan)) {
|
||||
RegisteredBoard board = registered.get(location);
|
||||
if (board == null || !candidate.worldName().equals(board.worldName())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return newSigns(plan).stream().noneMatch(sign ->
|
||||
registered.containsKey(sign.location())
|
||||
);
|
||||
}
|
||||
|
||||
private boolean isLegacyBoard(Candidate candidate, PhysicalBoardWorld world) {
|
||||
PhysicalBoardPlan plan = PhysicalBoardPlan.create(candidate.anchor(), candidate.facing());
|
||||
for (PhysicalBoardPlan.Placement placement : legacyPlacements(plan)) {
|
||||
if (!world.matches(placement, candidate.facing())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (PhysicalBoardPlan.Placement placement : newSigns(plan)) {
|
||||
if (!world.isEmpty(placement.location())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean upgrade(Candidate candidate, PhysicalBoardWorld world) throws IOException {
|
||||
List<PhysicalBoardPlan.Placement> signs = newSigns(
|
||||
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());
|
||||
}
|
||||
List<RegisteredBoard> additions = signs.stream()
|
||||
.map(sign -> new RegisteredBoard(sign.location(), candidate.worldName()))
|
||||
.toList();
|
||||
if (registry.registerAll(additions) != BoardRegistrationResult.CREATED) {
|
||||
rollback(world, snapshots, null);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (RuntimeException | IOException exception) {
|
||||
rollback(world, snapshots, exception);
|
||||
if (exception instanceof IOException ioException) {
|
||||
throw ioException;
|
||||
}
|
||||
throw new IOException("Could not upgrade physical quest board", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<Candidate> candidates(Map<BoardId, RegisteredBoard> registered) {
|
||||
Set<Candidate> candidates = new LinkedHashSet<>();
|
||||
for (RegisteredBoard board : registered.values()) {
|
||||
for (BoardFacing facing : BoardFacing.values()) {
|
||||
for (RelativeInteraction relative : LEGACY_INTERACTIONS) {
|
||||
BoardId location = board.id();
|
||||
candidates.add(new Candidate(new BoardId(
|
||||
location.worldId(),
|
||||
location.x() - relative.lateral() * facing.rightX()
|
||||
- relative.forward() * facing.x(),
|
||||
location.y() - relative.height(),
|
||||
location.z() - relative.lateral() * facing.rightZ()
|
||||
- relative.forward() * facing.z()
|
||||
), facing, board.worldName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private static List<RelativeInteraction> legacyInteractions() {
|
||||
List<RelativeInteraction> interactions = new ArrayList<>(15);
|
||||
for (int height = 1; height <= 4; height++) {
|
||||
for (int lateral = -1; lateral <= 1; lateral++) {
|
||||
interactions.add(new RelativeInteraction(lateral, height, 0));
|
||||
}
|
||||
}
|
||||
for (int lateral = -1; lateral <= 1; lateral++) {
|
||||
interactions.add(new RelativeInteraction(lateral, 2, 1));
|
||||
}
|
||||
return List.copyOf(interactions);
|
||||
}
|
||||
|
||||
private static List<PhysicalBoardPlan.Placement> legacyPlacements(PhysicalBoardPlan plan) {
|
||||
return plan.placements().stream().filter(placement ->
|
||||
!placement.sign() || placement.signKind() == PhysicalBoardSignKind.DECORATIVE
|
||||
).toList();
|
||||
}
|
||||
|
||||
private static Set<BoardId> legacyInteractionLocations(PhysicalBoardPlan plan) {
|
||||
Set<BoardId> locations = new LinkedHashSet<>();
|
||||
for (PhysicalBoardPlan.Placement placement : legacyPlacements(plan)) {
|
||||
if ("OAK_PLANKS".equals(placement.material()) || placement.sign()) {
|
||||
locations.add(placement.location());
|
||||
}
|
||||
}
|
||||
return Set.copyOf(locations);
|
||||
}
|
||||
|
||||
private static List<PhysicalBoardPlan.Placement> newSigns(PhysicalBoardPlan plan) {
|
||||
return plan.placements().stream().filter(placement ->
|
||||
placement.signKind() == PhysicalBoardSignKind.TITLE
|
||||
|| placement.signKind() == PhysicalBoardSignKind.INSTRUCTION
|
||||
).toList();
|
||||
}
|
||||
|
||||
private static void rollback(
|
||||
PhysicalBoardWorld world, Map<BoardId, Object> snapshots, Throwable failure
|
||||
) throws IOException {
|
||||
RuntimeException rollbackFailure = null;
|
||||
List<Map.Entry<BoardId, Object>> entries = new ArrayList<>(snapshots.entrySet());
|
||||
for (int index = entries.size() - 1; index >= 0; index--) {
|
||||
Map.Entry<BoardId, Object> entry = entries.get(index);
|
||||
try {
|
||||
world.restore(entry.getKey(), entry.getValue());
|
||||
} catch (RuntimeException exception) {
|
||||
if (rollbackFailure == null) {
|
||||
rollbackFailure = exception;
|
||||
} else {
|
||||
rollbackFailure.addSuppressed(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rollbackFailure != null) {
|
||||
if (failure != null) {
|
||||
failure.addSuppressed(rollbackFailure);
|
||||
} else {
|
||||
throw new IOException("Could not restore physical board upgrade", rollbackFailure);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,5 +7,9 @@ interface PhysicalBoardWorld {
|
||||
|
||||
void place(PhysicalBoardPlan.Placement placement, BoardFacing facing);
|
||||
|
||||
default boolean matches(PhysicalBoardPlan.Placement placement, BoardFacing facing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void restore(BoardId location, Object snapshot);
|
||||
}
|
||||
|
||||
@@ -13,10 +13,15 @@ public final class SpigotQuestBoardPlugin extends JavaPlugin {
|
||||
final BoardRegistry boards;
|
||||
final QuestService quests;
|
||||
final PlayerCommandSettings playerCommands;
|
||||
final int upgradedBoards;
|
||||
try {
|
||||
boards = new BoardRegistry(new YamlBoardRepository(
|
||||
getDataFolder().toPath().resolve("boards.yml")
|
||||
));
|
||||
upgradedBoards = upgradePhysicalBoards(boards, worldId -> {
|
||||
org.bukkit.World world = getServer().getWorld(worldId);
|
||||
return world == null ? null : new BukkitPhysicalBoardWorld(world);
|
||||
});
|
||||
quests = new QuestService(new YamlQuestRepository(
|
||||
getDataFolder().toPath().resolve("quests.yml")
|
||||
));
|
||||
@@ -69,10 +74,17 @@ public final class SpigotQuestBoardPlugin extends JavaPlugin {
|
||||
);
|
||||
getLogger().info(
|
||||
"Spigot Quest Board enabled with " + boards.size() + " boards and "
|
||||
+ quests.state().quests().size() + " quests."
|
||||
+ quests.state().quests().size() + " quests; upgraded " + upgradedBoards
|
||||
+ " physical boards."
|
||||
);
|
||||
}
|
||||
|
||||
static int upgradePhysicalBoards(
|
||||
BoardRegistry boards, PhysicalBoardUpgrader.WorldFactory worlds
|
||||
) throws IOException {
|
||||
return new PhysicalBoardUpgrader(boards, worlds).upgrade();
|
||||
}
|
||||
|
||||
static CommandHandlers commandHandlers(
|
||||
BoardRegistry boards,
|
||||
PlayerCommandSettings playerCommands,
|
||||
|
||||
Reference in New Issue
Block a user