feat(commands): add contextual tab completion
Release / release (push) Successful in 2m35s
CI / build (push) Successful in 1m21s

This commit is contained in:
dmg
2026-08-21 22:58:58 -04:00
parent 80810b3ba1
commit 7318d4afa6
11 changed files with 488 additions and 0 deletions
@@ -136,6 +136,14 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
settings.pendingSelectionTimeout(),
random
));
Objects.requireNonNull(getCommand("tyrant"), "Missing tyrant command metadata")
.setTabCompleter(new TyrantTabCompleter(stateManager, onlinePlayers));
Objects.requireNonNull(getCommand("vigilante"), "Missing vigilante metadata")
.setTabCompleter(new VigilanteTabCompleter(
stateManager, followers, onlinePlayers
));
Objects.requireNonNull(getCommand("tyrantadmin"), "Missing tyrantadmin metadata")
.setTabCompleter(new TyrantAdminTabCompleter(onlinePlayers));
getServer().getPluginManager().registerEvents(
new PlayerJoinListener(stateManager, clock, tyrantPresentation),
this
@@ -0,0 +1,19 @@
package games.dmg.spigottyrant;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
public final class TabSuggestions {
private TabSuggestions() {
}
public static List<String> matching(String prefix, Collection<String> candidates) {
String normalized = prefix == null ? "" : prefix.toLowerCase(Locale.ROOT);
return candidates.stream()
.filter(candidate -> candidate.toLowerCase(Locale.ROOT).startsWith(normalized))
.distinct()
.sorted(String.CASE_INSENSITIVE_ORDER)
.toList();
}
}
@@ -0,0 +1,60 @@
package games.dmg.spigottyrant;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
public final class TyrantAdminTabCompleter implements TabCompleter {
private static final String PERMISSION = "spigottyrant.admin";
private static final List<String> SUBCOMMANDS = List.of(
"status", "start", "pause", "resume", "reset"
);
private final OnlinePlayerDirectory onlinePlayers;
public TyrantAdminTabCompleter(OnlinePlayerDirectory onlinePlayers) {
this.onlinePlayers = onlinePlayers;
}
@Override
public List<String> onTabComplete(
CommandSender sender,
Command command,
String alias,
String[] arguments
) {
if (!sender.hasPermission(PERMISSION) || arguments.length == 0) {
return List.of();
}
if (arguments.length == 1) {
return TabSuggestions.matching(arguments[0], SUBCOMMANDS);
}
String subcommand = arguments[0].toLowerCase(java.util.Locale.ROOT);
if (arguments.length == 2) {
return switch (subcommand) {
case "start" -> TabSuggestions.matching(arguments[1], onlineNames(null));
case "reset" -> TabSuggestions.matching(arguments[1], List.of("confirm"));
default -> List.of();
};
}
if (arguments.length == 3 && subcommand.equals("start")) {
return TabSuggestions.matching(arguments[2], onlineNames(arguments[1]));
}
return List.of();
}
private List<String> onlineNames(String excludedName) {
List<String> names = new ArrayList<>();
for (UUID playerId : onlinePlayers.onlinePlayerIds()) {
Player player = onlinePlayers.findById(playerId);
if (player != null && (excludedName == null
|| !player.getName().equalsIgnoreCase(excludedName))) {
names.add(player.getName());
}
}
return List.copyOf(names);
}
}
@@ -0,0 +1,84 @@
package games.dmg.spigottyrant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
public final class TyrantTabCompleter implements TabCompleter {
private static final List<String> SUBCOMMANDS = List.of(
"menu", "status", "choices", "buy", "assign", "item", "intelligence",
"optout", "optin", "relinquish"
);
private static final List<String> UNLOCKS = java.util.Arrays.stream(TyrantUnlock.values())
.map(value -> value.name().toLowerCase(java.util.Locale.ROOT))
.toList();
private static final List<String> CLASSES = List.of("assassin", "fixer", "tamer");
private final TyrantStateManager stateManager;
private final OnlinePlayerDirectory onlinePlayers;
public TyrantTabCompleter(
TyrantStateManager stateManager,
OnlinePlayerDirectory onlinePlayers
) {
this.stateManager = stateManager;
this.onlinePlayers = onlinePlayers;
}
@Override
public List<String> onTabComplete(
CommandSender sender,
Command command,
String alias,
String[] arguments
) {
if (!(sender instanceof Player player) || arguments.length == 0) {
return List.of();
}
if (arguments.length == 1) {
return TabSuggestions.matching(arguments[0], SUBCOMMANDS);
}
String subcommand = arguments[0].toLowerCase(java.util.Locale.ROOT);
if (arguments.length == 2) {
return switch (subcommand) {
case "buy" -> TabSuggestions.matching(arguments[1], UNLOCKS);
case "assign" -> TabSuggestions.matching(arguments[1], CLASSES);
case "relinquish" -> TabSuggestions.matching(
arguments[1], List.of("confirm")
);
default -> List.of();
};
}
if (arguments.length == 3 && subcommand.equals("assign")) {
return TabSuggestions.matching(arguments[2], eligibleAssignees(player));
}
return List.of();
}
private List<String> eligibleAssignees(Player tyrant) {
Map<UUID, PlayerState> states = stateManager.players();
GameState game = stateManager.game();
Set<UUID> online = onlinePlayers.onlinePlayerIds();
List<String> names = new ArrayList<>();
for (UUID playerId : online) {
if (playerId.equals(tyrant.getUniqueId())
|| game != null && game.tyrantId().filter(playerId::equals).isPresent()) {
continue;
}
PlayerState state = states.get(playerId);
if (state != null && state.optedOutUntil().isPresent()) {
continue;
}
Player candidate = onlinePlayers.findById(playerId);
if (candidate != null) {
names.add(candidate.getName());
}
}
return List.copyOf(names);
}
}
@@ -0,0 +1,96 @@
package games.dmg.spigottyrant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
public final class VigilanteTabCompleter implements TabCompleter {
private static final List<String> SUBCOMMANDS = List.of(
"menu", "item", "invite", "accept", "dismiss", "leave"
);
private final TyrantStateManager stateManager;
private final FollowerService followers;
private final OnlinePlayerDirectory onlinePlayers;
public VigilanteTabCompleter(
TyrantStateManager stateManager,
FollowerService followers,
OnlinePlayerDirectory onlinePlayers
) {
this.stateManager = stateManager;
this.followers = followers;
this.onlinePlayers = onlinePlayers;
}
@Override
public List<String> onTabComplete(
CommandSender sender,
Command command,
String alias,
String[] arguments
) {
if (!(sender instanceof Player player) || arguments.length == 0) {
return List.of();
}
if (arguments.length == 1) {
return TabSuggestions.matching(arguments[0], SUBCOMMANDS);
}
if (arguments.length != 2) {
return List.of();
}
return switch (arguments[0].toLowerCase(java.util.Locale.ROOT)) {
case "invite" -> TabSuggestions.matching(
arguments[1], eligibleInvitees(player)
);
case "dismiss" -> TabSuggestions.matching(
arguments[1], currentFollowers(player)
);
default -> List.of();
};
}
private List<String> eligibleInvitees(Player vigilante) {
GameState game = stateManager.game();
Map<UUID, PlayerState> states = stateManager.players();
Set<UUID> pending = followers.invitedPlayerIds(vigilante.getUniqueId());
List<String> names = new ArrayList<>();
for (UUID playerId : onlinePlayers.onlinePlayerIds()) {
PlayerState state = states.get(playerId);
boolean assignedRole = game.tyrantId().filter(playerId::equals).isPresent()
|| game.vigilanteId().filter(playerId::equals).isPresent();
boolean ineligibleState = state != null
&& (state.optedOutUntil().isPresent() || state.followerOf().isPresent());
if (assignedRole || ineligibleState || pending.contains(playerId)) {
continue;
}
Player candidate = onlinePlayers.findById(playerId);
if (candidate != null) {
names.add(candidate.getName());
}
}
return List.copyOf(names);
}
private List<String> currentFollowers(Player vigilante) {
Map<UUID, PlayerState> states = stateManager.players();
List<String> names = new ArrayList<>();
for (UUID playerId : onlinePlayers.onlinePlayerIds()) {
PlayerState state = states.get(playerId);
if (state == null
|| state.followerOf().filter(vigilante.getUniqueId()::equals).isEmpty()) {
continue;
}
Player candidate = onlinePlayers.findById(playerId);
if (candidate != null) {
names.add(candidate.getName());
}
}
return List.copyOf(names);
}
}