feat(commands): add permission-aware tab completion
This commit is contained in:
@@ -48,6 +48,8 @@ Restart the server after copying the JAR. The plugin creates its configuration a
|
|||||||
|
|
||||||
The personal progress permission defaults to everyone. Administrative permissions default to server operators. Administrative changes are recorded in the server log.
|
The personal progress permission defaults to everyone. Administrative permissions default to server operators. Administrative changes are recorded in the server log.
|
||||||
|
|
||||||
|
Commands provide permission-aware tab completion for subcommands, online player names, and valid ranks. Known offline players remain valid command targets but are not suggested.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ For explosions, player damage events establish whether an unlocked player would
|
|||||||
|
|
||||||
## Commands and configuration
|
## Commands and configuration
|
||||||
|
|
||||||
`/creeperaura` exposes player progress and permission-protected offline administration. Rank requirements, multipliers, feedback duration, and messages are loaded from YAML. Valid command-based changes are written back to YAML and survive restart.
|
`/creeperaura` exposes player progress and permission-protected offline administration. Permission-aware tab completion suggests available subcommands, online players, and valid ranks without querying SQLite on the server thread. Rank requirements, multipliers, feedback duration, and messages are loaded from YAML. Valid command-based changes are written back to YAML and survive restart.
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ description: Chronological record of material changes to the Spigot Creeper Fear
|
|||||||
|
|
||||||
# Design Log
|
# Design Log
|
||||||
|
|
||||||
|
## 2026-08-10
|
||||||
|
|
||||||
|
- Completed US-008 with permission-aware command, online-player, and rank tab completion.
|
||||||
|
|
||||||
## 2026-08-08
|
## 2026-08-08
|
||||||
|
|
||||||
- Established the OKF v0.1 design bundle.
|
- Established the OKF v0.1 design bundle.
|
||||||
|
|||||||
@@ -13,3 +13,4 @@ description: Catalog of user stories for the Spigot Creeper Fear plugin.
|
|||||||
- [US-005: Administer player progression](us-005-administer-player-progression.md)
|
- [US-005: Administer player progression](us-005-administer-player-progression.md)
|
||||||
- [US-006: Configure aura progression](us-006-configure-aura-progression.md)
|
- [US-006: Configure aura progression](us-006-configure-aura-progression.md)
|
||||||
- [US-007: Build and release the plugin](us-007-build-and-release-plugin.md)
|
- [US-007: Build and release the plugin](us-007-build-and-release-plugin.md)
|
||||||
|
- [US-008: Autocomplete commands](us-008-autocomplete-commands.md)
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
type: User Story
|
||||||
|
title: "US-008: Autocomplete commands"
|
||||||
|
description: Help players and administrators discover valid Creeper Aura command arguments with permission-aware tab completion.
|
||||||
|
status: done
|
||||||
|
---
|
||||||
|
|
||||||
|
# US-008: Autocomplete commands
|
||||||
|
|
||||||
|
As a **command sender**, I want Creeper Aura commands to offer relevant tab completions so that I can enter valid commands quickly and accurately.
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- [x] `/creeperaura` suggests only subcommands allowed by the sender's permissions.
|
||||||
|
- [x] `progress`, `set`, `add`, and `rank` suggest matching online player names where appropriate.
|
||||||
|
- [x] `rank` suggests `locked`, `I`, `II`, `III`, `IV`, `V`, and `VI`.
|
||||||
|
- [x] `threshold` suggests configurable ranks `I` through `VI`.
|
||||||
|
- [x] Suggestions are filtered case-insensitively by the current input.
|
||||||
|
- [x] Unauthorized administrative subcommands and arguments are not suggested.
|
||||||
|
- [x] Completion works for both players and the server console.
|
||||||
|
- [x] Automated tests cover completion behavior.
|
||||||
|
- [x] Command metadata and project documentation describe autocomplete support.
|
||||||
|
|
||||||
|
Offline stored players remain valid command targets but are not suggested, avoiding synchronous database access during completion.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [Check personal progress](us-004-check-personal-progress.md)
|
||||||
|
- [Administer player progression](us-005-administer-player-progression.md)
|
||||||
|
- [Configure aura progression](us-006-configure-aura-progression.md)
|
||||||
|
- [User-story catalog](index.md)
|
||||||
@@ -3,6 +3,7 @@ package games.dmg.creeperfear;
|
|||||||
import games.dmg.creeperfear.aura.AuraRules;
|
import games.dmg.creeperfear.aura.AuraRules;
|
||||||
import games.dmg.creeperfear.aura.CreeperAuraListener;
|
import games.dmg.creeperfear.aura.CreeperAuraListener;
|
||||||
import games.dmg.creeperfear.command.CreeperAuraCommand;
|
import games.dmg.creeperfear.command.CreeperAuraCommand;
|
||||||
|
import games.dmg.creeperfear.command.CreeperAuraTabCompleter;
|
||||||
import games.dmg.creeperfear.command.ProgressMessages;
|
import games.dmg.creeperfear.command.ProgressMessages;
|
||||||
import games.dmg.creeperfear.command.ProgressMutations;
|
import games.dmg.creeperfear.command.ProgressMutations;
|
||||||
import games.dmg.creeperfear.config.AuraConfigLoader;
|
import games.dmg.creeperfear.config.AuraConfigLoader;
|
||||||
@@ -17,6 +18,7 @@ import games.dmg.creeperfear.progress.SqliteProgressRepository;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
import org.bukkit.command.PluginCommand;
|
||||||
import org.bukkit.plugin.java.JavaPlugin;
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
public final class CreeperFearPlugin extends JavaPlugin {
|
public final class CreeperFearPlugin extends JavaPlugin {
|
||||||
@@ -42,14 +44,16 @@ public final class CreeperFearPlugin extends JavaPlugin {
|
|||||||
new PlayerSessionListener(progressService, getLogger()), this);
|
new PlayerSessionListener(progressService, getLogger()), this);
|
||||||
getServer().getPluginManager().registerEvents(
|
getServer().getPluginManager().registerEvents(
|
||||||
new CreeperAuraListener(progressService, auraRules), this);
|
new CreeperAuraListener(progressService, auraRules), this);
|
||||||
Objects.requireNonNull(getCommand("creeperaura"), "creeperaura command")
|
PluginCommand creeperAuraCommand = Objects.requireNonNull(
|
||||||
.setExecutor(new CreeperAuraCommand(
|
getCommand("creeperaura"), "creeperaura command");
|
||||||
|
creeperAuraCommand.setExecutor(new CreeperAuraCommand(
|
||||||
this,
|
this,
|
||||||
progressService,
|
progressService,
|
||||||
new ProgressMessages(auraRules),
|
new ProgressMessages(auraRules),
|
||||||
new ProgressMutations(),
|
new ProgressMutations(),
|
||||||
progressFeedback,
|
progressFeedback,
|
||||||
configurationManager));
|
configurationManager));
|
||||||
|
creeperAuraCommand.setTabCompleter(new CreeperAuraTabCompleter(getServer()));
|
||||||
getServer().getOnlinePlayers().forEach(player -> progressService.loadOnline(player.getUniqueId())
|
getServer().getOnlinePlayers().forEach(player -> progressService.loadOnline(player.getUniqueId())
|
||||||
.exceptionally(failure -> {
|
.exceptionally(failure -> {
|
||||||
getLogger().log(Level.SEVERE,
|
getLogger().log(Level.SEVERE,
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package games.dmg.creeperfear.command;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import org.bukkit.Server;
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.command.TabCompleter;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
public final class CreeperAuraTabCompleter implements TabCompleter {
|
||||||
|
private static final List<String> ALL_RANKS = List.of("locked", "I", "II", "III", "IV", "V", "VI");
|
||||||
|
private static final List<String> CONFIGURABLE_RANKS = ALL_RANKS.subList(1, ALL_RANKS.size());
|
||||||
|
|
||||||
|
private final Server server;
|
||||||
|
|
||||||
|
public CreeperAuraTabCompleter(Server server) {
|
||||||
|
this.server = server;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> onTabComplete(
|
||||||
|
@NotNull CommandSender sender,
|
||||||
|
@NotNull Command command,
|
||||||
|
@NotNull String label,
|
||||||
|
@NotNull String[] args) {
|
||||||
|
if (args.length == 3
|
||||||
|
&& args[0].equalsIgnoreCase("rank")
|
||||||
|
&& sender.hasPermission("creeperfear.admin.modify")) {
|
||||||
|
return matching(ALL_RANKS, args[2]);
|
||||||
|
}
|
||||||
|
if (args.length == 2
|
||||||
|
&& args[0].equalsIgnoreCase("threshold")
|
||||||
|
&& sender.hasPermission("creeperfear.admin.configure")) {
|
||||||
|
return matching(CONFIGURABLE_RANKS, args[1]);
|
||||||
|
}
|
||||||
|
if (args.length == 2 && completesPlayer(sender, args[0])) {
|
||||||
|
List<String> playerNames = server.getOnlinePlayers().stream()
|
||||||
|
.map(player -> player.getName())
|
||||||
|
.toList();
|
||||||
|
return matching(playerNames, args[1]);
|
||||||
|
}
|
||||||
|
if (args.length != 1) return List.of();
|
||||||
|
|
||||||
|
List<String> suggestions = new ArrayList<>();
|
||||||
|
if (sender.hasPermission("creeperfear.progress")
|
||||||
|
|| sender.hasPermission("creeperfear.admin.inspect")) {
|
||||||
|
suggestions.add("progress");
|
||||||
|
}
|
||||||
|
if (sender.hasPermission("creeperfear.admin.modify")) {
|
||||||
|
suggestions.add("set");
|
||||||
|
suggestions.add("add");
|
||||||
|
suggestions.add("rank");
|
||||||
|
}
|
||||||
|
if (sender.hasPermission("creeperfear.admin.configure")) {
|
||||||
|
suggestions.add("threshold");
|
||||||
|
suggestions.add("reload");
|
||||||
|
}
|
||||||
|
return matching(suggestions, args[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean completesPlayer(CommandSender sender, String subcommand) {
|
||||||
|
if (subcommand.equalsIgnoreCase("progress")) {
|
||||||
|
return sender.hasPermission("creeperfear.admin.inspect");
|
||||||
|
}
|
||||||
|
return (subcommand.equalsIgnoreCase("set")
|
||||||
|
|| subcommand.equalsIgnoreCase("add")
|
||||||
|
|| subcommand.equalsIgnoreCase("rank"))
|
||||||
|
&& sender.hasPermission("creeperfear.admin.modify");
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> matching(List<String> candidates, String input) {
|
||||||
|
String prefix = input.toLowerCase(Locale.ROOT);
|
||||||
|
return candidates.stream()
|
||||||
|
.filter(candidate -> candidate.toLowerCase(Locale.ROOT).startsWith(prefix))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ author: dmg.games
|
|||||||
description: Unlock Creeper Aura ranks by defeating creepers.
|
description: Unlock Creeper Aura ranks by defeating creepers.
|
||||||
commands:
|
commands:
|
||||||
creeperaura:
|
creeperaura:
|
||||||
description: Check and administer Creeper Aura progression.
|
description: Check and administer Creeper Aura progression with permission-aware tab completion.
|
||||||
usage: /<command> <progress [player]|set <player> <progress>|add <player> <progress>|rank <player> <rank>|threshold <rank> <points>|reload>
|
usage: /<command> <progress [player]|set <player> <progress>|add <player> <progress>|rank <player> <rank>|threshold <rank> <points>|reload>
|
||||||
permissions:
|
permissions:
|
||||||
creeperfear.progress:
|
creeperfear.progress:
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package games.dmg.creeperfear.command;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.bukkit.Server;
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.command.ConsoleCommandSender;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class CreeperAuraTabCompleterTest {
|
||||||
|
private final Server server = mock(Server.class);
|
||||||
|
private final Command command = mock(Command.class);
|
||||||
|
private final CommandSender sender = mock(CommandSender.class);
|
||||||
|
private final CreeperAuraTabCompleter completer = new CreeperAuraTabCompleter(server);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void suggestsOnlySubcommandsAllowedByTheSendersPermissions() {
|
||||||
|
when(sender.hasPermission("creeperfear.progress")).thenReturn(true);
|
||||||
|
when(sender.hasPermission("creeperfear.admin.modify")).thenReturn(true);
|
||||||
|
|
||||||
|
List<String> suggestions = completer.onTabComplete(sender, command, "creeperaura", new String[] {""});
|
||||||
|
|
||||||
|
assertEquals(List.of("progress", "set", "add", "rank"), suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void filtersSubcommandsCaseInsensitivelyByPartialInput() {
|
||||||
|
when(sender.hasPermission("creeperfear.admin.configure")).thenReturn(true);
|
||||||
|
|
||||||
|
List<String> suggestions = completer.onTabComplete(sender, command, "creeperaura", new String[] {"TH"});
|
||||||
|
|
||||||
|
assertEquals(List.of("threshold"), suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void suggestsMatchingOnlinePlayersForPermittedPlayerArguments() {
|
||||||
|
Player alice = mock(Player.class);
|
||||||
|
Player bob = mock(Player.class);
|
||||||
|
when(alice.getName()).thenReturn("Alice");
|
||||||
|
when(bob.getName()).thenReturn("Bob");
|
||||||
|
doReturn(List.of(alice, bob)).when(server).getOnlinePlayers();
|
||||||
|
when(sender.hasPermission("creeperfear.admin.modify")).thenReturn(true);
|
||||||
|
|
||||||
|
List<String> suggestions = completer.onTabComplete(
|
||||||
|
sender, command, "creeperaura", new String[] {"rank", "aL"});
|
||||||
|
|
||||||
|
assertEquals(List.of("Alice"), suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void suggestsAllRanksForTheRankCommand() {
|
||||||
|
when(sender.hasPermission("creeperfear.admin.modify")).thenReturn(true);
|
||||||
|
|
||||||
|
List<String> suggestions = completer.onTabComplete(
|
||||||
|
sender, command, "creeperaura", new String[] {"rank", "Alice", ""});
|
||||||
|
|
||||||
|
assertEquals(List.of("locked", "I", "II", "III", "IV", "V", "VI"), suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void suggestsOnlyConfigurableRanksForThresholds() {
|
||||||
|
when(sender.hasPermission("creeperfear.admin.configure")).thenReturn(true);
|
||||||
|
|
||||||
|
List<String> suggestions = completer.onTabComplete(
|
||||||
|
sender, command, "creeperaura", new String[] {"threshold", ""});
|
||||||
|
|
||||||
|
assertEquals(List.of("I", "II", "III", "IV", "V", "VI"), suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void doesNotSuggestAdministrativeArgumentsWithoutPermission() {
|
||||||
|
assertEquals(List.of(), completer.onTabComplete(
|
||||||
|
sender, command, "creeperaura", new String[] {"progress", ""}));
|
||||||
|
assertEquals(List.of(), completer.onTabComplete(
|
||||||
|
sender, command, "creeperaura", new String[] {"rank", "Alice", ""}));
|
||||||
|
assertEquals(List.of(), completer.onTabComplete(
|
||||||
|
sender, command, "creeperaura", new String[] {"threshold", ""}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void completesCommandsForPlayersAndTheServerConsole() {
|
||||||
|
Player player = mock(Player.class);
|
||||||
|
ConsoleCommandSender console = mock(ConsoleCommandSender.class);
|
||||||
|
when(player.hasPermission("creeperfear.progress")).thenReturn(true);
|
||||||
|
when(console.hasPermission("creeperfear.progress")).thenReturn(true);
|
||||||
|
|
||||||
|
assertEquals(List.of("progress"), completer.onTabComplete(
|
||||||
|
player, command, "creeperaura", new String[] {""}));
|
||||||
|
assertEquals(List.of("progress"), completer.onTabComplete(
|
||||||
|
console, command, "creeperaura", new String[] {""}));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user