feat(command): add personal Tree Feller controls
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
package games.dmg.treefeller;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
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;
|
||||
|
||||
/** Player-only `/treefeller` command tree. */
|
||||
public final class TreeFellerCommand implements CommandExecutor, TabCompleter {
|
||||
private static final List<String> SUBCOMMANDS = List.of("enabled", "undo", "unlocked");
|
||||
private static final List<String> BOOLEAN_VALUES = List.of("off", "on");
|
||||
|
||||
private final PlayerStateStore states;
|
||||
private final Consumer<Exception> failureHandler;
|
||||
private final Function<String, String> messages;
|
||||
|
||||
public TreeFellerCommand(PlayerStateStore states, Consumer<Exception> failureHandler) {
|
||||
this(states, failureHandler, TreeFellerCommand::defaultMessage);
|
||||
}
|
||||
|
||||
public TreeFellerCommand(
|
||||
PlayerStateStore states,
|
||||
Consumer<Exception> failureHandler,
|
||||
Function<String, String> messages) {
|
||||
this.states = states;
|
||||
this.failureHandler = failureHandler;
|
||||
this.messages = messages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(
|
||||
CommandSender sender, Command command, String label, String[] arguments) {
|
||||
if (!(sender instanceof Player player)) {
|
||||
sender.sendMessage("Tree Feller player commands must be used in game.");
|
||||
return true;
|
||||
}
|
||||
if (arguments.length == 0) {
|
||||
sendUsage(player);
|
||||
return true;
|
||||
}
|
||||
if (!arguments[0].equalsIgnoreCase("enabled")) {
|
||||
sendUsage(player);
|
||||
return true;
|
||||
}
|
||||
|
||||
PlayerTreeFellerState state = stateFor(player);
|
||||
if (arguments.length == 1) {
|
||||
player.sendMessage("Tree Feller is " + (state.enabled() ? "enabled" : "disabled") + ".");
|
||||
if (state.locked()) {
|
||||
player.sendMessage("An administrative lock currently overrides your preference.");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (arguments.length != 2) {
|
||||
player.sendMessage("Usage: /treefeller enabled <on|off>");
|
||||
return true;
|
||||
}
|
||||
|
||||
Optional<Boolean> requested = parseBoolean(arguments[1]);
|
||||
if (requested.isEmpty()) {
|
||||
player.sendMessage("Usage: /treefeller enabled <on|off>");
|
||||
return true;
|
||||
}
|
||||
boolean enabled = requested.orElseThrow();
|
||||
if (state.enabled() == enabled) {
|
||||
player.sendMessage("Tree Feller is already " + (enabled ? "enabled" : "disabled") + ".");
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
states.save(state.withEnabled(enabled));
|
||||
player.sendMessage(color(messages.apply(enabled ? "enabled" : "disabled")));
|
||||
} catch (IOException exception) {
|
||||
failureHandler.accept(exception);
|
||||
player.sendMessage("Tree Feller could not save your preference; no change was applied.");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(
|
||||
CommandSender sender, Command command, String alias, String[] arguments) {
|
||||
if (arguments.length == 1) {
|
||||
return matching(SUBCOMMANDS, arguments[0]);
|
||||
}
|
||||
if (arguments.length == 2 && arguments[0].equalsIgnoreCase("enabled")) {
|
||||
return matching(BOOLEAN_VALUES, arguments[1]);
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private PlayerTreeFellerState stateFor(Player player) {
|
||||
return states.load(player.getUniqueId())
|
||||
.orElseGet(() -> PlayerTreeFellerState.initial(
|
||||
player.getUniqueId(), player.getName()))
|
||||
.observeName(player.getName());
|
||||
}
|
||||
|
||||
private void sendUsage(Player player) {
|
||||
player.sendMessage("Usage: /treefeller <enabled|unlocked|undo>");
|
||||
}
|
||||
|
||||
private static Optional<Boolean> parseBoolean(String value) {
|
||||
if (value.equalsIgnoreCase("on")) {
|
||||
return Optional.of(true);
|
||||
}
|
||||
if (value.equalsIgnoreCase("off")) {
|
||||
return Optional.of(false);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private static List<String> matching(List<String> values, String prefix) {
|
||||
String normalized = prefix.toLowerCase(Locale.ROOT);
|
||||
return values.stream().filter(value -> value.startsWith(normalized)).toList();
|
||||
}
|
||||
|
||||
private static String defaultMessage(String key) {
|
||||
return key.equals("enabled") ? "Tree Feller is enabled." : "Tree Feller is disabled.";
|
||||
}
|
||||
|
||||
private static String color(String value) {
|
||||
return value.replace('&', '\u00a7');
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package games.dmg.treefeller;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
import java.util.logging.Level;
|
||||
import org.bukkit.command.PluginCommand;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
/** Entry point for Tree Feller. */
|
||||
@@ -32,6 +34,16 @@ public final class TreeFellerPlugin extends JavaPlugin {
|
||||
exception -> getLogger().log(
|
||||
Level.SEVERE, "Unable to persist Tree Feller progress", exception));
|
||||
getServer().getPluginManager().registerEvents(progressListener, this);
|
||||
|
||||
TreeFellerCommand playerCommand = new TreeFellerCommand(
|
||||
playerStateRepository,
|
||||
exception -> getLogger().log(
|
||||
Level.SEVERE, "Unable to persist Tree Feller preference", exception),
|
||||
key -> settingsService.current().message(key));
|
||||
PluginCommand command = Objects.requireNonNull(
|
||||
getCommand("treefeller"), "treefeller command missing from plugin.yml");
|
||||
command.setExecutor(playerCommand);
|
||||
command.setTabCompleter(playerCommand);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
getLogger().severe("Tree Feller configuration is invalid: " + exception.getMessage());
|
||||
getServer().getPluginManager().disablePlugin(this);
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package games.dmg.treefeller;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.mockito.ArgumentMatchers.contains;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class TreeFellerCommandTest {
|
||||
@Test
|
||||
void persistentlyDisablesTheIssuingPlayerWithoutChangingUnlocks() throws Exception {
|
||||
UUID playerId = UUID.randomUUID();
|
||||
Player player = mock(Player.class);
|
||||
when(player.getUniqueId()).thenReturn(playerId);
|
||||
when(player.getName()).thenReturn("Player");
|
||||
InMemoryStateStore states = new InMemoryStateStore();
|
||||
states.state = PlayerTreeFellerState.initial(playerId, "Player")
|
||||
.withUnlocked(TreeSpecies.OAK, true);
|
||||
TreeFellerCommand handler = new TreeFellerCommand(states, ignored -> { });
|
||||
|
||||
boolean handled = handler.onCommand(
|
||||
player, mock(Command.class), "treefeller", new String[] {"enabled", "off"});
|
||||
|
||||
assertFalse(states.state.enabled());
|
||||
assertEquals(true, states.state.isUnlocked(TreeSpecies.OAK));
|
||||
assertEquals(1, states.saveCount);
|
||||
verify(player).sendMessage(contains("disabled"));
|
||||
assertEquals(true, handled);
|
||||
}
|
||||
|
||||
@Test
|
||||
void reportsTheSavedPreferenceAndAdministrativeOverrideWithoutChangingState() {
|
||||
UUID playerId = UUID.randomUUID();
|
||||
Player player = mock(Player.class);
|
||||
when(player.getUniqueId()).thenReturn(playerId);
|
||||
when(player.getName()).thenReturn("Player");
|
||||
InMemoryStateStore states = new InMemoryStateStore();
|
||||
states.state = PlayerTreeFellerState.initial(playerId, "Player").withLocked(true);
|
||||
TreeFellerCommand handler = new TreeFellerCommand(states, ignored -> { });
|
||||
|
||||
handler.onCommand(player, mock(Command.class), "treefeller", new String[] {"enabled"});
|
||||
|
||||
verify(player).sendMessage(contains("enabled"));
|
||||
verify(player).sendMessage(contains("administrative lock"));
|
||||
assertEquals(0, states.saveCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
void completesOnlyPlayerCommandSyntaxByArgumentPosition() {
|
||||
TreeFellerCommand handler = new TreeFellerCommand(new InMemoryStateStore(), ignored -> { });
|
||||
Command command = mock(Command.class);
|
||||
|
||||
assertEquals(List.of("enabled"), handler.onTabComplete(
|
||||
mock(Player.class), command, "treefeller", new String[] {"e"}));
|
||||
assertEquals(List.of("off", "on"), handler.onTabComplete(
|
||||
mock(Player.class), command, "treefeller", new String[] {"enabled", ""}));
|
||||
assertEquals(List.of(), handler.onTabComplete(
|
||||
mock(Player.class), command, "treefeller", new String[] {"admin", ""}));
|
||||
}
|
||||
|
||||
private static final class InMemoryStateStore implements PlayerStateStore {
|
||||
private PlayerTreeFellerState state;
|
||||
private int saveCount;
|
||||
|
||||
@Override
|
||||
public Optional<PlayerTreeFellerState> load(UUID playerId) {
|
||||
return Optional.ofNullable(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(PlayerTreeFellerState changed) {
|
||||
state = changed;
|
||||
saveCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user