feat(commands): unify settings and admin commands
Release / release (push) Successful in 2m28s
CI / build (push) Successful in 1m3s

This commit is contained in:
dmg
2026-08-10 20:44:34 -04:00
parent 5c9dc5b361
commit 4278e08bf3
26 changed files with 1080 additions and 699 deletions
@@ -1,17 +1,35 @@
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 {
final class BaseAdminCommand implements CommandExecutor, TabCompleter {
private static final List<String> SUBCOMMANDS = List.of(
"status", "setlevel", "setprogress", "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", "base_breaks"
);
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;
@@ -48,7 +66,7 @@ final class BaseAdminCommand implements CommandExecutor {
return true;
}
return switch (arguments[0].toLowerCase(Locale.ROOT)) {
case "progress" -> showProgress(sender, target.orElseThrow());
case "status" -> showProgress(sender, target.orElseThrow(), arguments);
case "setlevel" -> setLevel(sender, target.orElseThrow(), arguments);
case "setprogress" -> setProgress(sender, target.orElseThrow(), arguments);
case "clearcooldown" -> clearCooldown(sender, target.orElseThrow(), arguments);
@@ -60,7 +78,15 @@ final class BaseAdminCommand implements CommandExecutor {
};
}
private boolean showProgress(CommandSender sender, PlayerState player) {
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()
@@ -89,8 +115,16 @@ final class BaseAdminCommand implements CommandExecutor {
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 {
ProgressionPath path = ProgressionPath.valueOf(arguments[2].toUpperCase(Locale.ROOT));
int level = Integer.parseInt(arguments[3]);
PlayerState updated = stateManager.update(
target.playerId(),
@@ -100,6 +134,8 @@ final class BaseAdminCommand implements CommandExecutor {
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());
}
@@ -116,10 +152,18 @@ final class BaseAdminCommand implements CommandExecutor {
+ "Usage: /baseadmin setprogress <player> <counter> <amount>");
return true;
}
final ProgressCounter counter;
try {
ProgressCounter counter = ProgressCounter.valueOf(
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|base_breaks> <amount>");
return true;
}
try {
long amount = Long.parseLong(arguments[3]);
PlayerState updated = stateManager.update(
target.playerId(),
@@ -142,7 +186,12 @@ final class BaseAdminCommand implements CommandExecutor {
PlayerState target,
String[] arguments
) {
String selection = arguments.length >= 3
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)) {
@@ -186,8 +235,26 @@ final class BaseAdminCommand implements CommandExecutor {
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 {
ProgressionPath path = ProgressionPath.valueOf(arguments[2].toUpperCase(Locale.ROOT));
stateManager.update(
target.playerId(),
target.latestName(),
@@ -239,6 +306,76 @@ final class BaseAdminCommand implements CommandExecutor {
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 "clearcooldown" -> COOLDOWN_TYPES;
case "reset" -> RESET_PATHS;
default -> List.of();
};
return complete(options, arguments[2]);
}
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) {
@@ -249,7 +386,7 @@ final class BaseAdminCommand implements CommandExecutor {
private static void sendUsage(CommandSender sender) {
sender.sendMessage(ChatColor.YELLOW + "Usage: /baseadmin "
+ "<progress|setlevel|setprogress|clearcooldown|reset> <player> ...");
+ "<status|setlevel|setprogress|clearcooldown|reset> <player> ...");
sender.sendMessage(ChatColor.YELLOW + " /baseadmin config <key> <integer>");
}
}
@@ -1,31 +1,16 @@
package games.dmg.spigotbase;
import java.io.IOException;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
final class BaseCommand implements CommandExecutor {
private final BaseTeleportManager teleportManager;
private final BaseStateManager stateManager;
private final VisitorPolicy visitorPolicy;
private final PluginSettingsProvider settings;
BaseCommand(
BaseTeleportManager teleportManager,
BaseStateManager stateManager,
VisitorPolicy visitorPolicy,
PluginSettingsProvider settings
) {
BaseCommand(BaseTeleportManager teleportManager) {
this.teleportManager = teleportManager;
this.stateManager = stateManager;
this.visitorPolicy = visitorPolicy;
this.settings = settings;
}
@Override
@@ -38,95 +23,7 @@ final class BaseCommand implements CommandExecutor {
teleportManager.start(player);
return true;
}
if (arguments.length == 1 && arguments[0].equalsIgnoreCase("upgrade")) {
purchaseVisitorAccess(player);
return true;
}
player.sendMessage(ChatColor.RED + "Usage: /base [upgrade]");
player.sendMessage(ChatColor.RED + "Usage: /base");
return true;
}
private void purchaseVisitorAccess(Player player) {
PlayerState state = stateManager.player(player.getUniqueId(), player.getName());
if (!visitorPolicy.canPurchase(state)) {
player.sendMessage(ChatColor.RED + (state.baseLevel() >= 4
? "Base IV is already unlocked."
: "You must unlock Base III before purchasing Base IV."));
return;
}
int price = settings.current().visitorUnlockDiamondCost();
Material currency = Material.valueOf(settings.current().visitorCurrencyMaterial());
PlayerInventory inventory = player.getInventory();
if (countCurrency(inventory, currency) < price) {
player.sendMessage(ChatColor.RED + "Base IV costs " + price + " "
+ currency.name().toLowerCase(java.util.Locale.ROOT) + ".");
return;
}
ItemStack[] snapshot = cloneContents(inventory.getStorageContents());
removeCurrency(inventory, currency, price);
try {
stateManager.updateAndSave(
player.getUniqueId(),
player.getName(),
current -> current.withBaseLevel(4).withVisitorsEnabled(true)
);
} catch (IOException | RuntimeException exception) {
inventory.setStorageContents(snapshot);
player.sendMessage(ChatColor.RED + "The upgrade could not be saved; your diamonds were restored.");
return;
}
player.sendTitle(
ChatColor.GOLD + "Base IV Unlocked",
ChatColor.YELLOW + "Visitors may now teleport to your base",
settings.current().titleFadeInTicks(),
settings.current().titleStayTicks(),
settings.current().titleFadeOutTicks()
);
player.sendMessage(ChatColor.GREEN + "Base IV unlocked for " + price + " "
+ currency.name().toLowerCase(java.util.Locale.ROOT) + ".");
}
private static int countCurrency(PlayerInventory inventory, Material currency) {
int count = 0;
for (ItemStack item : inventory.getStorageContents()) {
if (item != null && item.getType() == currency) {
count += item.getAmount();
}
}
return count;
}
private static void removeCurrency(
PlayerInventory inventory,
Material currency,
int amount
) {
ItemStack[] contents = inventory.getStorageContents();
int remaining = amount;
for (int index = 0; index < contents.length && remaining > 0; index++) {
ItemStack item = contents[index];
if (item == null || item.getType() != currency) {
continue;
}
int removed = Math.min(remaining, item.getAmount());
remaining -= removed;
int newAmount = item.getAmount() - removed;
if (newAmount == 0) {
contents[index] = null;
} else {
ItemStack reduced = item.clone();
reduced.setAmount(newAmount);
contents[index] = reduced;
}
}
inventory.setStorageContents(contents);
}
private static ItemStack[] cloneContents(ItemStack[] contents) {
ItemStack[] copy = new ItemStack[contents.length];
for (int index = 0; index < contents.length; index++) {
copy[index] = contents[index] == null ? null : contents[index].clone();
}
return copy;
}
}
@@ -1,68 +0,0 @@
package games.dmg.spigotbase;
import java.util.List;
import java.util.Locale;
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;
final class BaseFlightCommand implements CommandExecutor, TabCompleter {
private static final List<String> MODES = List.of("on", "off");
private final BaseStateManager stateManager;
private final BaseFlightController controller;
BaseFlightCommand(BaseStateManager stateManager, BaseFlightController controller) {
this.stateManager = stateManager;
this.controller = controller;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
if (!(sender instanceof Player player)) {
sender.sendMessage("Only players can use base flight.");
return true;
}
PlayerState state = stateManager.player(player.getUniqueId(), player.getName());
if (state.flightLevel() < 1) {
player.sendMessage(ChatColor.RED + "Base flight is still locked.");
return true;
}
final boolean enabled;
try {
enabled = TogglePreference.resolve(state.flightEnabled(), arguments);
} catch (IllegalArgumentException exception) {
player.sendMessage(ChatColor.RED + "Usage: /baseflight [on|off]");
return true;
}
state = stateManager.update(
player.getUniqueId(),
player.getName(),
current -> current.withFlightEnabled(enabled)
);
if (!state.flightEnabled()) {
controller.removeGrantedFlight(player);
}
stateManager.saveIfDirty();
player.sendMessage(ChatColor.YELLOW + "Base flight is now "
+ (state.flightEnabled() ? ChatColor.GREEN + "on" : ChatColor.RED + "off")
+ ChatColor.YELLOW + ".");
return true;
}
@Override
public List<String> onTabComplete(
CommandSender sender,
Command command,
String alias,
String[] arguments
) {
if (arguments.length != 1) {
return List.of();
}
String prefix = arguments[0].toLowerCase(Locale.ROOT);
return MODES.stream().filter(mode -> mode.startsWith(prefix)).toList();
}
}
@@ -80,7 +80,7 @@ final class BaseFlightController implements Runnable {
stateManager.saveIfDirty();
player.sendTitle(
ChatColor.GOLD + "Base Flight " + roman(updated.flightLevel()) + " Unlocked",
ChatColor.YELLOW + "Use /baseflight to toggle flight",
ChatColor.YELLOW + "Use /basesettings flight enable to enable flight",
settings.current().titleFadeInTicks(),
settings.current().titleStayTicks(),
settings.current().titleFadeOutTicks()
@@ -1,73 +0,0 @@
package games.dmg.spigotbase;
import java.util.List;
import java.util.Locale;
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;
final class BaseNavigationCommand implements CommandExecutor, TabCompleter {
private static final List<String> MODES = List.of("on", "off");
private final BaseStateManager stateManager;
BaseNavigationCommand(BaseStateManager stateManager) {
this.stateManager = stateManager;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
if (!(sender instanceof Player player)) {
sender.sendMessage("Only players can use base navigation.");
return true;
}
PlayerState state = stateManager.player(player.getUniqueId(), player.getName());
if (state.baseLevel() < 2) {
player.sendMessage(ChatColor.RED + "Base II navigation is still locked.");
return true;
}
if (state.base().isEmpty()) {
player.sendMessage(ChatColor.RED + "Set your base before enabling navigation.");
return true;
}
final boolean enabled;
try {
enabled = TogglePreference.resolve(state.navigationEnabled(), arguments);
} catch (IllegalArgumentException exception) {
player.sendMessage(ChatColor.RED + "Usage: /basenavigation [on|off]");
return true;
}
state = stateManager.update(
player.getUniqueId(),
player.getName(),
current -> current.withNavigationEnabled(enabled)
);
stateManager.saveIfDirty();
player.sendMessage(ChatColor.YELLOW + "Base navigation is now "
+ (state.navigationEnabled() ? ChatColor.GREEN + "on" : ChatColor.RED + "off")
+ ChatColor.YELLOW + ".");
if (state.navigationEnabled()
&& !player.getWorld().getUID().equals(state.base().orElseThrow().worldId())) {
player.sendMessage(ChatColor.RED + "Your base is in another world: "
+ state.base().orElseThrow().worldName() + ".");
}
return true;
}
@Override
public List<String> onTabComplete(
CommandSender sender,
Command command,
String alias,
String[] arguments
) {
if (arguments.length != 1) {
return List.of();
}
String prefix = arguments[0].toLowerCase(Locale.ROOT);
return MODES.stream().filter(mode -> mode.startsWith(prefix)).toList();
}
}
@@ -1,121 +0,0 @@
package games.dmg.spigotbase;
import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
final class BaseProgressCommand implements CommandExecutor {
private final BaseStateManager stateManager;
private final PluginSettingsProvider settings;
private final TeleportPolicy teleportPolicy;
BaseProgressCommand(BaseStateManager stateManager, PluginSettingsProvider settings) {
this.stateManager = stateManager;
this.settings = settings;
this.teleportPolicy = new TeleportPolicy(settings);
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
if (!(sender instanceof Player player)) {
sender.sendMessage("Only players have base progression.");
return true;
}
PlayerState state = stateManager.player(player.getUniqueId(), player.getName());
if (arguments.length == 1 && arguments[0].equalsIgnoreCase("bossbar")) {
state = stateManager.update(
player.getUniqueId(),
player.getName(),
current -> current.withBossBarEnabled(!current.bossBarEnabled())
);
stateManager.saveIfDirty();
player.sendMessage(ChatColor.YELLOW + "Automatic progress boss bars are now "
+ (state.bossBarEnabled() ? ChatColor.GREEN + "on" : ChatColor.RED + "off")
+ ChatColor.YELLOW + ".");
return true;
}
player.sendMessage(ChatColor.GOLD + "=== Base Progress ===");
showBasePath(player, state);
showSizePath(player, state);
showFlightPath(player, state);
showWarmupPath(player, state);
showCooldownPath(player, state);
player.sendMessage(ChatColor.GRAY + "Boss bars: " + (state.bossBarEnabled() ? "on" : "off"));
state.base().ifPresentOrElse(
base -> player.sendMessage(ChatColor.GRAY + "Base: " + base.worldName() + " "
+ base.x() + ", " + base.y() + ", " + base.z()),
() -> player.sendMessage(ChatColor.GRAY + "Base: not set")
);
return true;
}
private void showBasePath(Player player, PlayerState state) {
String detail = switch (state.baseLevel()) {
case 0 -> state.grassAndDirtBroken() + "/" + settings.current().baseUnlockBlocks()
+ " grass or dirt → /setbase";
case 1 -> state.grassAndDirtBroken() + "/" + settings.current().navigationUnlockBlocks()
+ " grass or dirt → navigation";
case 2 -> state.blocksPlacedInBase() + "/" + settings.current().teleportUnlockPlacements()
+ " placements → /base";
case 3 -> settings.current().visitorUnlockDiamondCost() + " diamonds → visitor access";
case 4 -> "complete; visitor access unlocked";
default -> "invalid";
};
player.sendMessage(ChatColor.YELLOW + "Base " + state.baseLevel() + "/4: "
+ ChatColor.GRAY + detail);
}
private void showSizePath(Player player, PlayerState state) {
String detail = switch (state.sizeLevel()) {
case 0 -> state.stoneBroken() + "/" + settings.current().stoneExpansionBlocks() + " stone";
case 1 -> state.deepslateBroken() + "/" + settings.current().deepslateExpansionBlocks() + " deepslate";
case 2 -> state.obsidianBroken() + "/" + settings.current().obsidianExpansionBlocks() + " obsidian";
case 3 -> "complete; 150-block radius by default";
default -> "invalid";
};
player.sendMessage(colorForPrerequisite(state.baseLevel() >= 1) + "Base Size "
+ state.sizeLevel() + "/3: " + ChatColor.GRAY + detail);
}
private void showFlightPath(Player player, PlayerState state) {
String detail = state.flightLevel() >= 3
? "complete; world build height"
: (state.flightLevel() + 1) + " simultaneous elytra required";
player.sendMessage(colorForPrerequisite(state.baseLevel() >= 1) + "Base Flight "
+ state.flightLevel() + "/3: " + ChatColor.GRAY + detail
+ "; toggle=" + (state.flightEnabled() ? "on" : "off"));
}
private void showWarmupPath(Player player, PlayerState state) {
String detail = switch (state.warmupLevel()) {
case 0 -> state.blocksPlacedInBase() + "/" + settings.current().secondWarmupPlacements();
case 1 -> state.blocksPlacedInBase() + "/" + settings.current().thirdWarmupPlacements();
case 2 -> state.blocksPlacedInBase() + "/" + settings.current().instantWarmupPlacements();
case 3 -> "complete";
default -> "invalid";
};
player.sendMessage(colorForPrerequisite(state.baseLevel() >= 3) + "Teleport Warm-up "
+ state.warmupLevel() + "/3: " + ChatColor.GRAY
+ DurationFormatter.friendly(teleportPolicy.warmup(state)) + "; " + detail);
}
private void showCooldownPath(Player player, PlayerState state) {
String detail = switch (state.cooldownLevel()) {
case 0 -> state.blocksBrokenInBase() + "/" + settings.current().firstCooldownBreaks();
case 1 -> state.blocksBrokenInBase() + "/" + settings.current().secondCooldownBreaks();
case 2 -> state.blocksBrokenInBase() + "/" + settings.current().thirdCooldownBreaks();
case 3 -> state.blocksBrokenInBase() + "/" + settings.current().instantCooldownBreaks();
case 4 -> "complete";
default -> "invalid";
};
player.sendMessage(colorForPrerequisite(state.baseLevel() >= 3) + "Teleport Cooldown "
+ state.cooldownLevel() + "/4: " + ChatColor.GRAY
+ DurationFormatter.friendly(teleportPolicy.cooldown(state)) + "; " + detail);
}
private static ChatColor colorForPrerequisite(boolean met) {
return met ? ChatColor.YELLOW : ChatColor.RED;
}
}
@@ -149,7 +149,7 @@ final class BaseProgressListener implements Listener {
int level = update.player().baseLevel();
String subtitle = switch (level) {
case 1 -> "/setbase is now available";
case 2 -> "/basenavigation is now available";
case 2 -> "/basesettings navigation enable is now available";
case 3 -> "/base is now available";
case 4 -> "Visitors can now travel to your base";
default -> "A new base benefit is available";
@@ -0,0 +1,377 @@
package games.dmg.spigotbase;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import org.bukkit.ChatColor;
import org.bukkit.Material;
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.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
private static final List<String> SETTINGS = List.of(
"status", "upgrade", "visitors", "navigation", "flight", "bossbar"
);
private static final List<String> VISITOR_MODES = List.of("allowed", "blocked");
private static final List<String> ENABLE_MODES = List.of("enable", "disable");
private final BaseStateManager stateManager;
private final PluginSettingsProvider settings;
private final TeleportPolicy teleportPolicy;
private final BaseFlightController flightController;
BaseSettingsCommand(
BaseStateManager stateManager,
PluginSettingsProvider settings,
BaseFlightController flightController
) {
this.stateManager = stateManager;
this.settings = settings;
this.teleportPolicy = new TeleportPolicy(settings);
this.flightController = flightController;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
if (!(sender instanceof Player player)) {
sender.sendMessage("Only players have base progression.");
return true;
}
PlayerState state = stateManager.player(player.getUniqueId(), player.getName());
if (arguments.length == 0
|| arguments.length == 1 && arguments[0].equalsIgnoreCase("status")) {
showStatus(player, state);
return true;
}
if (arguments.length == 1 && arguments[0].equalsIgnoreCase("upgrade")) {
return purchaseVisitorAccess(player, state);
}
if (arguments.length == 2 && arguments[0].equalsIgnoreCase("visitors")) {
return updateVisitors(player, state, arguments[1]);
}
if (arguments.length == 2 && arguments[0].equalsIgnoreCase("navigation")) {
return updateNavigation(player, state, arguments[1]);
}
if (arguments.length == 2 && arguments[0].equalsIgnoreCase("flight")) {
return updateFlight(player, state, arguments[1]);
}
if (arguments.length == 2 && arguments[0].equalsIgnoreCase("bossbar")) {
return updateBossBar(player, arguments[1]);
}
sendUsage(player);
return true;
}
private boolean purchaseVisitorAccess(Player player, PlayerState state) {
if (!new VisitorPolicy().canPurchase(state)) {
player.sendMessage(ChatColor.RED + (state.baseLevel() >= 4
? "Base IV is already unlocked."
: "You must unlock Base III before purchasing Base IV."));
return true;
}
int price = settings.current().visitorUnlockDiamondCost();
Material currency = Material.valueOf(settings.current().visitorCurrencyMaterial());
PlayerInventory inventory = player.getInventory();
if (countCurrency(inventory, currency) < price) {
player.sendMessage(ChatColor.RED + "Base IV costs " + price + " "
+ currency.name().toLowerCase(java.util.Locale.ROOT) + ".");
return true;
}
ItemStack[] snapshot = cloneContents(inventory.getStorageContents());
removeCurrency(inventory, currency, price);
try {
stateManager.updateAndSave(
player.getUniqueId(),
player.getName(),
current -> current.withBaseLevel(4).withVisitorsEnabled(true)
);
} catch (IOException | RuntimeException exception) {
inventory.setStorageContents(snapshot);
player.sendMessage(ChatColor.RED
+ "The upgrade could not be saved; your diamonds were restored.");
return true;
}
player.sendTitle(
ChatColor.GOLD + "Base IV Unlocked",
ChatColor.YELLOW + "Visitors may now teleport to your base",
settings.current().titleFadeInTicks(),
settings.current().titleStayTicks(),
settings.current().titleFadeOutTicks()
);
player.sendMessage(ChatColor.GREEN + "Base IV unlocked for " + price + " "
+ currency.name().toLowerCase(java.util.Locale.ROOT) + ".");
return true;
}
private boolean updateVisitors(Player player, PlayerState state, String mode) {
if (state.baseLevel() < 4) {
player.sendMessage(ChatColor.RED + "Base IV visitor access is still locked.");
return true;
}
final boolean enabled;
if (mode.equalsIgnoreCase("allowed")) {
enabled = true;
} else if (mode.equalsIgnoreCase("blocked")) {
enabled = false;
} else {
sendUsage(player);
return true;
}
state = stateManager.update(
player.getUniqueId(),
player.getName(),
current -> current.withVisitorsEnabled(enabled)
);
stateManager.saveIfDirty();
player.sendMessage(ChatColor.YELLOW + "Visitor teleports are now "
+ (state.visitorsEnabled() ? ChatColor.GREEN + "allowed" : ChatColor.RED + "blocked")
+ ChatColor.YELLOW + ".");
return true;
}
private boolean updateNavigation(Player player, PlayerState state, String mode) {
if (state.baseLevel() < 2) {
player.sendMessage(ChatColor.RED + "Base II navigation is still locked.");
return true;
}
if (state.base().isEmpty()) {
player.sendMessage(ChatColor.RED + "Set your base before enabling navigation.");
return true;
}
Boolean enabled = enabledMode(mode);
if (enabled == null) {
sendUsage(player);
return true;
}
state = stateManager.update(
player.getUniqueId(),
player.getName(),
current -> current.withNavigationEnabled(enabled)
);
stateManager.saveIfDirty();
player.sendMessage(ChatColor.YELLOW + "Base navigation is now "
+ (state.navigationEnabled() ? ChatColor.GREEN + "enabled" : ChatColor.RED + "disabled")
+ ChatColor.YELLOW + ".");
if (state.navigationEnabled()
&& !player.getWorld().getUID().equals(state.base().orElseThrow().worldId())) {
player.sendMessage(ChatColor.RED + "Your base is in another world: "
+ state.base().orElseThrow().worldName() + ".");
}
return true;
}
private boolean updateFlight(Player player, PlayerState state, String mode) {
if (state.flightLevel() < 1) {
player.sendMessage(ChatColor.RED + "Base flight is still locked.");
return true;
}
Boolean enabled = enabledMode(mode);
if (enabled == null) {
sendUsage(player);
return true;
}
state = stateManager.update(
player.getUniqueId(),
player.getName(),
current -> current.withFlightEnabled(enabled)
);
if (!state.flightEnabled()) {
flightController.removeGrantedFlight(player);
}
stateManager.saveIfDirty();
player.sendMessage(ChatColor.YELLOW + "Base flight is now "
+ (state.flightEnabled() ? ChatColor.GREEN + "enabled" : ChatColor.RED + "disabled")
+ ChatColor.YELLOW + ".");
return true;
}
private boolean updateBossBar(Player player, String mode) {
Boolean enabled = enabledMode(mode);
if (enabled == null) {
sendUsage(player);
return true;
}
PlayerState state = stateManager.update(
player.getUniqueId(),
player.getName(),
current -> current.withBossBarEnabled(enabled)
);
stateManager.saveIfDirty();
player.sendMessage(ChatColor.YELLOW + "Automatic progress boss bars are now "
+ (state.bossBarEnabled() ? ChatColor.GREEN + "enabled" : ChatColor.RED + "disabled")
+ ChatColor.YELLOW + ".");
return true;
}
private void showStatus(Player player, PlayerState state) {
player.sendMessage(ChatColor.GOLD + "=== Base Progress ===");
showBasePath(player, state);
showSizePath(player, state);
showFlightPath(player, state);
showWarmupPath(player, state);
showCooldownPath(player, state);
player.sendMessage(ChatColor.GRAY + "Settings: visitors="
+ (state.visitorsEnabled() ? "allowed" : "blocked")
+ " navigation=" + (state.navigationEnabled() ? "enabled" : "disabled")
+ " flight=" + (state.flightEnabled() ? "enabled" : "disabled")
+ " bossbar=" + (state.bossBarEnabled() ? "enabled" : "disabled"));
state.base().ifPresentOrElse(
base -> player.sendMessage(ChatColor.GRAY + "Base: " + base.worldName() + " "
+ base.x() + ", " + base.y() + ", " + base.z()),
() -> player.sendMessage(ChatColor.GRAY + "Base: not set")
);
}
private void showBasePath(Player player, PlayerState state) {
String detail = switch (state.baseLevel()) {
case 0 -> state.grassAndDirtBroken() + "/" + settings.current().baseUnlockBlocks()
+ " grass or dirt → /setbase";
case 1 -> state.grassAndDirtBroken() + "/" + settings.current().navigationUnlockBlocks()
+ " grass or dirt → navigation";
case 2 -> state.blocksPlacedInBase() + "/" + settings.current().teleportUnlockPlacements()
+ " placements → /base";
case 3 -> settings.current().visitorUnlockDiamondCost()
+ " diamonds → /basesettings upgrade";
case 4 -> "complete; visitor access unlocked";
default -> "invalid";
};
player.sendMessage(ChatColor.YELLOW + "Base " + state.baseLevel() + "/4: "
+ ChatColor.GRAY + detail);
}
private void showSizePath(Player player, PlayerState state) {
String detail = switch (state.sizeLevel()) {
case 0 -> state.stoneBroken() + "/" + settings.current().stoneExpansionBlocks() + " stone";
case 1 -> state.deepslateBroken() + "/" + settings.current().deepslateExpansionBlocks() + " deepslate";
case 2 -> state.obsidianBroken() + "/" + settings.current().obsidianExpansionBlocks() + " obsidian";
case 3 -> "complete; 150-block radius by default";
default -> "invalid";
};
player.sendMessage(colorForPrerequisite(state.baseLevel() >= 1) + "Base Size "
+ state.sizeLevel() + "/3: " + ChatColor.GRAY + detail);
}
private void showFlightPath(Player player, PlayerState state) {
String detail = state.flightLevel() >= 3
? "complete; world build height"
: (state.flightLevel() + 1) + " simultaneous elytra required";
player.sendMessage(colorForPrerequisite(state.baseLevel() >= 1) + "Base Flight "
+ state.flightLevel() + "/3: " + ChatColor.GRAY + detail
+ "; setting=" + (state.flightEnabled() ? "enabled" : "disabled"));
}
private void showWarmupPath(Player player, PlayerState state) {
String detail = switch (state.warmupLevel()) {
case 0 -> state.blocksPlacedInBase() + "/" + settings.current().secondWarmupPlacements();
case 1 -> state.blocksPlacedInBase() + "/" + settings.current().thirdWarmupPlacements();
case 2 -> state.blocksPlacedInBase() + "/" + settings.current().instantWarmupPlacements();
case 3 -> "complete";
default -> "invalid";
};
player.sendMessage(colorForPrerequisite(state.baseLevel() >= 3) + "Teleport Warm-up "
+ state.warmupLevel() + "/3: " + ChatColor.GRAY
+ DurationFormatter.friendly(teleportPolicy.warmup(state)) + "; " + detail);
}
private void showCooldownPath(Player player, PlayerState state) {
String detail = switch (state.cooldownLevel()) {
case 0 -> state.blocksBrokenInBase() + "/" + settings.current().firstCooldownBreaks();
case 1 -> state.blocksBrokenInBase() + "/" + settings.current().secondCooldownBreaks();
case 2 -> state.blocksBrokenInBase() + "/" + settings.current().thirdCooldownBreaks();
case 3 -> state.blocksBrokenInBase() + "/" + settings.current().instantCooldownBreaks();
case 4 -> "complete";
default -> "invalid";
};
player.sendMessage(colorForPrerequisite(state.baseLevel() >= 3) + "Teleport Cooldown "
+ state.cooldownLevel() + "/4: " + ChatColor.GRAY
+ DurationFormatter.friendly(teleportPolicy.cooldown(state)) + "; " + detail);
}
@Override
public List<String> onTabComplete(
CommandSender sender,
Command command,
String alias,
String[] arguments
) {
if (arguments.length == 1) {
String prefix = arguments[0].toLowerCase(Locale.ROOT);
return SETTINGS.stream().filter(setting -> setting.startsWith(prefix)).toList();
}
if (arguments.length == 2) {
List<String> modes = arguments[0].equalsIgnoreCase("visitors")
? VISITOR_MODES
: switch (arguments[0].toLowerCase(Locale.ROOT)) {
case "navigation", "flight", "bossbar" -> ENABLE_MODES;
default -> List.of();
};
String prefix = arguments[1].toLowerCase(Locale.ROOT);
return modes.stream().filter(mode -> mode.startsWith(prefix)).toList();
}
return List.of();
}
private static int countCurrency(PlayerInventory inventory, Material currency) {
int count = 0;
for (ItemStack item : inventory.getStorageContents()) {
if (item != null && item.getType() == currency) {
count += item.getAmount();
}
}
return count;
}
private static void removeCurrency(PlayerInventory inventory, Material currency, int amount) {
ItemStack[] contents = inventory.getStorageContents();
int remaining = amount;
for (int index = 0; index < contents.length && remaining > 0; index++) {
ItemStack item = contents[index];
if (item == null || item.getType() != currency) {
continue;
}
int removed = Math.min(remaining, item.getAmount());
remaining -= removed;
int newAmount = item.getAmount() - removed;
if (newAmount == 0) {
contents[index] = null;
} else {
ItemStack reduced = item.clone();
reduced.setAmount(newAmount);
contents[index] = reduced;
}
}
inventory.setStorageContents(contents);
}
private static ItemStack[] cloneContents(ItemStack[] contents) {
ItemStack[] copy = new ItemStack[contents.length];
for (int index = 0; index < contents.length; index++) {
copy[index] = contents[index] == null ? null : contents[index].clone();
}
return copy;
}
private static Boolean enabledMode(String mode) {
if (mode.equalsIgnoreCase("enable")) {
return true;
}
if (mode.equalsIgnoreCase("disable")) {
return false;
}
return null;
}
private static ChatColor colorForPrerequisite(boolean met) {
return met ? ChatColor.YELLOW : ChatColor.RED;
}
private static void sendUsage(Player player) {
player.sendMessage(ChatColor.RED + "Usage: /basesettings "
+ "[status|upgrade|visitors <allowed|blocked>|navigation <enable|disable>"
+ "|flight <enable|disable>|bossbar <enable|disable>]");
}
}
@@ -1,63 +0,0 @@
package games.dmg.spigotbase;
import java.util.List;
import java.util.Locale;
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;
final class BaseVisitorsCommand implements CommandExecutor, TabCompleter {
private static final List<String> MODES = List.of("on", "off");
private final BaseStateManager stateManager;
BaseVisitorsCommand(BaseStateManager stateManager) {
this.stateManager = stateManager;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
if (!(sender instanceof Player player)) {
sender.sendMessage("Only players can manage base visitors.");
return true;
}
PlayerState state = stateManager.player(player.getUniqueId(), player.getName());
if (state.baseLevel() < 4) {
player.sendMessage(ChatColor.RED + "Base IV visitor access is still locked.");
return true;
}
final boolean enabled;
try {
enabled = TogglePreference.resolve(state.visitorsEnabled(), arguments);
} catch (IllegalArgumentException exception) {
player.sendMessage(ChatColor.RED + "Usage: /basevisitors [on|off]");
return true;
}
state = stateManager.update(
player.getUniqueId(),
player.getName(),
current -> current.withVisitorsEnabled(enabled)
);
stateManager.saveIfDirty();
player.sendMessage(ChatColor.YELLOW + "Visitor teleports are now "
+ (state.visitorsEnabled() ? ChatColor.GREEN + "on" : ChatColor.RED + "off")
+ ChatColor.YELLOW + ".");
return true;
}
@Override
public List<String> onTabComplete(
CommandSender sender,
Command command,
String alias,
String[] arguments
) {
if (arguments.length != 1) {
return List.of();
}
String prefix = arguments[0].toLowerCase(Locale.ROOT);
return MODES.stream().filter(mode -> mode.startsWith(prefix)).toList();
}
}
@@ -65,32 +65,25 @@ public final class SpigotBasePlugin extends JavaPlugin {
getServer().getPluginManager().registerEvents(teleportManager, this);
command("setbase").setExecutor(new SetBaseCommand(stateManager, baseService, Clock.systemUTC()));
command("base").setExecutor(
new BaseCommand(teleportManager, stateManager, visitorPolicy, settingsProvider)
command("base").setExecutor(new BaseCommand(teleportManager));
BaseSettingsCommand settingsCommand = new BaseSettingsCommand(
stateManager,
settingsProvider,
flightController
);
command("baseprogress").setExecutor(
new BaseProgressCommand(stateManager, settingsProvider)
);
BaseNavigationCommand navigationCommand = new BaseNavigationCommand(stateManager);
command("basenavigation").setExecutor(navigationCommand);
command("basenavigation").setTabCompleter(navigationCommand);
BaseFlightCommand flightCommand = new BaseFlightCommand(stateManager, flightController);
command("baseflight").setExecutor(flightCommand);
command("baseflight").setTabCompleter(flightCommand);
BaseVisitorsCommand visitorsCommand = new BaseVisitorsCommand(stateManager);
command("basevisitors").setExecutor(visitorsCommand);
command("basevisitors").setTabCompleter(visitorsCommand);
command("basesettings").setExecutor(settingsCommand);
command("basesettings").setTabCompleter(settingsCommand);
GoToBaseCommand goToBaseCommand = new GoToBaseCommand(stateManager, teleportManager);
command("gotobase").setExecutor(goToBaseCommand);
command("gotobase").setTabCompleter(goToBaseCommand);
command("baseadmin").setExecutor(
new BaseAdminCommand(
this,
stateManager,
new AdminProgressionService(settingsProvider),
settingsProvider
)
BaseAdminCommand adminCommand = new BaseAdminCommand(
this,
stateManager,
new AdminProgressionService(settingsProvider),
settingsProvider
);
command("baseadmin").setExecutor(adminCommand);
command("baseadmin").setTabCompleter(adminCommand);
getServer().getScheduler().runTaskTimer(
this,
@@ -1,22 +0,0 @@
package games.dmg.spigotbase;
import java.util.Locale;
final class TogglePreference {
private TogglePreference() {
}
static boolean resolve(boolean current, String[] arguments) {
if (arguments.length == 0) {
return !current;
}
if (arguments.length != 1) {
throw new IllegalArgumentException("expected zero or one argument");
}
return switch (arguments[0].toLowerCase(Locale.ROOT)) {
case "on" -> true;
case "off" -> false;
default -> throw new IllegalArgumentException("expected on or off");
};
}
}
+7 -17
View File
@@ -10,31 +10,21 @@ commands:
usage: /setbase
aliases: [sethome]
base:
description: Teleport to or upgrade your base.
usage: /base [upgrade]
description: Teleport to your base.
usage: /base
aliases: [home]
basenavigation:
description: Toggle particle navigation toward your base.
usage: /basenavigation [on|off]
aliases: [homenavigation]
baseflight:
description: Toggle flight within your base.
usage: /baseflight [on|off]
aliases: [homeflight]
basevisitors:
description: Toggle visitor access to your base.
usage: /basevisitors [on|off]
aliases: [homevisitors]
gotobase:
description: Visit an available player base.
usage: /gotobase <player>
aliases: [visit]
baseprogress:
description: View progression or toggle progress boss bars.
usage: /baseprogress [bossbar]
basesettings:
description: View progression, upgrade, and manage base settings.
usage: /basesettings [status|upgrade|visitors|navigation|flight|bossbar]
aliases: [homesettings]
baseadmin:
description: Administer Spigot Base.
usage: /baseadmin <subcommand>
aliases: [homeadmin]
permission: spigotbase.admin
permissions:
spigotbase.admin: