Files
spigot-base/src/main/java/games/dmg/spigotbase/BaseAdminCommand.java
T
dmg 259ea9410c
Release / release (push) Successful in 2m16s
CI / build (push) Successful in 1m3s
feat(spawning): add spawnable block overlay
2026-08-10 22:46:02 -04:00

441 lines
19 KiB
Java

package games.dmg.spigotbase;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.TreeSet;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
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;
import org.bukkit.plugin.java.JavaPlugin;
final class BaseAdminCommand implements CommandExecutor, TabCompleter {
private static final List<String> SUBCOMMANDS = List.of(
"status", "setlevel", "setprogress", "setsetting", "clearcooldown", "reset", "config"
);
private static final List<String> PATHS = List.of(
"base", "size", "flight", "warmup", "cooldown"
);
private static final List<String> COUNTERS = List.of(
"grass_dirt", "stone", "deepslate", "obsidian", "placements", "total_placements",
"base_breaks"
);
private static final List<String> SETTINGS = List.of("spawnable");
private static final List<String> ENABLE_MODES = List.of("enable", "disable");
private static final List<String> COOLDOWN_TYPES = List.of("personal", "visitor", "all");
private static final List<String> RESET_PATHS = List.of(
"size", "flight", "warmup", "cooldown", "all"
);
private static final List<String> LEVELS_THREE = List.of("0", "1", "2", "3");
private static final List<String> LEVELS_FOUR = List.of("0", "1", "2", "3", "4");
private final JavaPlugin plugin;
private final BaseStateManager stateManager;
private final AdminProgressionService progressionService;
private final PluginSettingsProvider settingsProvider;
BaseAdminCommand(
JavaPlugin plugin,
BaseStateManager stateManager,
AdminProgressionService progressionService,
PluginSettingsProvider settingsProvider
) {
this.plugin = plugin;
this.stateManager = stateManager;
this.progressionService = progressionService;
this.settingsProvider = settingsProvider;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
if (!sender.hasPermission("spigotbase.admin")) {
sender.sendMessage(ChatColor.RED + "You do not have permission to administer Spigot Base.");
return true;
}
if (arguments.length > 0 && arguments[0].equalsIgnoreCase("config")) {
return updateConfig(sender, arguments);
}
if (arguments.length < 2) {
sendUsage(sender);
return true;
}
Optional<PlayerState> target = resolve(arguments[1]);
if (target.isEmpty()) {
sender.sendMessage(ChatColor.RED + "That player is not online or previously known.");
return true;
}
return switch (arguments[0].toLowerCase(Locale.ROOT)) {
case "status" -> showProgress(sender, target.orElseThrow(), arguments);
case "setlevel" -> setLevel(sender, target.orElseThrow(), arguments);
case "setprogress" -> setProgress(sender, target.orElseThrow(), arguments);
case "setsetting" -> setSetting(sender, target.orElseThrow(), arguments);
case "clearcooldown" -> clearCooldown(sender, target.orElseThrow(), arguments);
case "reset" -> reset(sender, target.orElseThrow(), arguments);
default -> {
sendUsage(sender);
yield true;
}
};
}
private boolean showProgress(
CommandSender sender,
PlayerState player,
String[] arguments
) {
if (arguments.length != 2) {
sender.sendMessage(ChatColor.RED + "Usage: /baseadmin status <player>");
return true;
}
sender.sendMessage(ChatColor.GOLD + "=== " + player.latestName() + " Base Progress ===");
sender.sendMessage(ChatColor.YELLOW + "Levels: base=" + player.baseLevel()
+ " size=" + player.sizeLevel() + " flight=" + player.flightLevel()
+ " warmup=" + player.warmupLevel() + " cooldown=" + player.cooldownLevel());
sender.sendMessage(ChatColor.GRAY + "Grass/dirt=" + player.grassAndDirtBroken()
+ " stone=" + player.stoneBroken() + " deepslate=" + player.deepslateBroken()
+ " obsidian=" + player.obsidianBroken());
int spawnableThreshold = settingsProvider.current()
.spawnableOverlayUnlockPlacements();
sender.sendMessage(ChatColor.GRAY + "In-base placements=" + player.blocksPlacedInBase()
+ " total placements=" + player.totalBlocksPlaced()
+ " breaks=" + player.blocksBrokenInBase());
sender.sendMessage(ChatColor.GRAY + "Spawnable overlay="
+ (player.totalBlocksPlaced() >= spawnableThreshold ? "unlocked" : "locked")
+ " (" + player.totalBlocksPlaced() + "/" + spawnableThreshold + ")");
sender.sendMessage(ChatColor.GRAY + "Toggles: navigation=" + player.navigationEnabled()
+ " flight=" + player.flightEnabled() + " border=" + player.borderEnabled()
+ " spawnable=" + player.spawnableOverlayEnabled() + " bossbar=" + player.bossBarEnabled() + " visitors=" + player.visitorsEnabled());
sender.sendMessage(ChatColor.GRAY + "Base: " + player.base()
.map(base -> base.worldName() + " " + base.x() + "," + base.y() + "," + base.z())
.orElse("not set"));
sender.sendMessage(ChatColor.GRAY + "Visitor cooldowns=" + player.visitorCooldownUntil().size());
return true;
}
private boolean setLevel(
CommandSender sender,
PlayerState target,
String[] arguments
) {
if (arguments.length != 4) {
sender.sendMessage(ChatColor.RED + "Usage: /baseadmin setlevel <player> <path> <level>");
return true;
}
final ProgressionPath path;
try {
path = ProgressionPath.valueOf(arguments[2].toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException exception) {
sender.sendMessage(ChatColor.RED
+ "Usage: /baseadmin setlevel <player> "
+ "<base|size|flight|warmup|cooldown> <level>");
return true;
}
try {
int level = Integer.parseInt(arguments[3]);
PlayerState updated = stateManager.update(
target.playerId(),
target.latestName(),
current -> progressionService.setLevel(current, path, level)
);
stateManager.saveIfDirty();
sender.sendMessage(ChatColor.GREEN + "Set " + updated.latestName() + "'s "
+ path.name().toLowerCase(Locale.ROOT) + " level to " + level + ".");
} catch (NumberFormatException exception) {
sender.sendMessage(ChatColor.RED + "The level must be an integer.");
} catch (IllegalArgumentException exception) {
sender.sendMessage(ChatColor.RED + exception.getMessage());
}
return true;
}
private boolean setProgress(
CommandSender sender,
PlayerState target,
String[] arguments
) {
if (arguments.length != 4) {
sender.sendMessage(ChatColor.RED
+ "Usage: /baseadmin setprogress <player> <counter> <amount>");
return true;
}
final ProgressCounter counter;
try {
counter = ProgressCounter.valueOf(
arguments[2].toUpperCase(Locale.ROOT).replace('-', '_')
);
} catch (IllegalArgumentException exception) {
sender.sendMessage(ChatColor.RED
+ "Usage: /baseadmin setprogress <player> "
+ "<grass_dirt|stone|deepslate|obsidian|placements|total_placements|base_breaks> "
+ "<amount>");
return true;
}
try {
long amount = Long.parseLong(arguments[3]);
PlayerState updated = stateManager.update(
target.playerId(),
target.latestName(),
current -> progressionService.setProgress(current, counter, amount)
);
stateManager.saveIfDirty();
sender.sendMessage(ChatColor.GREEN + "Set " + updated.latestName() + "'s "
+ counter.name().toLowerCase(Locale.ROOT) + " progress to " + amount + ".");
} catch (NumberFormatException exception) {
sender.sendMessage(ChatColor.RED + "The progress amount must be an integer.");
} catch (IllegalArgumentException exception) {
sender.sendMessage(ChatColor.RED + exception.getMessage());
}
return true;
}
private boolean setSetting(
CommandSender sender,
PlayerState target,
String[] arguments
) {
if (arguments.length != 4
|| !arguments[2].equalsIgnoreCase("spawnable")
|| !ENABLE_MODES.contains(arguments[3].toLowerCase(Locale.ROOT))) {
sender.sendMessage(ChatColor.RED + "Usage: /baseadmin setsetting <player> "
+ "spawnable <enable|disable>");
return true;
}
boolean enabled = arguments[3].equalsIgnoreCase("enable");
if (enabled && target.totalBlocksPlaced()
< settingsProvider.current().spawnableOverlayUnlockPlacements()) {
sender.sendMessage(ChatColor.RED + "That player has not unlocked the spawnable overlay.");
return true;
}
if (enabled && (target.baseLevel() < 1 || target.base().isEmpty())) {
sender.sendMessage(ChatColor.RED + "That player does not have an established base.");
return true;
}
PlayerState updated = stateManager.update(
target.playerId(),
target.latestName(),
current -> current.withSpawnableOverlayEnabled(enabled)
);
stateManager.saveIfDirty();
sender.sendMessage(ChatColor.GREEN + "Set " + updated.latestName()
+ "'s spawnable overlay to " + (enabled ? "enabled" : "disabled") + ".");
return true;
}
private boolean clearCooldown(
CommandSender sender,
PlayerState target,
String[] arguments
) {
if (arguments.length < 2 || arguments.length > 3) {
sender.sendMessage(ChatColor.RED
+ "Usage: /baseadmin clearcooldown <player> [personal|visitor|all]");
return true;
}
String selection = arguments.length == 3
? arguments[2].toLowerCase(Locale.ROOT)
: "all";
if (!Arrays.asList("personal", "visitor", "all").contains(selection)) {
sender.sendMessage(ChatColor.RED
+ "Usage: /baseadmin clearcooldown <player> [personal|visitor|all]");
return true;
}
stateManager.update(target.playerId(), target.latestName(), current -> {
PlayerState updated = current;
if (selection.equals("personal") || selection.equals("all")) {
updated = updated.withoutPersonalCooldown();
}
if (selection.equals("visitor") || selection.equals("all")) {
updated = updated.withoutVisitorCooldowns();
}
return updated;
});
stateManager.saveIfDirty();
sender.sendMessage(ChatColor.GREEN + "Cleared " + selection + " cooldowns for "
+ target.latestName() + ".");
return true;
}
private boolean reset(CommandSender sender, PlayerState target, String[] arguments) {
if (arguments.length < 3) {
sender.sendMessage(ChatColor.RED + "Usage: /baseadmin reset <player> <path|all> [confirm]");
return true;
}
if (arguments[2].equalsIgnoreCase("all")) {
if (arguments.length != 4 || !arguments[3].equalsIgnoreCase("confirm")) {
sender.sendMessage(ChatColor.RED + "Repeat with: /baseadmin reset "
+ target.latestName() + " all confirm");
return true;
}
stateManager.update(
target.playerId(),
target.latestName(),
ignored -> PlayerState.newPlayer(target.playerId(), target.latestName())
);
stateManager.saveIfDirty();
sender.sendMessage(ChatColor.GREEN + "Reset all progression for " + target.latestName() + ".");
return true;
}
if (arguments.length != 3) {
sender.sendMessage(ChatColor.RED
+ "Usage: /baseadmin reset <player> <size|flight|warmup|cooldown|all> [confirm]");
return true;
}
if (arguments[2].equalsIgnoreCase("base")) {
sender.sendMessage(ChatColor.RED
+ "Resetting base removes all progression. Use: /baseadmin reset "
+ target.latestName() + " all confirm");
return true;
}
final ProgressionPath path;
try {
path = ProgressionPath.valueOf(arguments[2].toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException exception) {
sender.sendMessage(ChatColor.RED
+ "Usage: /baseadmin reset <player> <size|flight|warmup|cooldown|all> [confirm]");
return true;
}
try {
stateManager.update(
target.playerId(),
target.latestName(),
current -> progressionService.resetPath(current, path)
);
stateManager.saveIfDirty();
sender.sendMessage(ChatColor.GREEN + "Reset " + path.name().toLowerCase(Locale.ROOT)
+ " progression for " + target.latestName() + ".");
} catch (IllegalArgumentException exception) {
sender.sendMessage(ChatColor.RED + exception.getMessage());
}
return true;
}
private boolean updateConfig(CommandSender sender, String[] arguments) {
if (arguments.length != 3) {
sender.sendMessage(ChatColor.RED + "Usage: /baseadmin config <key> <integer>");
return true;
}
String key = arguments[1];
Object previous = plugin.getConfig().get(key);
if (!(previous instanceof Number)) {
sender.sendMessage(ChatColor.RED + "Unknown numeric setting: " + key);
return true;
}
try {
long value = Long.parseLong(arguments[2]);
java.util.Map<String, Object> proposed =
new java.util.HashMap<>(plugin.getConfig().getValues(false));
proposed.put(key, value);
PluginSettings updated = PluginSettingsValidator.validateMaterials(
PluginSettings.from(proposed)
);
plugin.getConfig().set(key, value);
try {
plugin.saveConfig();
} catch (RuntimeException exception) {
plugin.getConfig().set(key, previous);
throw exception;
}
settingsProvider.update(updated);
sender.sendMessage(ChatColor.GREEN + "Updated " + key + " to " + value
+ "; the change is active immediately.");
} catch (NumberFormatException exception) {
sender.sendMessage(ChatColor.RED + "The setting value must be an integer.");
} catch (IllegalArgumentException exception) {
sender.sendMessage(ChatColor.RED + exception.getMessage());
}
return true;
}
@Override
public List<String> onTabComplete(
CommandSender sender,
Command command,
String alias,
String[] arguments
) {
if (!sender.hasPermission("spigotbase.admin")) {
return List.of();
}
if (arguments.length == 1) {
String prefix = arguments[0].toLowerCase(Locale.ROOT);
return SUBCOMMANDS.stream()
.filter(subcommand -> subcommand.startsWith(prefix))
.toList();
}
if (arguments.length == 2 && arguments[0].equalsIgnoreCase("config")) {
String prefix = arguments[1].toLowerCase(Locale.ROOT);
return plugin.getConfig().getValues(false).entrySet().stream()
.filter(entry -> entry.getValue() instanceof Number)
.map(java.util.Map.Entry::getKey)
.filter(key -> key.toLowerCase(Locale.ROOT).startsWith(prefix))
.sorted(String.CASE_INSENSITIVE_ORDER)
.toList();
}
if (arguments.length == 2) {
String prefix = arguments[1].toLowerCase(Locale.ROOT);
TreeSet<String> names = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
stateManager.knownPlayers().values().stream()
.map(PlayerState::latestName)
.forEach(names::add);
if (plugin.getServer() != null) {
plugin.getServer().getOnlinePlayers().stream()
.map(Player::getName)
.forEach(names::add);
}
return names.stream()
.filter(name -> name.toLowerCase(Locale.ROOT).startsWith(prefix))
.toList();
}
if (arguments.length == 3) {
List<String> options = switch (arguments[0].toLowerCase(Locale.ROOT)) {
case "setlevel" -> PATHS;
case "setprogress" -> COUNTERS;
case "setsetting" -> SETTINGS;
case "clearcooldown" -> COOLDOWN_TYPES;
case "reset" -> RESET_PATHS;
default -> List.of();
};
return complete(options, arguments[2]);
}
if (arguments.length == 4 && arguments[0].equalsIgnoreCase("setsetting")) {
return complete(ENABLE_MODES, arguments[3]);
}
if (arguments.length == 4 && arguments[0].equalsIgnoreCase("setlevel")) {
List<String> levels = switch (arguments[2].toLowerCase(Locale.ROOT)) {
case "base", "cooldown" -> LEVELS_FOUR;
case "size", "flight", "warmup" -> LEVELS_THREE;
default -> List.of();
};
return complete(levels, arguments[3]);
}
if (arguments.length == 4 && arguments[0].equalsIgnoreCase("reset")
&& arguments[2].equalsIgnoreCase("all")) {
return complete(List.of("confirm"), arguments[3]);
}
return List.of();
}
private static List<String> complete(List<String> options, String input) {
String prefix = input.toLowerCase(Locale.ROOT);
return options.stream().filter(option -> option.startsWith(prefix)).toList();
}
private Optional<PlayerState> resolve(String name) {
Player online = Bukkit.getPlayerExact(name);
if (online != null) {
return Optional.of(stateManager.player(online.getUniqueId(), online.getName()));
}
return stateManager.findByName(name);
}
private static void sendUsage(CommandSender sender) {
sender.sendMessage(ChatColor.YELLOW + "Usage: /baseadmin "
+ "<status|setlevel|setprogress|setsetting|clearcooldown|reset> <player> ...");
sender.sendMessage(ChatColor.YELLOW + " /baseadmin config <key> <integer>");
}
}