feat(commands): unify settings and admin commands
This commit is contained in:
@@ -24,28 +24,29 @@ The plugin JAR is written to `build/libs/`.
|
||||
```text
|
||||
/setbase (alias: /sethome)
|
||||
/base (alias: /home)
|
||||
/base upgrade (alias: /home upgrade)
|
||||
/basenavigation [on|off] (alias: /homenavigation)
|
||||
/baseflight [on|off] (alias: /homeflight)
|
||||
/basevisitors [on|off] (alias: /homevisitors)
|
||||
/gotobase <player> (alias: /visit <player>)
|
||||
/baseprogress
|
||||
/baseprogress bossbar
|
||||
/basesettings (alias: /homesettings)
|
||||
/basesettings status
|
||||
/basesettings upgrade
|
||||
/basesettings visitors <allowed|blocked>
|
||||
/basesettings navigation <enable|disable>
|
||||
/basesettings flight <enable|disable>
|
||||
/basesettings bossbar <enable|disable>
|
||||
```
|
||||
|
||||
`/base` has a stationary warm-up. Looking around is allowed, while movement between blocks, damage, teleportation, world changes, death, logout, and conflicting teleport commands cancel it without consuming the cooldown.
|
||||
|
||||
## Administration
|
||||
|
||||
The `spigotbase.admin` permission is granted to server operators by default.
|
||||
The `spigotbase.admin` permission is granted to server operators by default. `/homeadmin` aliases `/baseadmin`.
|
||||
|
||||
```text
|
||||
/baseadmin progress <player>
|
||||
/baseadmin status <player>
|
||||
/baseadmin setlevel <player> <base|size|flight|warmup|cooldown> <level>
|
||||
/baseadmin setprogress <player> <grass_dirt|stone|deepslate|obsidian|placements|base_breaks> <amount>
|
||||
/baseadmin config <numeric-key> <value>
|
||||
/baseadmin clearcooldown <player> [personal|visitor|all]
|
||||
/baseadmin reset <player> <base|size|flight|warmup|cooldown>
|
||||
/baseadmin reset <player> <size|flight|warmup|cooldown>
|
||||
/baseadmin reset <player> all confirm
|
||||
```
|
||||
|
||||
|
||||
@@ -53,3 +53,19 @@ description: Chronological record of material decisions affecting the Spigot Bas
|
||||
- Added `/homenavigation`, `/homeflight`, and `/homevisitors` aliases with the same autocomplete as their canonical commands.
|
||||
- Generalized toggle preference resolution for navigation, flight, and visitor access.
|
||||
- Verified the implementation with `./gradlew clean check jar`.
|
||||
|
||||
## 2026-08-10 — Unified base settings command
|
||||
|
||||
- Replaced the separate progress, navigation, flight, and visitor commands with `/basesettings` and its `/homesettings` alias.
|
||||
- Added nested autocomplete for status, Base IV upgrades, visitor access, navigation, flight, and progress boss-bar settings.
|
||||
- Moved the Base IV purchase from `/base upgrade` to `/basesettings upgrade`, leaving `/base` and `/home` dedicated to personal teleportation.
|
||||
- Preserved the full progression report as the default `/basesettings` response and as `/basesettings status`.
|
||||
- Verified the breaking command restructure with `./gradlew clean check jar`.
|
||||
|
||||
## 2026-08-10 — Administrative command autocomplete and reset safety
|
||||
|
||||
- Added `/homeadmin` as an alias for `/baseadmin` and replaced the administrative `progress` subcommand with `status`.
|
||||
- Added permission-aware, context-sensitive autocomplete for subcommands, online and known offline players, paths, levels, counters, cooldown types, reset confirmation, and numeric configuration keys.
|
||||
- Removed the unsafe unconfirmed base reset path; complete resets now require `reset <player> all confirm`.
|
||||
- Tightened argument validation and replaced implementation-specific enum errors with player-facing usage guidance.
|
||||
- Verified the administrative revision with `./gradlew clean check jar`.
|
||||
|
||||
@@ -17,10 +17,8 @@ As a **player with Base I**, I want visual guidance toward my base so that I can
|
||||
- [x] While enabled and in the base's world, particles are drawn along the ground to indicate the direction toward the base.
|
||||
- [x] Particle generation is bounded to avoid excessive server or client load.
|
||||
- [x] A player in another world receives a clear message instead of a misleading particle direction.
|
||||
- [x] `/basenavigation` toggles guidance on and off after Base II is unlocked.
|
||||
- [x] `/basenavigation on` enables guidance idempotently and `/basenavigation off` disables it idempotently.
|
||||
- [x] Invalid navigation arguments show command usage, and `on` and `off` are offered through autocomplete.
|
||||
- [x] `/homenavigation` aliases `/basenavigation` with identical behavior and autocomplete.
|
||||
- [x] `/basesettings navigation enable` enables guidance idempotently after Base II is unlocked, and `/basesettings navigation disable` disables it idempotently.
|
||||
- [x] Invalid navigation arguments show command usage, and `enable` and `disable` are offered through autocomplete.
|
||||
- [x] Grass-or-dirt progress after Base II preserves the player's selected navigation preference.
|
||||
- [x] The navigation preference persists across reconnects and restarts.
|
||||
|
||||
|
||||
@@ -24,9 +24,8 @@ As a **player with Base I**, I want to unlock controlled flight around my base s
|
||||
- [x] Plugin-granted flight remains active in the warning buffer and displays prominent on-screen notice that the player is leaving the base.
|
||||
- [x] Passing beyond the warning buffer removes only flight granted by this plugin.
|
||||
- [x] Flight is not granted outside the unlocked vertical range.
|
||||
- [x] `/baseflight` toggles the player's unlocked base flight on and off.
|
||||
- [x] `/baseflight on` enables flight idempotently and `/baseflight off` disables it idempotently, with `on` and `off` offered through autocomplete.
|
||||
- [x] `/homeflight` aliases `/baseflight` with identical behavior and autocomplete.
|
||||
- [x] `/basesettings flight enable` enables unlocked flight idempotently, and `/basesettings flight disable` disables it idempotently.
|
||||
- [x] Invalid flight arguments show command usage, and `enable` and `disable` are offered through autocomplete.
|
||||
- [x] The flight toggle persists across reconnects and restarts.
|
||||
- [x] The plugin handles teleportation, world changes, game-mode changes, death, logout, and plugin shutdown without leaving unintended flight enabled.
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ As a **player with Base II**, I want to earn `/base` so that I can return safely
|
||||
- [x] Only placements made in Survival mode and within the base's current horizontal and vertical bounds count.
|
||||
- [x] Player-placed blocks and replacement of previously broken blocks may contribute repeatedly.
|
||||
- [x] Base III unlocks `/base` with a configurable 30-second warm-up and three-hour cooldown by default.
|
||||
- [x] `/home` is an alias for `/base`, including `/home upgrade`.
|
||||
- [x] `/home` aliases the teleport-only `/base` command.
|
||||
- [x] Looking around without changing block coordinates does not cancel the warm-up.
|
||||
- [x] Changing block X, Y, or Z, taking damage, teleporting, changing worlds, dying, disconnecting, or starting a conflicting teleport cancels the warm-up.
|
||||
- [x] Cancellation clearly informs the player and does not consume the cooldown.
|
||||
|
||||
@@ -12,12 +12,11 @@ As a **player with Base III**, I want to open my base to visitors so that other
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] Base IV requires Base III and an established base.
|
||||
- [x] `/base upgrade` offers the Base IV purchase for a configurable price that defaults to 128 diamonds.
|
||||
- [x] `/basesettings upgrade` offers the Base IV purchase for a configurable price that defaults to 128 diamonds, replacing `/base upgrade` and `/home upgrade`.
|
||||
- [x] A successful purchase removes the complete price atomically from the player's direct inventory.
|
||||
- [x] Insufficient funds, an invalid state, or a failed persistence operation does not consume any diamonds or grant Base IV.
|
||||
- [x] `/basevisitors` lets a Base IV owner toggle visitor access on and off.
|
||||
- [x] `/basevisitors on` enables access idempotently and `/basevisitors off` disables it idempotently, with `on` and `off` offered through autocomplete.
|
||||
- [x] `/homevisitors` aliases `/basevisitors` with identical behavior and autocomplete.
|
||||
- [x] `/basesettings visitors allowed` permits visitor access idempotently for a Base IV owner, and `/basesettings visitors blocked` blocks it idempotently.
|
||||
- [x] Invalid visitor arguments show command usage, and `allowed` and `blocked` are offered through autocomplete.
|
||||
- [x] The visitor-access preference persists across reconnects and restarts.
|
||||
- [x] `/gotobase <owner>` autocompletes bases that the requesting player is currently eligible to visit.
|
||||
- [x] `/visit <owner>` aliases `/gotobase <owner>` with identical autocomplete, including eligible bases whose owners are offline.
|
||||
|
||||
@@ -11,17 +11,20 @@ As a **player**, I want to inspect my progression and receive timely milestone f
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] `/baseprogress` shows the player's Base, Base Size, Base Flight, Teleport Warm-up, and Teleport Cooldown paths.
|
||||
- [x] `/basesettings` and `/basesettings status` show the player's Base, Base Size, Base Flight, Teleport Warm-up, and Teleport Cooldown paths plus current visitor, navigation, flight, and boss-bar settings.
|
||||
- [x] Each path identifies earned levels, unmet prerequisites, current progress, the next threshold, and the next reward.
|
||||
- [x] Locked secondary paths clearly identify Base I or another sequential level as their prerequisite.
|
||||
- [x] Relevant qualifying activity briefly displays a configurable progress boss bar for the active milestone.
|
||||
- [x] Boss-bar text and fill accurately represent the current count and threshold and never exceed 100 percent.
|
||||
- [x] The automatic boss bar disappears after a configurable number of seconds.
|
||||
- [x] `/baseprogress bossbar` toggles automatic progress boss bars on and off.
|
||||
- [x] Disabling automatic boss bars does not prevent `/baseprogress` from displaying progress.
|
||||
- [x] `/basesettings bossbar enable` enables automatic progress boss bars idempotently, and `/basesettings bossbar disable` disables them idempotently.
|
||||
- [x] Invalid boss-bar arguments show command usage, and `enable` and `disable` are offered through autocomplete.
|
||||
- [x] Disabling automatic boss bars does not prevent `/basesettings` from displaying progress.
|
||||
- [x] The boss-bar preference persists across reconnects and restarts.
|
||||
- [x] Each newly unlocked level displays prominent full-screen title and subtitle text describing the reward.
|
||||
- [x] Unlock notifications occur once per earned level and do not repeat after reconnecting or restarting.
|
||||
- [x] `/homesettings` aliases `/basesettings` with identical behavior and autocomplete.
|
||||
- [x] Superseded `/baseprogress`, `/basenavigation`, `/baseflight`, and `/basevisitors` commands and their aliases are no longer registered.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -14,16 +14,20 @@ As a **server administrator**, I want command-based progression controls so that
|
||||
- [x] Administrative commands require `spigotbase.admin`, which server operators receive by default.
|
||||
- [x] Administrative player arguments safely resolve online players and previously known offline players.
|
||||
- [x] Player state remains keyed by UUID while retaining the latest known name for lookup and display.
|
||||
- [x] `/baseadmin progress <player>` displays the player's base, counters, earned path levels, active cooldowns, toggles, and visitor settings.
|
||||
- [x] `/baseadmin status <player>` displays the player's base, counters, earned path levels, active cooldowns, toggles, and visitor settings.
|
||||
- [x] `/baseadmin setlevel <player> <path> <level>` sets an earned path level while enforcing or explicitly granting required preceding levels.
|
||||
- [x] `/baseadmin setprogress <player> <path> <amount>` updates the selected counter and consistently evaluates reached tiers.
|
||||
- [x] `/baseadmin reset <player> <path>` resets a selected path without silently leaving benefits that require it.
|
||||
- [x] `/baseadmin reset <player> <size|flight|warmup|cooldown>` resets a selected path without silently leaving benefits that require it.
|
||||
- [x] `/baseadmin reset <player> base` is rejected and directs the administrator to the confirmed complete-reset command.
|
||||
- [x] `/baseadmin reset <player> all` removes the player's base, progression, active cooldowns, and plugin preferences after confirmation.
|
||||
- [x] Administrators can clear personal and visitor cooldowns independently.
|
||||
- [x] Administrative commands can update configured block requirements, warm-ups, and cooldowns for each level using validated values.
|
||||
- [x] Runtime configuration changes are persisted for subsequent restarts.
|
||||
- [x] Lowered progression requirements are evaluated for a player on their next relevant action rather than immediately updating every stored player.
|
||||
- [x] Every successful mutation reports exactly what changed, and invalid requests make no partial changes.
|
||||
- [x] `/homeadmin` aliases `/baseadmin` with identical permission requirements and autocomplete.
|
||||
- [x] Permission-aware autocomplete offers subcommands, online and known offline players, progression paths, valid levels, progress counters, cooldown types, reset paths and confirmation, and numeric configuration keys.
|
||||
- [x] Invalid and extra arguments show friendly usage without exposing implementation-specific enum errors or modifying state.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -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);
|
||||
player.sendMessage(ChatColor.RED + "Usage: /base");
|
||||
return true;
|
||||
}
|
||||
player.sendMessage(ChatColor.RED + "Usage: /base [upgrade]");
|
||||
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(
|
||||
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");
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
final class BaseAdminCommandTest {
|
||||
@Test
|
||||
void invalidPathShowsFriendlyUsage() {
|
||||
CommandSender sender = mock(CommandSender.class);
|
||||
when(sender.hasPermission("spigotbase.admin")).thenReturn(true);
|
||||
PlayerState target = PlayerState.newPlayer(UUID.randomUUID(), "Builder");
|
||||
BaseStateManager stateManager = mock(BaseStateManager.class);
|
||||
when(stateManager.findByName("Builder")).thenReturn(Optional.of(target));
|
||||
BaseAdminCommand command = new BaseAdminCommand(
|
||||
mock(JavaPlugin.class),
|
||||
stateManager,
|
||||
mock(AdminProgressionService.class),
|
||||
new PluginSettingsProvider(PluginSettings.from(Map.of()))
|
||||
);
|
||||
|
||||
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
|
||||
bukkit.when(() -> Bukkit.getPlayerExact("Builder")).thenReturn(null);
|
||||
command.onCommand(sender, null, "baseadmin",
|
||||
new String[] {"setlevel", "Builder", "unknown", "1"});
|
||||
}
|
||||
|
||||
verify(sender).sendMessage(ChatColor.RED
|
||||
+ "Usage: /baseadmin setlevel <player> <base|size|flight|warmup|cooldown> <level>");
|
||||
verify(stateManager, never()).update(any(), anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void autocompletesValidLevelsAndResetConfirmation() {
|
||||
CommandSender sender = mock(CommandSender.class);
|
||||
when(sender.hasPermission("spigotbase.admin")).thenReturn(true);
|
||||
BaseAdminCommand command = new BaseAdminCommand(
|
||||
mock(JavaPlugin.class),
|
||||
mock(BaseStateManager.class),
|
||||
mock(AdminProgressionService.class),
|
||||
new PluginSettingsProvider(PluginSettings.from(Map.of()))
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
List.of("0", "1", "2", "3", "4"),
|
||||
command.onTabComplete(sender, null, "baseadmin",
|
||||
new String[] {"setlevel", "Builder", "base", ""})
|
||||
);
|
||||
assertEquals(
|
||||
List.of("0", "1", "2", "3"),
|
||||
command.onTabComplete(sender, null, "baseadmin",
|
||||
new String[] {"setlevel", "Builder", "flight", ""})
|
||||
);
|
||||
assertEquals(
|
||||
List.of("confirm"),
|
||||
command.onTabComplete(sender, null, "baseadmin",
|
||||
new String[] {"reset", "Builder", "all", ""})
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void autocompletesCommandSpecificOptions() {
|
||||
CommandSender sender = mock(CommandSender.class);
|
||||
when(sender.hasPermission("spigotbase.admin")).thenReturn(true);
|
||||
BaseAdminCommand command = new BaseAdminCommand(
|
||||
mock(JavaPlugin.class),
|
||||
mock(BaseStateManager.class),
|
||||
mock(AdminProgressionService.class),
|
||||
new PluginSettingsProvider(PluginSettings.from(Map.of()))
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
List.of("base", "size", "flight", "warmup", "cooldown"),
|
||||
command.onTabComplete(sender, null, "baseadmin",
|
||||
new String[] {"setlevel", "Builder", ""})
|
||||
);
|
||||
assertEquals(
|
||||
List.of("grass_dirt", "stone", "deepslate", "obsidian", "placements", "base_breaks"),
|
||||
command.onTabComplete(sender, null, "baseadmin",
|
||||
new String[] {"setprogress", "Builder", ""})
|
||||
);
|
||||
assertEquals(
|
||||
List.of("personal", "visitor", "all"),
|
||||
command.onTabComplete(sender, null, "baseadmin",
|
||||
new String[] {"clearcooldown", "Builder", ""})
|
||||
);
|
||||
assertEquals(
|
||||
List.of("size", "flight", "warmup", "cooldown", "all"),
|
||||
command.onTabComplete(sender, null, "baseadmin",
|
||||
new String[] {"reset", "Builder", ""})
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void autocompletesNumericConfigurationKeys() {
|
||||
CommandSender sender = mock(CommandSender.class);
|
||||
when(sender.hasPermission("spigotbase.admin")).thenReturn(true);
|
||||
JavaPlugin plugin = mock(JavaPlugin.class);
|
||||
FileConfiguration configuration = mock(FileConfiguration.class);
|
||||
when(plugin.getConfig()).thenReturn(configuration);
|
||||
when(configuration.getValues(false)).thenReturn(Map.of(
|
||||
"base-unlock-blocks", 250,
|
||||
"visitor-currency-material", "DIAMOND",
|
||||
"title-stay-ticks", 70
|
||||
));
|
||||
BaseAdminCommand command = new BaseAdminCommand(
|
||||
plugin,
|
||||
mock(BaseStateManager.class),
|
||||
mock(AdminProgressionService.class),
|
||||
new PluginSettingsProvider(PluginSettings.from(Map.of()))
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
List.of("base-unlock-blocks"),
|
||||
command.onTabComplete(sender, null, "baseadmin", new String[] {"config", "base"})
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void autocompletesKnownOfflinePlayers() {
|
||||
CommandSender sender = mock(CommandSender.class);
|
||||
when(sender.hasPermission("spigotbase.admin")).thenReturn(true);
|
||||
BaseStateManager stateManager = mock(BaseStateManager.class);
|
||||
PlayerState alex = PlayerState.newPlayer(UUID.randomUUID(), "Alex");
|
||||
PlayerState blake = PlayerState.newPlayer(UUID.randomUUID(), "Blake");
|
||||
when(stateManager.knownPlayers()).thenReturn(Map.of(
|
||||
alex.playerId(), alex,
|
||||
blake.playerId(), blake
|
||||
));
|
||||
BaseAdminCommand command = new BaseAdminCommand(
|
||||
mock(JavaPlugin.class),
|
||||
stateManager,
|
||||
mock(AdminProgressionService.class),
|
||||
new PluginSettingsProvider(PluginSettings.from(Map.of()))
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
List.of("Alex", "Blake"),
|
||||
command.onTabComplete(sender, null, "baseadmin", new String[] {"status", ""})
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void autocompletesAdminSubcommandsForPermittedSenders() {
|
||||
CommandSender sender = mock(CommandSender.class);
|
||||
when(sender.hasPermission("spigotbase.admin")).thenReturn(true);
|
||||
BaseAdminCommand command = new BaseAdminCommand(
|
||||
mock(JavaPlugin.class),
|
||||
mock(BaseStateManager.class),
|
||||
mock(AdminProgressionService.class),
|
||||
new PluginSettingsProvider(PluginSettings.from(Map.of()))
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
List.of("status", "setlevel", "setprogress", "clearcooldown", "reset", "config"),
|
||||
command.onTabComplete(sender, null, "baseadmin", new String[] {""})
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetBaseRequiresConfirmedCompleteReset() {
|
||||
CommandSender sender = mock(CommandSender.class);
|
||||
when(sender.hasPermission("spigotbase.admin")).thenReturn(true);
|
||||
PlayerState target = PlayerState.newPlayer(UUID.randomUUID(), "Builder");
|
||||
BaseStateManager stateManager = mock(BaseStateManager.class);
|
||||
when(stateManager.findByName("Builder")).thenReturn(Optional.of(target));
|
||||
BaseAdminCommand command = new BaseAdminCommand(
|
||||
mock(JavaPlugin.class),
|
||||
stateManager,
|
||||
mock(AdminProgressionService.class),
|
||||
new PluginSettingsProvider(PluginSettings.from(Map.of()))
|
||||
);
|
||||
|
||||
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
|
||||
bukkit.when(() -> Bukkit.getPlayerExact("Builder")).thenReturn(null);
|
||||
command.onCommand(
|
||||
sender,
|
||||
null,
|
||||
"baseadmin",
|
||||
new String[] {"reset", "Builder", "base"}
|
||||
);
|
||||
}
|
||||
|
||||
verify(stateManager, never()).update(any(), anyString(), any());
|
||||
verify(sender).sendMessage(ChatColor.RED
|
||||
+ "Resetting base removes all progression. Use: /baseadmin reset Builder all confirm");
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusDisplaysKnownPlayerProgress() {
|
||||
CommandSender sender = mock(CommandSender.class);
|
||||
when(sender.hasPermission("spigotbase.admin")).thenReturn(true);
|
||||
PlayerState target = PlayerState.newPlayer(UUID.randomUUID(), "Builder");
|
||||
BaseStateManager stateManager = mock(BaseStateManager.class);
|
||||
when(stateManager.findByName("Builder")).thenReturn(Optional.of(target));
|
||||
BaseAdminCommand command = new BaseAdminCommand(
|
||||
mock(JavaPlugin.class),
|
||||
stateManager,
|
||||
mock(AdminProgressionService.class),
|
||||
new PluginSettingsProvider(PluginSettings.from(Map.of()))
|
||||
);
|
||||
|
||||
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
|
||||
bukkit.when(() -> Bukkit.getPlayerExact("Builder")).thenReturn(null);
|
||||
command.onCommand(sender, null, "baseadmin", new String[] {"status", "Builder"});
|
||||
}
|
||||
|
||||
verify(sender).sendMessage(ChatColor.GOLD + "=== Builder Base Progress ===");
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.UnaryOperator;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class BaseFlightCommandTest {
|
||||
@Test
|
||||
void autocompletesExplicitModes() {
|
||||
BaseFlightCommand command = new BaseFlightCommand(
|
||||
mock(BaseStateManager.class),
|
||||
mock(BaseFlightController.class)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
List.of("off"),
|
||||
command.onTabComplete(null, null, "baseflight", new String[] {"of"})
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void explicitOnIsIdempotent() {
|
||||
UUID playerId = UUID.randomUUID();
|
||||
Player player = mock(Player.class);
|
||||
when(player.getUniqueId()).thenReturn(playerId);
|
||||
when(player.getName()).thenReturn("Builder");
|
||||
|
||||
PlayerState current = PlayerState.newPlayer(playerId, "Builder")
|
||||
.withAdministrativeLevels(1, 0, 1, 0, 0, false, true, false);
|
||||
AtomicReference<PlayerState> updated = new AtomicReference<>();
|
||||
BaseStateManager stateManager = mock(BaseStateManager.class);
|
||||
when(stateManager.player(playerId, "Builder")).thenReturn(current);
|
||||
when(stateManager.update(any(), anyString(), any())).thenAnswer(invocation -> {
|
||||
@SuppressWarnings("unchecked")
|
||||
UnaryOperator<PlayerState> operation = invocation.getArgument(2);
|
||||
PlayerState result = operation.apply(current);
|
||||
updated.set(result);
|
||||
return result;
|
||||
});
|
||||
|
||||
BaseFlightCommand command = new BaseFlightCommand(
|
||||
stateManager,
|
||||
mock(BaseFlightController.class)
|
||||
);
|
||||
command.onCommand(player, null, "baseflight", new String[] {"on"});
|
||||
|
||||
assertTrue(updated.get().flightEnabled());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.UnaryOperator;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class BaseSettingsCommandTest {
|
||||
@Test
|
||||
void invalidArgumentsShowUsage() {
|
||||
UUID playerId = UUID.randomUUID();
|
||||
Player player = mock(Player.class);
|
||||
when(player.getUniqueId()).thenReturn(playerId);
|
||||
when(player.getName()).thenReturn("Builder");
|
||||
BaseStateManager stateManager = mock(BaseStateManager.class);
|
||||
when(stateManager.player(playerId, "Builder"))
|
||||
.thenReturn(PlayerState.newPlayer(playerId, "Builder"));
|
||||
BaseSettingsCommand command = new BaseSettingsCommand(
|
||||
stateManager,
|
||||
new PluginSettingsProvider(PluginSettings.from(Map.of())),
|
||||
mock(BaseFlightController.class)
|
||||
);
|
||||
|
||||
command.onCommand(player, null, "basesettings", new String[] {"unknown"});
|
||||
|
||||
verify(player).sendMessage(org.mockito.ArgumentMatchers.<String>argThat(
|
||||
message -> message.contains("Usage: /basesettings")
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void autocompletesValuesForEachSetting() {
|
||||
BaseSettingsCommand command = new BaseSettingsCommand(
|
||||
mock(BaseStateManager.class),
|
||||
new PluginSettingsProvider(PluginSettings.from(Map.of())),
|
||||
mock(BaseFlightController.class)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
List.of("allowed"),
|
||||
command.onTabComplete(null, null, "basesettings", new String[] {"visitors", "a"})
|
||||
);
|
||||
for (String setting : List.of("navigation", "flight", "bossbar")) {
|
||||
assertEquals(
|
||||
List.of("disable"),
|
||||
command.onTabComplete(null, null, "basesettings", new String[] {setting, "d"})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void autocompletesSettingNames() {
|
||||
BaseSettingsCommand command = new BaseSettingsCommand(
|
||||
mock(BaseStateManager.class),
|
||||
new PluginSettingsProvider(PluginSettings.from(Map.of())),
|
||||
mock(BaseFlightController.class)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
List.of("status", "upgrade", "visitors", "navigation", "flight", "bossbar"),
|
||||
command.onTabComplete(null, null, "basesettings", new String[] {""})
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void upgradePurchasesBaseIv() throws Exception {
|
||||
UUID playerId = UUID.randomUUID();
|
||||
Player player = mock(Player.class);
|
||||
when(player.getUniqueId()).thenReturn(playerId);
|
||||
when(player.getName()).thenReturn("Host");
|
||||
PlayerInventory inventory = mock(PlayerInventory.class);
|
||||
when(player.getInventory()).thenReturn(inventory);
|
||||
when(inventory.getStorageContents()).thenReturn(new ItemStack[] {
|
||||
new ItemStack(Material.DIAMOND, 64),
|
||||
new ItemStack(Material.DIAMOND, 64)
|
||||
});
|
||||
|
||||
PlayerState current = PlayerState.newPlayer(playerId, "Host")
|
||||
.withAdministrativeLevels(3, 0, 0, 0, 0, false, false, false)
|
||||
.withBase(
|
||||
new BaseLocation(UUID.randomUUID(), "world", 0, 64, 0, 0, 0),
|
||||
Instant.EPOCH
|
||||
);
|
||||
AtomicReference<PlayerState> updated = new AtomicReference<>();
|
||||
BaseStateManager stateManager = mock(BaseStateManager.class);
|
||||
when(stateManager.player(playerId, "Host")).thenReturn(current);
|
||||
when(stateManager.updateAndSave(any(), anyString(), any())).thenAnswer(invocation -> {
|
||||
@SuppressWarnings("unchecked")
|
||||
UnaryOperator<PlayerState> operation = invocation.getArgument(2);
|
||||
PlayerState result = operation.apply(current);
|
||||
updated.set(result);
|
||||
return result;
|
||||
});
|
||||
BaseSettingsCommand command = new BaseSettingsCommand(
|
||||
stateManager,
|
||||
new PluginSettingsProvider(PluginSettings.from(Map.of())),
|
||||
mock(BaseFlightController.class)
|
||||
);
|
||||
|
||||
command.onCommand(player, null, "basesettings", new String[] {"upgrade"});
|
||||
|
||||
assertEquals(4, updated.get().baseLevel());
|
||||
assertTrue(updated.get().visitorsEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void bossbarEnableIsIdempotent() {
|
||||
UUID playerId = UUID.randomUUID();
|
||||
PlayerState current = PlayerState.newPlayer(playerId, "Miner");
|
||||
|
||||
CommandResult result = execute(current, "bossbar", "enable");
|
||||
|
||||
assertTrue(result.updated().bossBarEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void flightDisableIsIdempotentAndRemovesGrantedFlight() {
|
||||
UUID playerId = UUID.randomUUID();
|
||||
PlayerState current = PlayerState.newPlayer(playerId, "Pilot")
|
||||
.withAdministrativeLevels(1, 0, 1, 0, 0, false, true, false);
|
||||
|
||||
CommandResult result = execute(current, "flight", "disable");
|
||||
|
||||
assertFalse(result.updated().flightEnabled());
|
||||
verify(result.flightController()).removeGrantedFlight(any(Player.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void navigationDisableIsIdempotent() {
|
||||
UUID playerId = UUID.randomUUID();
|
||||
PlayerState current = PlayerState.newPlayer(playerId, "Explorer")
|
||||
.withAdministrativeLevels(2, 0, 0, 0, 0, true, false, false)
|
||||
.withBase(
|
||||
new BaseLocation(UUID.randomUUID(), "world", 0, 64, 0, 0, 0),
|
||||
Instant.EPOCH
|
||||
);
|
||||
|
||||
CommandResult result = execute(current, "navigation", "disable");
|
||||
|
||||
assertFalse(result.updated().navigationEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void visitorsAllowedIsIdempotent() {
|
||||
UUID playerId = UUID.randomUUID();
|
||||
PlayerState current = PlayerState.newPlayer(playerId, "Host")
|
||||
.withAdministrativeLevels(4, 0, 0, 0, 0, false, false, true);
|
||||
|
||||
CommandResult result = execute(current, "visitors", "allowed");
|
||||
|
||||
assertTrue(result.updated().visitorsEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultAndStatusDisplayTheFullProgressReport() {
|
||||
UUID playerId = UUID.randomUUID();
|
||||
Player player = mock(Player.class);
|
||||
when(player.getUniqueId()).thenReturn(playerId);
|
||||
when(player.getName()).thenReturn("Builder");
|
||||
|
||||
BaseStateManager stateManager = mock(BaseStateManager.class);
|
||||
when(stateManager.player(playerId, "Builder"))
|
||||
.thenReturn(PlayerState.newPlayer(playerId, "Builder"));
|
||||
BaseSettingsCommand command = new BaseSettingsCommand(
|
||||
stateManager,
|
||||
new PluginSettingsProvider(PluginSettings.from(Map.of())),
|
||||
mock(BaseFlightController.class)
|
||||
);
|
||||
|
||||
command.onCommand(player, null, "basesettings", new String[0]);
|
||||
command.onCommand(player, null, "basesettings", new String[] {"status"});
|
||||
|
||||
verify(player, times(2)).sendMessage(ChatColor.GOLD + "=== Base Progress ===");
|
||||
verify(player, times(2)).sendMessage(
|
||||
org.mockito.ArgumentMatchers.<String>argThat(message ->
|
||||
message.contains("Settings: visitors=")
|
||||
&& message.contains("navigation=")
|
||||
&& message.contains("flight=")
|
||||
&& message.contains("bossbar=")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private static CommandResult execute(PlayerState current, String... arguments) {
|
||||
Player player = mock(Player.class);
|
||||
when(player.getUniqueId()).thenReturn(current.playerId());
|
||||
when(player.getName()).thenReturn(current.latestName());
|
||||
|
||||
AtomicReference<PlayerState> updated = new AtomicReference<>();
|
||||
BaseStateManager stateManager = mock(BaseStateManager.class);
|
||||
when(stateManager.player(current.playerId(), current.latestName())).thenReturn(current);
|
||||
when(stateManager.update(any(), anyString(), any())).thenAnswer(invocation -> {
|
||||
@SuppressWarnings("unchecked")
|
||||
UnaryOperator<PlayerState> operation = invocation.getArgument(2);
|
||||
PlayerState result = operation.apply(current);
|
||||
updated.set(result);
|
||||
return result;
|
||||
});
|
||||
BaseFlightController flightController = mock(BaseFlightController.class);
|
||||
BaseSettingsCommand command = new BaseSettingsCommand(
|
||||
stateManager,
|
||||
new PluginSettingsProvider(PluginSettings.from(Map.of())),
|
||||
flightController
|
||||
);
|
||||
|
||||
command.onCommand(player, null, "basesettings", arguments);
|
||||
return new CommandResult(updated.get(), flightController);
|
||||
}
|
||||
|
||||
private record CommandResult(PlayerState updated, BaseFlightController flightController) {
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.UnaryOperator;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class BaseVisitorsCommandTest {
|
||||
@Test
|
||||
void autocompletesExplicitModes() {
|
||||
BaseVisitorsCommand command = new BaseVisitorsCommand(mock(BaseStateManager.class));
|
||||
|
||||
assertEquals(
|
||||
List.of("on"),
|
||||
command.onTabComplete(null, null, "basevisitors", new String[] {"on"})
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void explicitOnIsIdempotent() {
|
||||
UUID playerId = UUID.randomUUID();
|
||||
Player player = mock(Player.class);
|
||||
when(player.getUniqueId()).thenReturn(playerId);
|
||||
when(player.getName()).thenReturn("Host");
|
||||
|
||||
PlayerState current = PlayerState.newPlayer(playerId, "Host")
|
||||
.withAdministrativeLevels(4, 0, 0, 0, 0, false, false, true);
|
||||
AtomicReference<PlayerState> updated = new AtomicReference<>();
|
||||
BaseStateManager stateManager = mock(BaseStateManager.class);
|
||||
when(stateManager.player(playerId, "Host")).thenReturn(current);
|
||||
when(stateManager.update(any(), anyString(), any())).thenAnswer(invocation -> {
|
||||
@SuppressWarnings("unchecked")
|
||||
UnaryOperator<PlayerState> operation = invocation.getArgument(2);
|
||||
PlayerState result = operation.apply(current);
|
||||
updated.set(result);
|
||||
return result;
|
||||
});
|
||||
|
||||
BaseVisitorsCommand command = new BaseVisitorsCommand(stateManager);
|
||||
command.onCommand(player, null, "basevisitors", new String[] {"on"});
|
||||
|
||||
assertTrue(updated.get().visitorsEnabled());
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import java.io.InputStream;
|
||||
@@ -10,6 +11,34 @@ import org.junit.jupiter.api.Test;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
final class PluginMetadataTest {
|
||||
@Test
|
||||
void homeadminAliasesBaseadmin() {
|
||||
Map<?, ?> commands = commands();
|
||||
|
||||
Map<?, ?> admin = (Map<?, ?>) commands.get("baseadmin");
|
||||
assertEquals(List.of("homeadmin"), admin.get("aliases"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotDeclareSupersededSettingsCommands() {
|
||||
Map<?, ?> commands = commands();
|
||||
|
||||
for (String command : List.of(
|
||||
"baseprogress", "basenavigation", "baseflight", "basevisitors"
|
||||
)) {
|
||||
assertFalse(commands.containsKey(command), command);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void homesettingsAliasesBasesettings() {
|
||||
Map<?, ?> commands = commands();
|
||||
|
||||
Map<?, ?> settings = (Map<?, ?>) commands.get("basesettings");
|
||||
assertNotNull(settings);
|
||||
assertEquals(List.of("homesettings"), settings.get("aliases"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sethomeAliasesSetbase() {
|
||||
Map<?, ?> commands = commands();
|
||||
@@ -26,30 +55,6 @@ final class PluginMetadataTest {
|
||||
assertEquals(List.of("home"), base.get("aliases"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void homevisitorsAliasesBasevisitors() {
|
||||
Map<?, ?> commands = commands();
|
||||
|
||||
Map<?, ?> visitors = (Map<?, ?>) commands.get("basevisitors");
|
||||
assertEquals(List.of("homevisitors"), visitors.get("aliases"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void homeflightAliasesBaseflight() {
|
||||
Map<?, ?> commands = commands();
|
||||
|
||||
Map<?, ?> flight = (Map<?, ?>) commands.get("baseflight");
|
||||
assertEquals(List.of("homeflight"), flight.get("aliases"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void homenavigationAliasesBasenavigation() {
|
||||
Map<?, ?> commands = commands();
|
||||
|
||||
Map<?, ?> navigation = (Map<?, ?>) commands.get("basenavigation");
|
||||
assertEquals(List.of("homenavigation"), navigation.get("aliases"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void visitAliasesGotobase() {
|
||||
Map<?, ?> commands = commands();
|
||||
@@ -70,8 +75,7 @@ final class PluginMetadataTest {
|
||||
|
||||
Map<?, ?> commands = (Map<?, ?>) plugin.get("commands");
|
||||
for (String command : new String[] {
|
||||
"setbase", "base", "basenavigation", "baseflight",
|
||||
"basevisitors", "gotobase", "baseprogress", "baseadmin"
|
||||
"setbase", "base", "gotobase", "basesettings", "baseadmin"
|
||||
}) {
|
||||
assertNotNull(commands.get(command), command);
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class TogglePreferenceTest {
|
||||
@Test
|
||||
void noArgumentTogglesCurrentPreference() {
|
||||
assertFalse(TogglePreference.resolve(true, new String[0]));
|
||||
assertTrue(TogglePreference.resolve(false, new String[0]));
|
||||
}
|
||||
|
||||
@Test
|
||||
void explicitModesAreIdempotent() {
|
||||
assertTrue(TogglePreference.resolve(true, new String[] {"on"}));
|
||||
assertTrue(TogglePreference.resolve(false, new String[] {"on"}));
|
||||
assertFalse(TogglePreference.resolve(true, new String[] {"off"}));
|
||||
assertFalse(TogglePreference.resolve(false, new String[] {"off"}));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnknownOrExtraArguments() {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> TogglePreference.resolve(false, new String[] {"maybe"})
|
||||
);
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> TogglePreference.resolve(false, new String[] {"on", "off"})
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user