feat(settings): add native base control dialog
Release / release (push) Successful in 2m45s
CI / build (push) Successful in 1m18s

This commit is contained in:
dmg
2026-08-24 10:54:26 -04:00
parent b2ba4d5821
commit b6bbf9183a
10 changed files with 589 additions and 12 deletions
@@ -20,13 +20,29 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.function.UnaryOperator;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import net.md_5.bungee.api.dialog.Dialog;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
final class BaseSettingsCommandTest {
@Test
void rejectsNonPlayerExecution() {
CommandSender sender = mock(CommandSender.class);
BaseSettingsCommand command = new BaseSettingsCommand(
mock(BaseStateManager.class),
new PluginSettingsProvider(PluginSettings.from(Map.of())),
mock(BaseFlightController.class)
);
command.onCommand(sender, null, "basesettings", new String[0]);
verify(sender).sendMessage("Only players have base progression.");
}
@Test
void spawnableEnableIsIdempotentAfterUnlock() {
UUID playerId = UUID.randomUUID();
@@ -156,8 +172,8 @@ final class BaseSettingsCommandTest {
assertEquals(
List.of(
"status", "upgrade", "pocket", "visitors", "navigation", "flight", "border",
"spawnable", "bossbar"
"ui", "status", "upgrade", "pocket", "visitors", "navigation", "flight",
"border", "spawnable", "bossbar"
),
command.onTabComplete(null, null, "basesettings", new String[] {""})
);
@@ -526,7 +542,7 @@ final class BaseSettingsCommandTest {
}
@Test
void defaultAndStatusDisplayTheFullProgressReport() {
void defaultAndUiOpenDialogWhileStatusDisplaysTheFullProgressReport() {
UUID playerId = UUID.randomUUID();
Player player = mock(Player.class);
when(player.getUniqueId()).thenReturn(playerId);
@@ -542,17 +558,19 @@ final class BaseSettingsCommandTest {
);
command.onCommand(player, null, "basesettings", new String[0]);
command.onCommand(player, null, "homesettings", new String[] {"ui"});
command.onCommand(player, null, "basesettings", new String[] {"status"});
verify(player, times(2)).sendMessage(ChatColor.GOLD + "=== Base Progress ===");
verify(player, times(2)).sendMessage(
verify(player, times(2)).showDialog(any(Dialog.class));
verify(player).sendMessage(ChatColor.GOLD + "=== Base Progress ===");
verify(player).sendMessage(
org.mockito.ArgumentMatchers.<String>argThat(message ->
message.contains("Spawnable Overlay:")
&& message.contains("0/250 placements")
&& message.contains("locked")
)
);
verify(player, times(2)).sendMessage(
verify(player).sendMessage(
org.mockito.ArgumentMatchers.<String>argThat(message ->
message.contains("Settings: visitors=")
&& message.contains("navigation=")
@@ -0,0 +1,152 @@
package games.dmg.spigotbase;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import net.md_5.bungee.api.dialog.ConfirmationDialog;
import net.md_5.bungee.api.dialog.Dialog;
import net.md_5.bungee.api.dialog.DialogListDialog;
import net.md_5.bungee.api.dialog.MultiActionDialog;
import net.md_5.bungee.api.dialog.NoticeDialog;
import net.md_5.bungee.api.dialog.action.Action;
import net.md_5.bungee.api.dialog.action.ActionButton;
import net.md_5.bungee.api.dialog.action.RunCommandAction;
import org.junit.jupiter.api.Test;
final class BaseSettingsDialogFactoryTest {
@Test
void createsControlsForEveryBaseSettingAndConfirmedPurchase() {
UUID playerId = UUID.randomUUID();
PlayerState owner = PlayerState.newPlayer(playerId, "Builder")
.withAdministrativeLevels(4, 2, 2, 1, 1, true, true, true)
.withBase(
new BaseLocation(UUID.randomUUID(), "world", 10, 64, 20, 10, 20),
Instant.EPOCH
)
.withBorderEnabled(true)
.withTotalBlocksPlaced(250)
.withSpawnableOverlayEnabled(true);
PocketBaseState pocket = new PocketBaseState(
playerId,
1,
Optional.empty(),
Optional.empty(),
true,
false,
PocketBaseBiome.PLAINS
);
Dialog dialog = new BaseSettingsDialogFactory(
PluginSettings.from(Map.of())
).create(owner, pocket);
DialogListDialog root = assertInstanceOf(DialogListDialog.class, dialog);
assertTrue(root.getBase().body().get(0).toString().contains("Base 4/4"));
List<String> commands = commands(root);
assertTrue(commands.contains("/basesettings status"));
assertTrue(commands.contains("/basesettings visitors blocked"));
assertTrue(commands.contains("/basesettings navigation disable"));
assertTrue(commands.contains("/basesettings flight disable"));
assertTrue(commands.contains("/basesettings border disable"));
assertTrue(commands.contains("/basesettings spawnable disable"));
assertTrue(commands.contains("/basesettings bossbar disable"));
assertTrue(commands.contains("/basesettings pocket upgrade"));
assertTrue(commands.contains("/basesettings pocket mobs hostile disable"));
assertTrue(commands.contains("/basesettings pocket mobs passive enable"));
assertTrue(commands.contains("/basesettings pocket type nether crimson_forest"));
}
@Test
void requiresConfirmationForBaseIvPurchase() {
UUID playerId = UUID.randomUUID();
PlayerState owner = PlayerState.newPlayer(playerId, "Builder")
.withAdministrativeLevels(3, 0, 0, 0, 0, false, false, false)
.withBase(
new BaseLocation(UUID.randomUUID(), "world", 0, 64, 0, 0, 0),
Instant.EPOCH
);
DialogListDialog root = assertInstanceOf(
DialogListDialog.class,
new BaseSettingsDialogFactory(PluginSettings.from(Map.of())).create(
owner,
PocketBaseState.locked(playerId)
)
);
ConfirmationDialog upgrade = assertInstanceOf(
ConfirmationDialog.class,
root.dialogs().get(2)
);
RunCommandAction action = assertInstanceOf(
RunCommandAction.class,
upgrade.yes().action()
);
assertTrue(action.template().equals("/basesettings upgrade"));
}
@Test
void marksUnavailableControlsAsLocked() {
UUID playerId = UUID.randomUUID();
PlayerState owner = PlayerState.newPlayer(playerId, "Newcomer");
DialogListDialog root = assertInstanceOf(
DialogListDialog.class,
new BaseSettingsDialogFactory(PluginSettings.from(Map.of())).create(
owner,
PocketBaseState.locked(playerId)
)
);
MultiActionDialog controls = assertInstanceOf(
MultiActionDialog.class,
root.dialogs().get(0)
);
List<String> labels = controls.actions().stream()
.map(ActionButton::label)
.map(Object::toString)
.toList();
assertTrue(labels.stream().anyMatch(label -> label.contains("Visitors: Locked")));
assertTrue(labels.stream().anyMatch(label -> label.contains("Navigation: Locked")));
assertTrue(labels.stream().anyMatch(label -> label.contains("Flight: Locked")));
assertTrue(labels.stream().anyMatch(label -> label.contains("Border: Locked")));
assertTrue(labels.stream().anyMatch(label -> label.contains("Spawnable Overlay: Locked")));
}
private static List<String> commands(Dialog dialog) {
List<String> commands = new ArrayList<>();
collect(dialog, commands);
return commands;
}
private static void collect(Dialog dialog, List<String> commands) {
if (dialog instanceof DialogListDialog list) {
list.dialogs().forEach(child -> collect(child, commands));
collect(list.exitAction(), commands);
} else if (dialog instanceof MultiActionDialog actions) {
actions.actions().forEach(button -> collect(button, commands));
collect(actions.exitAction(), commands);
} else if (dialog instanceof ConfirmationDialog confirmation) {
collect(confirmation.yes(), commands);
collect(confirmation.no(), commands);
} else if (dialog instanceof NoticeDialog notice) {
collect(notice.action(), commands);
}
}
private static void collect(ActionButton button, List<String> commands) {
if (button == null) {
return;
}
Action action = button.action();
if (action instanceof RunCommandAction command) {
commands.add(command.template());
}
}
}