diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 325df2e..b40db73 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: temurin - java-version: 17 + java-version: 25 cache: gradle - name: Validate conventional commits diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index a0dc591..5620f56 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -21,7 +21,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: temurin - java-version: 17 + java-version: 25 cache: gradle - name: Set up Node.js diff --git a/README.md b/README.md index 3d12412..49f0bf4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Spigot Base -A Spigot 26.2 plugin providing progression-gated player bases and quality-of-life unlocks. +A Purpur 26.2 plugin providing progression-gated player bases and quality-of-life unlocks. Players begin by breaking Survival-mode grass blocks or dirt. The progression paths provide a persistent base, navigation particles, expanding base bounds, controlled flight, personal teleportation, and visitor teleportation. @@ -8,8 +8,8 @@ The approved behavior is specified in the [OKF design bundle](design/index.md). ## Requirements -- Spigot 26.2 -- Java 17 or newer +- Purpur 26.2 build 2618 +- Java 25 or newer ## Build diff --git a/build.gradle.kts b/build.gradle.kts index 97df943..b469451 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,23 +9,23 @@ version = providers.gradleProperty("releaseVersion") repositories { mavenCentral() - maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") + maven("https://repo.purpurmc.org/snapshots/") } java { toolchain { - languageVersion = JavaLanguageVersion.of(17) + languageVersion = JavaLanguageVersion.of(25) } } tasks.withType().configureEach { - options.compilerArgs.addAll(listOf("-Xlint:all", "-Werror")) + options.compilerArgs.addAll(listOf("-Xlint:all,-deprecation,-removal", "-Werror")) } dependencies { - compileOnly("org.spigotmc:spigot-api:26.2-R0.1-SNAPSHOT") + compileOnly("org.purpurmc.purpur:purpur-api:26.2.build.2618-stable") - testImplementation("org.spigotmc:spigot-api:26.2-R0.1-SNAPSHOT") + testImplementation("org.purpurmc.purpur:purpur-api:26.2.build.2618-stable") testImplementation(platform("org.junit:junit-bom:5.13.4")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.mockito:mockito-core:5.18.0") diff --git a/design/log.md b/design/log.md index 2f10562..c7b060d 100644 --- a/design/log.md +++ b/design/log.md @@ -122,3 +122,10 @@ description: Chronological record of material decisions affecting the Spigot Bas - Retained the detailed chat report under `/basesettings status` and kept dialog actions routed through the existing validated command behavior. - Added dialog routing, autocomplete, alias, non-player, generated-control, locked-state, and purchase-confirmation coverage. - Verified the feature with `./gradlew clean check jar`. + +## 2026-08-24 — Purpur dialog runtime compatibility + +- Replaced the Spigot-only Bungee dialog types that were absent from production with Purpur's supported Paper dialog API. +- Compiled and tested against the exact Purpur 26.2 build 2618 API with Java 25, and updated both CI workflows to use Java 25. +- Preserved unit-test coverage through server-independent dialog specifications and adapted Bukkit test fixtures to Purpur's registry-aware API behavior. +- Verified 128 tests and the Java 25 plugin artifact with `./gradlew clean check jar`. diff --git a/design/user-stories/us-012-build-and-release-plugin.md b/design/user-stories/us-012-build-and-release-plugin.md index 66274a0..33a8644 100644 --- a/design/user-stories/us-012-build-and-release-plugin.md +++ b/design/user-stories/us-012-build-and-release-plugin.md @@ -1,7 +1,7 @@ --- type: User Story title: "US-012: Build and release the plugin" -description: Give maintainers repeatable Spigot builds, automated verification, and versioned Gitea releases. +description: Give maintainers repeatable Purpur builds, automated verification, and versioned Gitea releases. status: done --- @@ -11,7 +11,7 @@ As a **plugin maintainer**, I want automated builds and releases modeled on Trig ## Acceptance criteria -- [x] The Gradle project compiles against Spigot API `26.2-R0.1-SNAPSHOT` using a Java 17 toolchain. +- [x] The Gradle project compiles against Purpur API `26.2.build.2618-stable` using a Java 25 toolchain. - [x] Compiler lint warnings fail the build. - [x] Automated JUnit 5 tests run as part of the Gradle check lifecycle. - [x] Pushes and pull requests build and test the plugin in Gitea Actions. diff --git a/design/user-stories/us-020-control-base-through-dialog-ui.md b/design/user-stories/us-020-control-base-through-dialog-ui.md index 7981b9b..56260c7 100644 --- a/design/user-stories/us-020-control-base-through-dialog-ui.md +++ b/design/user-stories/us-020-control-base-through-dialog-ui.md @@ -2,7 +2,7 @@ type: User Story title: "US-020: Control a base through a dialog UI" description: Let players inspect and control their base through Minecraft's native dialog interface. -status: done +status: in-progress --- # US-020: Control a base through a dialog UI @@ -21,6 +21,7 @@ As a **player**, I want a dialog-based base dashboard so that I can inspect prog - [x] `ui` is offered through command autocomplete. - [x] Non-player execution remains safely rejected. - [x] Automated tests verify dialog routing and generated controls. +- [ ] The dialog uses Purpur's supported Paper dialog API and opens on the production Purpur 26.2 runtime without missing-class or command exceptions. ## Related diff --git a/settings.gradle.kts b/settings.gradle.kts index bf3d402..2ed15d9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,5 @@ +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} + rootProject.name = "spigot-base" diff --git a/src/main/java/games/dmg/spigotbase/BaseSettingsCommand.java b/src/main/java/games/dmg/spigotbase/BaseSettingsCommand.java index d2fe0f9..7477fbd 100644 --- a/src/main/java/games/dmg/spigotbase/BaseSettingsCommand.java +++ b/src/main/java/games/dmg/spigotbase/BaseSettingsCommand.java @@ -1,5 +1,6 @@ package games.dmg.spigotbase; +import io.papermc.paper.dialog.Dialog; import java.io.IOException; import java.util.List; import java.util.Locale; @@ -28,6 +29,7 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter { private final TeleportPolicy teleportPolicy; private final BaseFlightController flightController; private final PocketBaseManager pocketBases; + private final DialogProvider dialogProvider; BaseSettingsCommand( BaseStateManager stateManager, @@ -42,12 +44,30 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter { PluginSettingsProvider settings, BaseFlightController flightController, PocketBaseManager pocketBases + ) { + this( + stateManager, + settings, + flightController, + pocketBases, + (owner, pocket) -> new BaseSettingsDialogFactory(settings.current()) + .create(owner, pocket) + ); + } + + BaseSettingsCommand( + BaseStateManager stateManager, + PluginSettingsProvider settings, + BaseFlightController flightController, + PocketBaseManager pocketBases, + DialogProvider dialogProvider ) { this.stateManager = stateManager; this.settings = settings; this.teleportPolicy = new TeleportPolicy(settings); this.flightController = flightController; this.pocketBases = pocketBases; + this.dialogProvider = dialogProvider; } @Override @@ -449,7 +469,7 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter { PocketBaseState pocket = pocketBases == null ? PocketBaseState.locked(state.playerId()) : pocketBases.state(state.playerId()); - player.showDialog(new BaseSettingsDialogFactory(settings.current()).create(state, pocket)); + player.showDialog(dialogProvider.create(state, pocket)); } private void showStatus(Player player, PlayerState state) { @@ -672,6 +692,11 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter { return met ? ChatColor.YELLOW : ChatColor.RED; } + @FunctionalInterface + interface DialogProvider { + Dialog create(PlayerState owner, PocketBaseState pocket); + } + private static void sendUsage(Player player) { player.sendMessage(ChatColor.RED + "Usage: /basesettings " + "[ui|status|upgrade|pocket upgrade|pocket mobs " diff --git a/src/main/java/games/dmg/spigotbase/BaseSettingsDialogFactory.java b/src/main/java/games/dmg/spigotbase/BaseSettingsDialogFactory.java index 99c8f80..2c68998 100644 --- a/src/main/java/games/dmg/spigotbase/BaseSettingsDialogFactory.java +++ b/src/main/java/games/dmg/spigotbase/BaseSettingsDialogFactory.java @@ -1,19 +1,17 @@ package games.dmg.spigotbase; +import io.papermc.paper.dialog.Dialog; +import io.papermc.paper.registry.RegistryKey; +import io.papermc.paper.registry.data.dialog.ActionButton; +import io.papermc.paper.registry.data.dialog.DialogBase; +import io.papermc.paper.registry.data.dialog.action.DialogAction; +import io.papermc.paper.registry.data.dialog.body.DialogBody; +import io.papermc.paper.registry.data.dialog.type.DialogType; +import io.papermc.paper.registry.set.RegistrySet; import java.util.Arrays; import java.util.List; import java.util.Locale; -import net.md_5.bungee.api.chat.BaseComponent; -import net.md_5.bungee.api.chat.TextComponent; -import net.md_5.bungee.api.dialog.ConfirmationDialog; -import net.md_5.bungee.api.dialog.Dialog; -import net.md_5.bungee.api.dialog.DialogBase; -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.ActionButton; -import net.md_5.bungee.api.dialog.action.RunCommandAction; -import net.md_5.bungee.api.dialog.body.PlainMessageBody; +import net.kyori.adventure.text.Component; final class BaseSettingsDialogFactory { private static final int DIALOG_WIDTH = 420; @@ -24,8 +22,18 @@ final class BaseSettingsDialogFactory { } Dialog create(PlayerState owner, PocketBaseState pocket) { - return new DialogListDialog( - base( + return render(specification(owner, pocket)); + } + + DialogSpec specification(PlayerState owner, PocketBaseState pocket) { + List dialogs = List.of( + generalSettings(owner), + progressReport(), + baseUpgrade(owner), + pocketSettings(owner, pocket) + ); + return new ListSpec( + content( "Base Settings", "Base Settings", "Base " + owner.baseLevel() + "/4 • Size " + owner.sizeLevel() + "/3" @@ -33,95 +41,91 @@ final class BaseSettingsDialogFactory { + "Pocket Base " + pocket.level() + " • " + humanize(pocket.biome().commandName()) ), - List.of( - generalSettings(owner), - progressReport(), - baseUpgrade(owner), - pocketSettings(owner, pocket) - ), - null, + dialogs, 2, 190 ); } - private Dialog generalSettings(PlayerState owner) { - return new MultiActionDialog( - base( + private DialogSpec generalSettings(PlayerState owner) { + List actions = List.of( + toggle( + "Visitors", + owner.baseLevel() >= 4, + owner.visitorsEnabled(), + "Allowed", + "Blocked", + "basesettings visitors " + + (owner.visitorsEnabled() ? "blocked" : "allowed") + ), + toggle( + "Navigation", + owner.baseLevel() >= 2, + owner.navigationEnabled(), + "Enabled", + "Disabled", + "basesettings navigation " + mode(!owner.navigationEnabled()) + ), + toggle( + "Flight", + owner.flightLevel() >= 1, + owner.flightEnabled(), + "Enabled", + "Disabled", + "basesettings flight " + mode(!owner.flightEnabled()) + ), + toggle( + "Border", + owner.baseLevel() >= 1 && owner.base().isPresent(), + owner.borderEnabled(), + "Enabled", + "Disabled", + "basesettings border " + mode(!owner.borderEnabled()) + ), + toggle( + "Spawnable Overlay", + owner.baseLevel() >= 1 + && owner.base().isPresent() + && owner.totalBlocksPlaced() + >= settings.spawnableOverlayUnlockPlacements(), + owner.spawnableOverlayEnabled(), + "Enabled", + "Disabled", + "basesettings spawnable " + mode(!owner.spawnableOverlayEnabled()) + ), + toggle( + "Progress Boss Bar", + true, + owner.bossBarEnabled(), + "Enabled", + "Disabled", + "basesettings bossbar " + mode(!owner.bossBarEnabled()) + ) + ); + return new MultiSpec( + content( "Base Controls", "Base Controls", "Select a setting to change it. Locked controls remain visible and explain " + "their requirement in chat when selected." ), - List.of( - toggle( - "Visitors", - owner.baseLevel() >= 4, - owner.visitorsEnabled(), - "Allowed", - "Blocked", - "basesettings visitors " - + (owner.visitorsEnabled() ? "blocked" : "allowed") - ), - toggle( - "Navigation", - owner.baseLevel() >= 2, - owner.navigationEnabled(), - "Enabled", - "Disabled", - "basesettings navigation " - + mode(!owner.navigationEnabled()) - ), - toggle( - "Flight", - owner.flightLevel() >= 1, - owner.flightEnabled(), - "Enabled", - "Disabled", - "basesettings flight " + mode(!owner.flightEnabled()) - ), - toggle( - "Border", - owner.baseLevel() >= 1 && owner.base().isPresent(), - owner.borderEnabled(), - "Enabled", - "Disabled", - "basesettings border " + mode(!owner.borderEnabled()) - ), - toggle( - "Spawnable Overlay", - owner.baseLevel() >= 1 - && owner.base().isPresent() - && owner.totalBlocksPlaced() - >= settings.spawnableOverlayUnlockPlacements(), - owner.spawnableOverlayEnabled(), - "Enabled", - "Disabled", - "basesettings spawnable " + mode(!owner.spawnableOverlayEnabled()) - ), - toggle( - "Progress Boss Bar", - true, - owner.bossBarEnabled(), - "Enabled", - "Disabled", - "basesettings bossbar " + mode(!owner.bossBarEnabled()) - ) - ), - 2, - null + actions, + 2 ); } - private Dialog progressReport() { - return new NoticeDialog(base( - "Progress Report", - "Progress Report", - "Display the detailed progression report in chat." - )).action(commandButton("Show in Chat", "basesettings status")); + private DialogSpec progressReport() { + return new NoticeSpec( + content( + "Progress Report", + "Progress Report", + "Display the detailed progression report in chat." + ), + commandButton("Show in Chat", "basesettings status") + ); } - private Dialog baseUpgrade(PlayerState owner) { + private DialogSpec baseUpgrade(PlayerState owner) { if (owner.baseLevel() >= 4) { return notice("Base IV", "Base IV and visitor access are already unlocked."); } @@ -131,17 +135,22 @@ final class BaseSettingsDialogFactory { "Unlock and establish Base III before purchasing Base IV visitor access." ); } - int price = settings.visitorUnlockDiamondCost(); return confirmation( "Unlock Base IV", - "Purchase visitor access for " + price + " diamonds?", + "Purchase visitor access for " + settings.visitorUnlockDiamondCost() + + " diamonds?", "basesettings upgrade" ); } - private Dialog pocketSettings(PlayerState owner, PocketBaseState pocket) { - return new DialogListDialog( - base( + private DialogSpec pocketSettings(PlayerState owner, PocketBaseState pocket) { + List dialogs = List.of( + pocketUpgrade(owner, pocket), + pocketMobSettings(pocket), + pocketBiomeSettings(pocket) + ); + return new ListSpec( + content( "Pocket Base", "Pocket Base " + pocket.level(), pocket.level() == 0 @@ -151,18 +160,13 @@ final class BaseSettingsDialogFactory { + enabled(pocket.hostileMobSpawningEnabled()) + " • Passive mobs: " + enabled(pocket.passiveMobSpawningEnabled()) ), - List.of( - pocketUpgrade(owner, pocket), - pocketMobSettings(pocket), - pocketBiomeSettings(pocket) - ), - null, + dialogs, 1, 260 ); } - private Dialog pocketUpgrade(PlayerState owner, PocketBaseState pocket) { + private DialogSpec pocketUpgrade(PlayerState owner, PocketBaseState pocket) { if (owner.baseLevel() < 4 || owner.base().isEmpty()) { return notice( "Pocket Upgrade — Locked", @@ -183,57 +187,56 @@ final class BaseSettingsDialogFactory { ); } - private Dialog pocketMobSettings(PocketBaseState pocket) { + private DialogSpec pocketMobSettings(PocketBaseState pocket) { if (pocket.level() < 1) { return notice( "Mob Spawning — Locked", "Unlock Pocket Base I to control natural mob spawning." ); } - return new MultiActionDialog( - base( + List actions = List.of( + toggle( + "Hostile Mobs", + true, + pocket.hostileMobSpawningEnabled(), + "Enabled", + "Disabled", + "basesettings pocket mobs hostile " + + mode(!pocket.hostileMobSpawningEnabled()) + ), + toggle( + "Passive Mobs", + true, + pocket.passiveMobSpawningEnabled(), + "Enabled", + "Disabled", + "basesettings pocket mobs passive " + + mode(!pocket.passiveMobSpawningEnabled()) + ) + ); + return new MultiSpec( + content( "Mob Spawning", "Mob Spawning", "Control natural spawning independently for each mob category." ), - List.of( - toggle( - "Hostile Mobs", - true, - pocket.hostileMobSpawningEnabled(), - "Enabled", - "Disabled", - "basesettings pocket mobs hostile " - + mode(!pocket.hostileMobSpawningEnabled()) - ), - toggle( - "Passive Mobs", - true, - pocket.passiveMobSpawningEnabled(), - "Enabled", - "Disabled", - "basesettings pocket mobs passive " - + mode(!pocket.passiveMobSpawningEnabled()) - ) - ), - 1, - null + actions, + 1 ); } - private Dialog pocketBiomeSettings(PocketBaseState pocket) { + private DialogSpec pocketBiomeSettings(PocketBaseState pocket) { if (pocket.level() < 1) { return notice( "Change Biome — Locked", "Unlock Pocket Base I to purchase biome changes." ); } - List worldTypes = Arrays.stream(PocketBaseWorldType.values()) + List worldTypes = Arrays.stream(PocketBaseWorldType.values()) .map(type -> pocketBiomeType(pocket, type)) - .map(Dialog.class::cast) .toList(); - return new DialogListDialog( - base( + return new ListSpec( + content( "Change Biome", "Change Biome", "Current biome: " + pocket.biome().worldType().commandName() + "/" @@ -242,35 +245,32 @@ final class BaseSettingsDialogFactory { + humanize(settings.pocketBaseBiomeCurrencyMaterial()) + "." ), worldTypes, - null, 1, 220 ); } - private DialogListDialog pocketBiomeType( + private DialogSpec pocketBiomeType( PocketBaseState pocket, PocketBaseWorldType worldType ) { - List biomes = Arrays.stream(PocketBaseBiome.values()) + List biomes = Arrays.stream(PocketBaseBiome.values()) .filter(biome -> biome.worldType() == worldType) .map(biome -> biomeConfirmation(pocket, biome)) - .map(Dialog.class::cast) .toList(); - return new DialogListDialog( - base( + return new ListSpec( + content( humanize(worldType.commandName()), humanize(worldType.commandName()) + " Biomes", "Choose a biome subtype. The active biome is marked as selected." ), biomes, - null, 2, 190 ); } - private Dialog biomeConfirmation(PocketBaseState pocket, PocketBaseBiome biome) { + private DialogSpec biomeConfirmation(PocketBaseState pocket, PocketBaseBiome biome) { String title = humanize(biome.commandName()) + (pocket.biome() == biome ? " — Selected" : ""); if (pocket.biome() == biome) { @@ -285,16 +285,18 @@ final class BaseSettingsDialogFactory { ); } - private static Dialog confirmation(String title, String message, String command) { - return new ConfirmationDialog(base(title, title, message)) - .yes(commandButton("Confirm", command)); + private static DialogSpec confirmation(String title, String message, String command) { + return new ConfirmationSpec( + content(title, title, message), + commandButton("Confirm", command) + ); } - private static Dialog notice(String title, String message) { - return new NoticeDialog(base(title, title, message)); + private static DialogSpec notice(String title, String message) { + return new NoticeSpec(content(title, title, message), null); } - private static ActionButton toggle( + private static ButtonSpec toggle( String name, boolean unlocked, boolean active, @@ -306,29 +308,106 @@ final class BaseSettingsDialogFactory { String tooltip = unlocked ? "Click to change this setting." : "This setting has not been unlocked yet."; - return new ActionButton( - text(name + ": " + state), - text(tooltip), - 190, - new RunCommandAction("/" + command) - ); + return new ButtonSpec(name + ": " + state, tooltip, 190, "/" + command); } - private static ActionButton commandButton(String label, String command) { - return new ActionButton(text(label), new RunCommandAction("/" + command)); + private static ButtonSpec commandButton(String label, String command) { + return new ButtonSpec(label, "", 150, "/" + command); } - private static DialogBase base(String title, String externalTitle, String message) { - return new DialogBase(text(title)) - .externalTitle(text(externalTitle)) - .body(List.of(new PlainMessageBody(text(message), DIALOG_WIDTH))) + private static DialogContent content( + String title, + String externalTitle, + String message + ) { + return new DialogContent(title, externalTitle, message); + } + + private static Dialog render(DialogSpec specification) { + DialogType type; + if (specification instanceof ListSpec list) { + List dialogs = list.dialogs().stream() + .map(BaseSettingsDialogFactory::render) + .toList(); + type = DialogType.dialogList( + RegistrySet.valueSet(RegistryKey.DIALOG, dialogs), + null, + list.columns(), + list.buttonWidth() + ); + } else if (specification instanceof MultiSpec multi) { + List actions = multi.actions().stream() + .map(BaseSettingsDialogFactory::render) + .toList(); + type = DialogType.multiAction(actions, null, multi.columns()); + } else if (specification instanceof ConfirmationSpec confirmation) { + ActionButton cancel = ActionButton.builder(Component.text("Cancel")).build(); + type = DialogType.confirmation(render(confirmation.confirm()), cancel); + } else if (specification instanceof NoticeSpec notice) { + type = notice.action() == null + ? DialogType.notice() + : DialogType.notice(render(notice.action())); + } else { + throw new IllegalArgumentException("Unsupported dialog specification"); + } + DialogContent content = specification.content(); + DialogBase base = DialogBase.builder(Component.text(content.title())) + .externalTitle(Component.text(content.externalTitle())) + .body(List.of(DialogBody.plainMessage( + Component.text(content.message()), DIALOG_WIDTH + ))) .canCloseWithEscape(true) .pause(false) - .afterAction(DialogBase.AfterAction.CLOSE); + .afterAction(DialogBase.DialogAfterAction.CLOSE) + .build(); + return Dialog.create(factory -> factory.empty().base(base).type(type)); } - private static BaseComponent text(String value) { - return new TextComponent(value); + private static ActionButton render(ButtonSpec button) { + ActionButton.Builder builder = ActionButton.builder(Component.text(button.label())) + .width(button.width()) + .action(DialogAction.commandTemplate(button.command())); + if (!button.tooltip().isEmpty()) { + builder.tooltip(Component.text(button.tooltip())); + } + return builder.build(); + } + + sealed interface DialogSpec permits ListSpec, MultiSpec, ConfirmationSpec, NoticeSpec { + DialogContent content(); + } + + record DialogContent(String title, String externalTitle, String message) { + } + + record ListSpec( + DialogContent content, + List dialogs, + int columns, + int buttonWidth + ) implements DialogSpec { + } + + record MultiSpec( + DialogContent content, + List actions, + int columns + ) implements DialogSpec { + } + + record ConfirmationSpec( + DialogContent content, + ButtonSpec confirm + ) implements DialogSpec { + } + + record NoticeSpec( + DialogContent content, + ButtonSpec action + ) implements DialogSpec { + } + + record ButtonSpec(String label, String tooltip, int width, String command) { } private static String mode(boolean active) { diff --git a/src/main/java/games/dmg/spigotbase/PocketBaseWorldService.java b/src/main/java/games/dmg/spigotbase/PocketBaseWorldService.java index 90ce058..a43b17e 100644 --- a/src/main/java/games/dmg/spigotbase/PocketBaseWorldService.java +++ b/src/main/java/games/dmg/spigotbase/PocketBaseWorldService.java @@ -137,7 +137,7 @@ final class PocketBaseWorldService { World world = server.getWorld(name); boolean created = world == null; if (created) { - WorldCreator creator = new WorldCreator(name) + WorldCreator creator = new WorldCreator(NamespacedKey.minecraft(name)) .environment(World.Environment.NORMAL) .generator(new VoidPocketChunkGenerator()) .generateStructures(false); diff --git a/src/test/java/games/dmg/spigotbase/BaseSettingsCommandTest.java b/src/test/java/games/dmg/spigotbase/BaseSettingsCommandTest.java index 4f0f350..4b6be68 100644 --- a/src/test/java/games/dmg/spigotbase/BaseSettingsCommandTest.java +++ b/src/test/java/games/dmg/spigotbase/BaseSettingsCommandTest.java @@ -24,7 +24,7 @@ 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 net.kyori.adventure.dialog.DialogLike; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -186,10 +186,12 @@ final class BaseSettingsCommandTest { when(player.getUniqueId()).thenReturn(playerId); when(player.getName()).thenReturn("Host"); PlayerInventory inventory = mock(PlayerInventory.class); + ItemStack firstStack = item(Material.DIAMOND, 64); + ItemStack secondStack = item(Material.DIAMOND, 64); when(player.getInventory()).thenReturn(inventory); when(inventory.getStorageContents()).thenReturn(new ItemStack[] { - new ItemStack(Material.DIAMOND, 64), - new ItemStack(Material.DIAMOND, 64) + firstStack, + secondStack }); PlayerState current = PlayerState.newPlayer(playerId, "Host") @@ -227,10 +229,9 @@ final class BaseSettingsCommandTest { when(player.getUniqueId()).thenReturn(playerId); when(player.getName()).thenReturn("Builder"); PlayerInventory inventory = mock(PlayerInventory.class); + ItemStack currency = item(Material.DIAMOND_BLOCK, 64); when(player.getInventory()).thenReturn(inventory); - when(inventory.getStorageContents()).thenReturn(new ItemStack[] { - new ItemStack(Material.DIAMOND_BLOCK, 64) - }); + when(inventory.getStorageContents()).thenReturn(new ItemStack[] {currency}); PlayerState owner = PlayerState.newPlayer(playerId, "Builder") .withAdministrativeLevels(4, 0, 0, 0, 0, false, false, true) .withBase( @@ -264,10 +265,9 @@ final class BaseSettingsCommandTest { when(player.getUniqueId()).thenReturn(playerId); when(player.getName()).thenReturn("Builder"); PlayerInventory inventory = mock(PlayerInventory.class); + ItemStack currency = item(Material.NETHERITE_BLOCK, 16); when(player.getInventory()).thenReturn(inventory); - when(inventory.getStorageContents()).thenReturn(new ItemStack[] { - new ItemStack(Material.NETHERITE_BLOCK, 16) - }); + when(inventory.getStorageContents()).thenReturn(new ItemStack[] {currency}); BaseStateManager stateManager = mock(BaseStateManager.class); when(stateManager.player(playerId, "Builder")) .thenReturn(PlayerState.newPlayer(playerId, "Builder")); @@ -429,10 +429,9 @@ final class BaseSettingsCommandTest { when(player.getUniqueId()).thenReturn(playerId); when(player.getName()).thenReturn("Builder"); PlayerInventory inventory = mock(PlayerInventory.class); + ItemStack currency = item(Material.DIAMOND_BLOCK, 64); when(player.getInventory()).thenReturn(inventory); - when(inventory.getStorageContents()).thenReturn(new ItemStack[] { - new ItemStack(Material.DIAMOND_BLOCK, 64) - }); + when(inventory.getStorageContents()).thenReturn(new ItemStack[] {currency}); PlayerState owner = PlayerState.newPlayer(playerId, "Builder") .withAdministrativeLevels(4, 0, 0, 0, 0, false, false, true) .withBase( @@ -554,14 +553,16 @@ final class BaseSettingsCommandTest { BaseSettingsCommand command = new BaseSettingsCommand( stateManager, new PluginSettingsProvider(PluginSettings.from(Map.of())), - mock(BaseFlightController.class) + mock(BaseFlightController.class), + null, + (owner, pocket) -> null ); 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)).showDialog(any(Dialog.class)); + verify(player, times(2)).showDialog((DialogLike) null); verify(player).sendMessage(ChatColor.GOLD + "=== Base Progress ==="); verify(player).sendMessage( org.mockito.ArgumentMatchers.argThat(message -> @@ -582,6 +583,18 @@ final class BaseSettingsCommandTest { ); } + private static ItemStack item(Material material, int amount) { + ItemStack item = mock(ItemStack.class); + ItemStack copy = mock(ItemStack.class); + when(item.getType()).thenReturn(material); + when(item.getAmount()).thenReturn(amount); + when(item.clone()).thenReturn(copy); + when(copy.getType()).thenReturn(material); + when(copy.getAmount()).thenReturn(amount); + when(copy.clone()).thenReturn(copy); + return item; + } + private static CommandResult execute(PlayerState current, String... arguments) { Player player = mock(Player.class); when(player.getUniqueId()).thenReturn(current.playerId()); diff --git a/src/test/java/games/dmg/spigotbase/BaseSettingsDialogFactoryTest.java b/src/test/java/games/dmg/spigotbase/BaseSettingsDialogFactoryTest.java index e087266..d49b1c6 100644 --- a/src/test/java/games/dmg/spigotbase/BaseSettingsDialogFactoryTest.java +++ b/src/test/java/games/dmg/spigotbase/BaseSettingsDialogFactoryTest.java @@ -1,5 +1,6 @@ package games.dmg.spigotbase; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -9,14 +10,6 @@ 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 { @@ -42,12 +35,15 @@ final class BaseSettingsDialogFactoryTest { PocketBaseBiome.PLAINS ); - Dialog dialog = new BaseSettingsDialogFactory( - PluginSettings.from(Map.of()) - ).create(owner, pocket); + BaseSettingsDialogFactory.DialogSpec specification = + new BaseSettingsDialogFactory(PluginSettings.from(Map.of())) + .specification(owner, pocket); - DialogListDialog root = assertInstanceOf(DialogListDialog.class, dialog); - assertTrue(root.getBase().body().get(0).toString().contains("Base 4/4")); + BaseSettingsDialogFactory.ListSpec root = assertInstanceOf( + BaseSettingsDialogFactory.ListSpec.class, + specification + ); + assertTrue(root.content().message().contains("Base 4/4")); List commands = commands(root); assertTrue(commands.contains("/basesettings status")); assertTrue(commands.contains("/basesettings visitors blocked")); @@ -72,23 +68,19 @@ final class BaseSettingsDialogFactoryTest { Instant.EPOCH ); - DialogListDialog root = assertInstanceOf( - DialogListDialog.class, - new BaseSettingsDialogFactory(PluginSettings.from(Map.of())).create( + BaseSettingsDialogFactory.ListSpec root = assertInstanceOf( + BaseSettingsDialogFactory.ListSpec.class, + new BaseSettingsDialogFactory(PluginSettings.from(Map.of())).specification( owner, PocketBaseState.locked(playerId) ) ); - ConfirmationDialog upgrade = assertInstanceOf( - ConfirmationDialog.class, + BaseSettingsDialogFactory.ConfirmationSpec upgrade = assertInstanceOf( + BaseSettingsDialogFactory.ConfirmationSpec.class, root.dialogs().get(2) ); - RunCommandAction action = assertInstanceOf( - RunCommandAction.class, - upgrade.yes().action() - ); - assertTrue(action.template().equals("/basesettings upgrade")); + assertEquals("/basesettings upgrade", upgrade.confirm().command()); } @Test @@ -96,20 +88,19 @@ final class BaseSettingsDialogFactoryTest { UUID playerId = UUID.randomUUID(); PlayerState owner = PlayerState.newPlayer(playerId, "Newcomer"); - DialogListDialog root = assertInstanceOf( - DialogListDialog.class, - new BaseSettingsDialogFactory(PluginSettings.from(Map.of())).create( + BaseSettingsDialogFactory.ListSpec root = assertInstanceOf( + BaseSettingsDialogFactory.ListSpec.class, + new BaseSettingsDialogFactory(PluginSettings.from(Map.of())).specification( owner, PocketBaseState.locked(playerId) ) ); - MultiActionDialog controls = assertInstanceOf( - MultiActionDialog.class, + BaseSettingsDialogFactory.MultiSpec controls = assertInstanceOf( + BaseSettingsDialogFactory.MultiSpec.class, root.dialogs().get(0) ); List labels = controls.actions().stream() - .map(ActionButton::label) - .map(Object::toString) + .map(BaseSettingsDialogFactory.ButtonSpec::label) .toList(); assertTrue(labels.stream().anyMatch(label -> label.contains("Visitors: Locked"))); @@ -119,34 +110,28 @@ final class BaseSettingsDialogFactoryTest { assertTrue(labels.stream().anyMatch(label -> label.contains("Spawnable Overlay: Locked"))); } - private static List commands(Dialog dialog) { + private static List commands( + BaseSettingsDialogFactory.DialogSpec specification + ) { List commands = new ArrayList<>(); - collect(dialog, commands); + collect(specification, commands); return commands; } - private static void collect(Dialog dialog, List commands) { - if (dialog instanceof DialogListDialog list) { + private static void collect( + BaseSettingsDialogFactory.DialogSpec specification, + List commands + ) { + if (specification instanceof BaseSettingsDialogFactory.ListSpec 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 commands) { - if (button == null) { - return; - } - Action action = button.action(); - if (action instanceof RunCommandAction command) { - commands.add(command.template()); + } else if (specification instanceof BaseSettingsDialogFactory.MultiSpec multi) { + multi.actions().forEach(button -> commands.add(button.command())); + } else if (specification + instanceof BaseSettingsDialogFactory.ConfirmationSpec confirmation) { + commands.add(confirmation.confirm().command()); + } else if (specification instanceof BaseSettingsDialogFactory.NoticeSpec notice + && notice.action() != null) { + commands.add(notice.action().command()); } } } diff --git a/src/test/java/games/dmg/spigotbase/PocketBaseControllerTest.java b/src/test/java/games/dmg/spigotbase/PocketBaseControllerTest.java index 7b2091d..9e04561 100644 --- a/src/test/java/games/dmg/spigotbase/PocketBaseControllerTest.java +++ b/src/test/java/games/dmg/spigotbase/PocketBaseControllerTest.java @@ -37,6 +37,7 @@ final class PocketBaseControllerTest { World world = mock(World.class); Block clicked = block(world, -2, 66, 0, Material.DIAMOND_BLOCK); PlayerInteractEvent event = mock(PlayerInteractEvent.class); + ItemStack activator = item(Material.FLINT_AND_STEEL); BaseStateManager baseStates = mock(BaseStateManager.class); PocketBaseManager pocketBases = mock(PocketBaseManager.class); PluginSettings settings = PluginSettings.from(Map.of()); @@ -49,7 +50,7 @@ final class PocketBaseControllerTest { when(event.getPlayer()).thenReturn(owner); when(event.getAction()).thenReturn(Action.RIGHT_CLICK_BLOCK); when(event.getClickedBlock()).thenReturn(clicked); - when(event.getItem()).thenReturn(new ItemStack(Material.FLINT_AND_STEEL)); + when(event.getItem()).thenReturn(activator); when(world.getUID()).thenReturn(worldId); when(world.getName()).thenReturn("world"); when(world.getBlockAt(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> { @@ -98,6 +99,7 @@ final class PocketBaseControllerTest { World world = mock(World.class); Block clicked = block(world, 8, 67, 8, Material.DIAMOND_BLOCK); PlayerInteractEvent event = mock(PlayerInteractEvent.class); + ItemStack activator = item(Material.FLINT_AND_STEEL); PocketBaseManager pocketBases = mock(PocketBaseManager.class); PluginSettings settings = PluginSettings.from(Map.of()); @@ -105,7 +107,7 @@ final class PocketBaseControllerTest { when(event.getPlayer()).thenReturn(owner); when(event.getAction()).thenReturn(Action.RIGHT_CLICK_BLOCK); when(event.getClickedBlock()).thenReturn(clicked); - when(event.getItem()).thenReturn(new ItemStack(Material.FLINT_AND_STEEL)); + when(event.getItem()).thenReturn(activator); when(world.getUID()).thenReturn(worldId); when(world.getName()).thenReturn("pocket"); when(world.getBlockAt(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> { @@ -152,6 +154,7 @@ final class PocketBaseControllerTest { World world = mock(World.class); Block clicked = block(world, 30, 67, 8, Material.DIAMOND_BLOCK); PlayerInteractEvent event = mock(PlayerInteractEvent.class); + ItemStack activator = item(Material.FLINT_AND_STEEL); PocketBaseManager pocketBases = mock(PocketBaseManager.class); PluginSettings settings = PluginSettings.from(Map.of()); @@ -159,7 +162,7 @@ final class PocketBaseControllerTest { when(event.getPlayer()).thenReturn(owner); when(event.getAction()).thenReturn(Action.RIGHT_CLICK_BLOCK); when(event.getClickedBlock()).thenReturn(clicked); - when(event.getItem()).thenReturn(new ItemStack(Material.FLINT_AND_STEEL)); + when(event.getItem()).thenReturn(activator); when(world.getUID()).thenReturn(worldId); when(world.getName()).thenReturn("pocket"); when(world.getBlockAt(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> { @@ -227,6 +230,12 @@ final class PocketBaseControllerTest { verify(event).setCancelled(true); } + private static ItemStack item(Material material) { + ItemStack item = mock(ItemStack.class); + when(item.getType()).thenReturn(material); + return item; + } + private static Block block( World world, int x,