feat(admin): control player quest commands
Release / release (push) Successful in 2m8s
CI / build (push) Successful in 1m15s

This commit is contained in:
dmg
2026-09-05 08:36:45 -04:00
parent af6d6d0827
commit be13b9b87c
14 changed files with 537 additions and 30 deletions
@@ -0,0 +1,23 @@
package games.dmg.spigotquestboard;
import java.io.IOException;
import java.util.Objects;
final class PlayerCommandSettings {
private final PlayerCommandSettingsRepository repository;
private boolean enabled;
PlayerCommandSettings(PlayerCommandSettingsRepository repository) throws IOException {
this.repository = Objects.requireNonNull(repository, "repository");
enabled = repository.loadEnabled();
}
boolean enabled() {
return enabled;
}
void setEnabled(boolean enabled) throws IOException {
repository.saveEnabled(enabled);
this.enabled = enabled;
}
}
@@ -0,0 +1,8 @@
package games.dmg.spigotquestboard;
import java.io.IOException;
interface PlayerCommandSettingsRepository {
boolean loadEnabled() throws IOException;
void saveEnabled(boolean enabled) throws IOException;
}
@@ -1,19 +1,24 @@
package games.dmg.spigotquestboard;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
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.command.TabCompleter;
import org.bukkit.entity.Player;
final class QuestAdminCommand implements CommandExecutor {
final class QuestAdminCommand implements CommandExecutor, TabCompleter {
private static final String PERMISSION = "spigotquestboard.admin";
private final BoardRegistry registry;
private final PlayerCommandSettings playerCommands;
QuestAdminCommand(BoardRegistry registry) {
QuestAdminCommand(BoardRegistry registry, PlayerCommandSettings playerCommands) {
this.registry = Objects.requireNonNull(registry, "registry");
this.playerCommands = Objects.requireNonNull(playerCommands, "playerCommands");
}
@Override
@@ -24,18 +29,50 @@ final class QuestAdminCommand implements CommandExecutor {
sender.sendMessage("You do not have permission to administer quest boards.");
return true;
}
if (arguments.length == 2 && "commands".equalsIgnoreCase(arguments[0])) {
return updatePlayerCommands(sender, arguments[1]);
}
if (arguments.length != 1 || !"createboard".equalsIgnoreCase(arguments[0])) {
sender.sendMessage("Usage: /questadmin createboard");
sender.sendMessage(
"Usage: /questadmin createboard | /questadmin commands enable|disable"
);
return true;
}
createBoard(sender);
return true;
}
private boolean updatePlayerCommands(CommandSender sender, String action) {
final boolean enabled;
if ("enable".equalsIgnoreCase(action)) {
enabled = true;
} else if ("disable".equalsIgnoreCase(action)) {
enabled = false;
} else {
sender.sendMessage("Usage: /questadmin commands enable|disable");
return true;
}
try {
playerCommands.setEnabled(enabled);
sender.sendMessage("Player quest commands " + (enabled ? "enabled." : "disabled."));
} catch (IOException exception) {
sender.sendMessage(
"Player command setting could not be saved. Player quest commands remain "
+ (playerCommands.enabled() ? "enabled." : "disabled.")
);
}
return true;
}
private void createBoard(CommandSender sender) {
if (!(sender instanceof Player player)) {
sender.sendMessage("A player must target the quest board block.");
return true;
return;
}
Block target = player.getTargetBlockExact(5);
if (target == null) {
sender.sendMessage("Target a physical block within five blocks.");
return true;
return;
}
try {
BoardRegistrationResult result = registry.register(RegisteredBoard.from(target));
@@ -45,6 +82,28 @@ final class QuestAdminCommand implements CommandExecutor {
} catch (IOException exception) {
sender.sendMessage("The quest board could not be saved. No board was created.");
}
return true;
}
@Override
public List<String> onTabComplete(
CommandSender sender, Command command, String alias, String[] arguments
) {
if (!sender.hasPermission(PERMISSION)) {
return List.of();
}
if (arguments.length == 1) {
return startsWith(List.of("createboard", "commands"), arguments[0]);
}
if (arguments.length == 2 && "commands".equalsIgnoreCase(arguments[0])) {
return startsWith(List.of("enable", "disable"), arguments[1]);
}
return List.of();
}
private static List<String> startsWith(List<String> candidates, String prefix) {
String normalized = prefix.toLowerCase(Locale.ROOT);
return candidates.stream()
.filter(candidate -> candidate.startsWith(normalized))
.toList();
}
}
@@ -20,6 +20,7 @@ final class QuestCommand implements CommandExecutor, TabCompleter {
private final QuestCancellationGateway canceller;
private final QuestClaimGateway claimant;
private final Clock clock;
private final PlayerCommandSettings playerCommands;
QuestCommand(QuestCreationGateway creator, QuestBrowser browser, Clock clock) {
this(creator, browser, null, null, null, clock);
@@ -51,6 +52,18 @@ final class QuestCommand implements CommandExecutor, TabCompleter {
QuestCancellationGateway canceller,
QuestClaimGateway claimant,
Clock clock
) {
this(creator, browser, completer, canceller, claimant, clock, null);
}
QuestCommand(
QuestCreationGateway creator,
QuestBrowser browser,
QuestCompletionGateway completer,
QuestCancellationGateway canceller,
QuestClaimGateway claimant,
Clock clock,
PlayerCommandSettings playerCommands
) {
this.creator = Objects.requireNonNull(creator, "creator");
this.browser = Objects.requireNonNull(browser, "browser");
@@ -58,12 +71,19 @@ final class QuestCommand implements CommandExecutor, TabCompleter {
this.canceller = canceller;
this.claimant = claimant;
this.clock = Objects.requireNonNull(clock, "clock");
this.playerCommands = playerCommands;
}
@Override
public boolean onCommand(
CommandSender sender, Command command, String label, String[] arguments
) {
if (!playerCommandsEnabled()) {
sender.sendMessage(
"Player quest commands are disabled. Use a physical quest board instead."
);
return true;
}
if (arguments.length == 0
|| (arguments.length == 1 && "list".equalsIgnoreCase(arguments[0]))) {
Instant now = clock.instant();
@@ -158,7 +178,7 @@ final class QuestCommand implements CommandExecutor, TabCompleter {
public List<String> onTabComplete(
CommandSender sender, Command command, String alias, String[] arguments
) {
if (!(sender instanceof Player)) {
if (!playerCommandsEnabled() || !(sender instanceof Player)) {
return List.of();
}
if (arguments.length == 1) {
@@ -184,6 +204,10 @@ final class QuestCommand implements CommandExecutor, TabCompleter {
return List.of();
}
private boolean playerCommandsEnabled() {
return playerCommands == null || playerCommands.enabled();
}
private static List<String> startsWith(List<String> candidates, String prefix) {
String normalized = prefix.toLowerCase(Locale.ROOT);
return candidates.stream()
@@ -12,6 +12,7 @@ public final class SpigotQuestBoardPlugin extends JavaPlugin {
public void onEnable() {
final BoardRegistry boards;
final QuestService quests;
final PlayerCommandSettings playerCommands;
try {
boards = new BoardRegistry(new YamlBoardRepository(
getDataFolder().toPath().resolve("boards.yml")
@@ -19,15 +20,17 @@ public final class SpigotQuestBoardPlugin extends JavaPlugin {
quests = new QuestService(new YamlQuestRepository(
getDataFolder().toPath().resolve("quests.yml")
));
playerCommands = new PlayerCommandSettings(
new YamlPlayerCommandSettingsRepository(
getDataFolder().toPath().resolve("settings.yml")
)
);
} catch (IOException exception) {
getLogger().log(Level.SEVERE, "Could not load quest board state", exception);
getLogger().log(Level.SEVERE, "Could not load plugin state", exception);
getServer().getPluginManager().disablePlugin(this);
return;
}
QuestAdminCommand admin = new QuestAdminCommand(boards);
command("questadmin").setExecutor(admin);
QuestCreationGateway creator = new QuestCreationController(
quests, new BukkitBlockMaterialCatalog(), new BukkitHeldRewardInventory()
);
@@ -42,11 +45,13 @@ public final class SpigotQuestBoardPlugin extends JavaPlugin {
QuestClaimGateway claimant = new QuestClaimController(
quests, new BukkitQuestClaimInventory()
);
QuestCommand questCommand = new QuestCommand(
creator, quests, completer, canceller, claimant, clock
CommandHandlers handlers = commandHandlers(
boards, playerCommands, creator, quests, completer, canceller, claimant, clock
);
command("quests").setExecutor(questCommand);
command("quests").setTabCompleter(questCommand);
command("questadmin").setExecutor(handlers.admin());
command("questadmin").setTabCompleter(handlers.admin());
command("quests").setExecutor(handlers.quests());
command("quests").setTabCompleter(handlers.quests());
getServer().getPluginManager().registerEvents(notifier, this);
getServer().getPluginManager().registerEvents(
new QuestBoardInteractionListener(
@@ -68,6 +73,26 @@ public final class SpigotQuestBoardPlugin extends JavaPlugin {
);
}
static CommandHandlers commandHandlers(
BoardRegistry boards,
PlayerCommandSettings playerCommands,
QuestCreationGateway creator,
QuestBrowser browser,
QuestCompletionGateway completer,
QuestCancellationGateway canceller,
QuestClaimGateway claimant,
Clock clock
) {
return new CommandHandlers(
new QuestAdminCommand(boards, playerCommands),
new QuestCommand(
creator, browser, completer, canceller, claimant, clock, playerCommands
)
);
}
record CommandHandlers(QuestAdminCommand admin, QuestCommand quests) { }
private PluginCommand command(String name) {
return Objects.requireNonNull(getCommand(name), "Missing command metadata for " + name);
}
@@ -0,0 +1,53 @@
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.Objects;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration;
final class YamlPlayerCommandSettingsRepository implements PlayerCommandSettingsRepository {
private static final String ENABLED_PATH = "player-commands.enabled";
private final Path path;
YamlPlayerCommandSettingsRepository(Path path) {
this.path = Objects.requireNonNull(path, "path");
}
@Override
public boolean loadEnabled() throws IOException {
if (!Files.exists(path)) {
return false;
}
YamlConfiguration yaml = new YamlConfiguration();
try {
yaml.load(path.toFile());
} catch (InvalidConfigurationException exception) {
throw new IOException("Invalid player command settings", exception);
}
return yaml.getBoolean(ENABLED_PATH, false);
}
@Override
public void saveEnabled(boolean enabled) throws IOException {
YamlConfiguration yaml = new YamlConfiguration();
yaml.set(ENABLED_PATH, enabled);
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);
}
}
}