diff --git a/src/main/java/world/bentobox/magiccobblestonegenerator/commands/admin/GeneratorAdminCommand.java b/src/main/java/world/bentobox/magiccobblestonegenerator/commands/admin/GeneratorAdminCommand.java index 7beb633..f97c652 100644 --- a/src/main/java/world/bentobox/magiccobblestonegenerator/commands/admin/GeneratorAdminCommand.java +++ b/src/main/java/world/bentobox/magiccobblestonegenerator/commands/admin/GeneratorAdminCommand.java @@ -64,6 +64,7 @@ public void setup() new ImportCommand(this.getAddon(), this); new GeneratorWhyCommand(this.getAddon(), this); new GeneratorDatabaseCommand(this.getAddon(), this); + new ResetCommand(this.getAddon(), this); } @@ -93,10 +94,86 @@ public boolean execute(User user, String label, List args) // --------------------------------------------------------------------- - // Section: Subcommadns + // Section: Subcommands // --------------------------------------------------------------------- + /** + * Base class for admin subcommands that take a single player name argument. It validates the argument, resolves the + * target player UUID and provides player name tab-completion, delegating the actual work to + * {@link #executeForTarget(User, String, UUID)}. + */ + private abstract static class PlayerTargetCommand extends ConfirmableCommand + { + protected PlayerTargetCommand(StoneGeneratorAddon addon, CompositeCommand parentCommand, String label) + { + super(addon, parentCommand, label); + } + + + @Override + public void setup() + { + // Permission and locale references are derived from the subcommand label. + this.setPermission("admin.stone-generator." + this.getLabel()); + this.setParametersHelp(Constants.ADMIN_COMMANDS + this.getLabel() + ".parameters"); + this.setDescription(Constants.ADMIN_COMMANDS + this.getLabel() + ".description"); + + this.setOnlyPlayer(false); + } + + + @Override + public boolean execute(User user, String label, List args) + { + // If args are not right, show help + if (args.size() != 1) + { + this.showHelp(this, user); + return false; + } + + // Get target + UUID targetUUID = Util.getUUID(args.get(0)); + + if (targetUUID == null) + { + Utils.sendMessage(user, + user.getTranslation("general.errors.unknown-player", TextVariables.NAME, args.get(0))); + return false; + } + + return this.executeForTarget(user, args.get(0), targetUUID); + } + + + /** + * Runs the subcommand for the resolved target player. + * + * @param user the user running the command. + * @param targetName the player name argument as typed. + * @param targetUUID the resolved target player UUID. + * @return {@code true} if the command executed successfully. + */ + protected abstract boolean executeForTarget(User user, String targetName, UUID targetUUID); + + + @Override + public Optional> tabComplete(User user, String alias, List args) + { + if (args.isEmpty()) + { + // Don't show every player on the server. Require at least the first letter + return Optional.empty(); + } + + return Optional.of(Util.tabLimit( + new ArrayList<>(Util.getOnlinePlayerList(user)), + args.get(args.size() - 1))); + } + } + + /** * This is a confirmation command for importing generators from template file. It requires confirmation as it * removes every data from database. @@ -164,7 +241,7 @@ public boolean execute(User user, String label, List args) * This is a debug command for admins. Admins could use to check which generator user is using and could faster find * an issue. */ - private static class GeneratorWhyCommand extends ConfirmableCommand + private static class GeneratorWhyCommand extends PlayerTargetCommand { /** * This is simple constructor for initializing /{admin_command} why generator command. @@ -178,58 +255,9 @@ public GeneratorWhyCommand(StoneGeneratorAddon addon, CompositeCommand parentCom } - /** - * Setups anything that is needed for this command.

It is recommended you do the following in this - * method: - *
    - *
  • Register any of the sub-commands of this command;
  • - *
  • Define the permission required to use this command using {@link - * CompositeCommand#setPermission(String)};
  • - *
  • Define whether this command can only be run by players or not using {@link - * CompositeCommand#setOnlyPlayer(boolean)};
  • - *
- */ - @Override - public void setup() - { - this.setPermission("admin.stone-generator.why"); - this.setParametersHelp(Constants.ADMIN_COMMANDS + "why.parameters"); - this.setDescription(Constants.ADMIN_COMMANDS + "why.description"); - - this.setOnlyPlayer(false); - } - - - /** - * Defines what will be executed when this command is run. - * - * @param user the {@link User} who is executing this command. - * @param label the label which has been used to execute this command. It can be {@link - * CompositeCommand#getLabel()} or an alias. - * @param args the command arguments. - * @return {@code true} if the command executed successfully, {@code false} otherwise. - */ @Override - public boolean execute(User user, String label, List args) + protected boolean executeForTarget(User user, String targetName, UUID targetUUID) { - // If args are not right, show help - if (args.size() != 1) - { - this.showHelp(this, user); - return false; - } - - // Get target - UUID targetUUID = Util.getUUID(args.get(0)); - - if (targetUUID == null) - { - Utils.sendMessage(user, - user.getTranslation("general.errors.unknown-player", - TextVariables.NAME, args.get(0))); - return false; - } - // Set meta data on player Island island = this.getAddon().getIslands().getIsland(this.getWorld(), targetUUID); @@ -281,31 +309,55 @@ public boolean execute(User user, String label, List args) return true; } + } + /** + * This command resets a single player's island generator data (unlocked, purchased and active generators) without + * touching the rest of the database. Requires confirmation as it is destructive. + */ + private static class ResetCommand extends PlayerTargetCommand + { /** - * Tab Completer for CompositeCommands. Note that any registered sub-commands will be automatically added to the - * list. Use this to add tab-complete for things like names. + * This is simple constructor for initializing /{admin_command} generator reset command. * - * @param user the {@link User} who is executing this command. - * @param alias alias for command - * @param args command arguments - * @return List of strings that could be used to complete this command. + * @param addon StoneGeneratorAddon addon. + * @param parentCommand Parent Command where we hook our command into. */ + public ResetCommand(StoneGeneratorAddon addon, CompositeCommand parentCommand) + { + super(addon, parentCommand, "reset"); + } + + @Override - public Optional> tabComplete(User user, String alias, List args) + protected boolean executeForTarget(User user, String targetName, UUID targetUUID) { - if (args.isEmpty()) - { - // Don't show every player on the server. Require at least the first letter - return Optional.empty(); - } - else + Island island = this.getAddon().getIslands().getIsland(this.getWorld(), targetUUID); + + if (island == null) { - return Optional.of(Util.tabLimit( - new ArrayList<>(Util.getOnlinePlayerList(user)), - args.get(args.size() - 1))); + Utils.sendMessage(user, user.getTranslation("general.errors.player-has-no-island")); + return false; } + + // Fall back to the typed argument if the server cannot resolve a name for the UUID. + final String resolvedName = this.getPlayers().getName(targetUUID); + final String displayName = resolvedName == null || resolvedName.isEmpty() ? targetName : resolvedName; + + this.askConfirmation(user, + user.getTranslation(Constants.CONVERSATIONS + "prefix") + + user.getTranslation(Constants.ADMIN_COMMANDS + "reset.confirmation", + Constants.PLAYER, displayName), + () -> + { + this.getAddon().getAddonManager().resetIslandData(island); + Utils.sendMessage(user, + user.getTranslation(Constants.MESSAGES + "generator-data-reset", + Constants.PLAYER, displayName)); + }); + + return true; } } } diff --git a/src/main/java/world/bentobox/magiccobblestonegenerator/managers/StoneGeneratorManager.java b/src/main/java/world/bentobox/magiccobblestonegenerator/managers/StoneGeneratorManager.java index bfd3e1e..f02dbc5 100644 --- a/src/main/java/world/bentobox/magiccobblestonegenerator/managers/StoneGeneratorManager.java +++ b/src/main/java/world/bentobox/magiccobblestonegenerator/managers/StoneGeneratorManager.java @@ -1448,6 +1448,24 @@ public void wipeGeneratorData(GeneratorDataObject dataObject) { this.wipeGeneratorData(dataObject.getUniqueId()); } + /** + * This method resets all generator data for the given island: its stored data is removed and a fresh default data + * object is recreated. Unlocked, purchased and active generators are cleared (#149). + * + * @param island Island whose generator data must be reset. + */ + public void resetIslandData(@NotNull Island island) { + this.wipeGeneratorData(island.getUniqueId()); + // Recreate a fresh, default data object so the island keeps working immediately. addIslandData works even + // for ownerless islands (e.g. spawn), unlike validateIslandData which returns early when there is no owner. + this.addIslandData(island); + + if (island.getOwner() != null) { + // For owned islands, also re-apply owner bundles/limits and re-evaluate unlocks. + this.validateIslandData(island); + } + } + // --------------------------------------------------------------------- // Section: Methods // --------------------------------------------------------------------- diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index 88714fe..2570868 100644 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -11,6 +11,10 @@ stone-generator: why: parameters: "" description: "toggles Magic Cobblestone Generator debug messages" + reset: + parameters: "" + description: "resets a player's island generator data" + confirmation: "This will reset [player]'s generator data (unlocked, purchased and active generators) - please confirm" database: description: "Main database command" import-database: @@ -999,6 +1003,8 @@ stone-generator: generator-exhausted: "Generator '[generator]' has reached its generation limit and is now on cooldown for [number] more minute(s)." # Message that appears when purchasing the generator. generator-purchased: "Generator '[generator]' is purchased." + # Message that appears after an admin resets a player's generator data. + generator-data-reset: "Generator data for [player] has been reset." # Message that appears when trying to buy already purchased generator. generator-already-purchased: "Generator '[generator]' is already purchased." # Message that appears when trying to buy generator without required island level diff --git a/src/test/java/world/bentobox/magiccobblestonegenerator/commands/admin/GeneratorAdminCommandTest.java b/src/test/java/world/bentobox/magiccobblestonegenerator/commands/admin/GeneratorAdminCommandTest.java index e4f6dae..e75d739 100644 --- a/src/test/java/world/bentobox/magiccobblestonegenerator/commands/admin/GeneratorAdminCommandTest.java +++ b/src/test/java/world/bentobox/magiccobblestonegenerator/commands/admin/GeneratorAdminCommandTest.java @@ -41,6 +41,7 @@ class GeneratorAdminCommandTest extends CommonTestSetup { private Settings settings; private CompositeCommand ic; private CompositeCommand why; + private CompositeCommand reset; @Override @BeforeEach @@ -78,6 +79,7 @@ public void setUp() throws Exception { gac = new GeneratorAdminCommand(addon, ac); ic = gac.getSubCommand("import").get(); why = gac.getSubCommand("why").get(); + reset = gac.getSubCommand("reset").get(); } @Test @@ -130,7 +132,7 @@ void testExecuteUserStringListOfStringHelp() { "stone-generator.commands.admin.main.parameters"); verify(user).getTranslation( "stone-generator.commands.admin.main.description"); - verify(user, times(4)).isPlayer(); + verify(user, times(5)).isPlayer(); verify(user).sendMessage( "commands.help.syntax-no-parameters", "[usage]", @@ -155,6 +157,12 @@ void testExecuteUserStringListOfStringHelp() { "/null generator database", "[description]", "stone-generator.commands.admin.database.description"); + verify(user).sendMessage( + "commands.help.syntax-no-parameters", + "[usage]", + "/null generator reset", + "[description]", + "stone-generator.commands.admin.reset.description"); verify(user).sendMessage( "commands.help.end"); } @@ -181,4 +189,35 @@ void testExecuteUserStringListOfStringWhyPlayer() { "stone-generator.conversations.prefixgeneral.errors.player-is-not-owner"); } + @Test + void testSetupReset() { + assertEquals("admin.stone-generator.reset", reset.getPermission()); + assertEquals("stone-generator.commands.admin.reset.parameters", reset.getParameters()); + assertEquals("stone-generator.commands.admin.reset.description", reset.getDescription()); + assertFalse(reset.isOnlyPlayer()); + } + + @Test + void testExecuteResetNoArgs() { + assertFalse(reset.execute(user, "bskyblock", List.of())); + verify(user).sendMessage( + "commands.help.header", + "[label]", + "BSkyBlock World"); + verify(user).getTranslationOrNothing( + "stone-generator.commands.admin.reset.parameters"); + verify(user).getTranslation( + "stone-generator.commands.admin.reset.description"); + verify(user).sendMessage( + "commands.help.end"); + } + + @Test + void testExecuteResetPlayerNoIsland() { + // Target resolves to a UUID but has no island in this world. + assertFalse(reset.execute(user, "bskyblock", List.of("tastybento"))); + verify(user).sendMessage( + "stone-generator.conversations.prefixgeneral.errors.player-has-no-island"); + } + } diff --git a/src/test/java/world/bentobox/magiccobblestonegenerator/managers/StoneGeneratorManagerTest.java b/src/test/java/world/bentobox/magiccobblestonegenerator/managers/StoneGeneratorManagerTest.java index 8f3b76a..abfa428 100644 --- a/src/test/java/world/bentobox/magiccobblestonegenerator/managers/StoneGeneratorManagerTest.java +++ b/src/test/java/world/bentobox/magiccobblestonegenerator/managers/StoneGeneratorManagerTest.java @@ -710,6 +710,14 @@ void testWipeGeneratorDataString() { assertDoesNotThrow(() -> sgm.wipeGeneratorData(uuid.toString())); } + @Test + void testResetIslandData() { + when(island.getUniqueId()).thenReturn("island-149"); + assertDoesNotThrow(() -> sgm.resetIslandData(island)); + // The island's stored data is deleted as part of the reset. + verify(h).deleteID("island-149"); + } + @Test void testWipeGeneratorDataGeneratorDataObject() { assertDoesNotThrow(() -> sgm.wipeGeneratorData(generatorData));