From 3532b663375be04772cc1cde8bdf1a8045f16be7 Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 10 Jul 2026 14:07:05 -0700 Subject: [PATCH] Run level-completion check when challenges are completed by admins Normal completion via TryToComplete validated level completion after marking a challenge complete, but the admin command and admin GUI called setChallengeComplete directly, so a level whose last challenge was admin-completed never registered as complete. Fixes #385 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01NKxodNE4h3TsSHMqDEeC8v --- .../commands/admin/CompleteCommand.java | 2 + .../managers/ChallengesManager.java | 105 ++++++++++++++++++ .../panel/admin/ListUsersPanel.java | 9 +- .../challenges/tasks/TryToComplete.java | 82 +++++++------- .../challenges/tasks/TryToCompleteTest.java | 12 +- 5 files changed, 161 insertions(+), 49 deletions(-) diff --git a/src/main/java/world/bentobox/challenges/commands/admin/CompleteCommand.java b/src/main/java/world/bentobox/challenges/commands/admin/CompleteCommand.java index 71f5b3a1..fb1c79a9 100644 --- a/src/main/java/world/bentobox/challenges/commands/admin/CompleteCommand.java +++ b/src/main/java/world/bentobox/challenges/commands/admin/CompleteCommand.java @@ -108,6 +108,8 @@ else if (!args.get(1).isEmpty()) this.addon.getChallengesManager().setChallengeComplete( targetUUID, this.getWorld(), challenge, user.getUniqueId()); + // Try to complete the level if all challenges are done + this.addon.getChallengesManager().tryCompleteLevelAdmin(target, this.getWorld(), challenge); if (user.isPlayer()) { diff --git a/src/main/java/world/bentobox/challenges/managers/ChallengesManager.java b/src/main/java/world/bentobox/challenges/managers/ChallengesManager.java index 7be3baa0..ffb8e6cb 100644 --- a/src/main/java/world/bentobox/challenges/managers/ChallengesManager.java +++ b/src/main/java/world/bentobox/challenges/managers/ChallengesManager.java @@ -1774,6 +1774,111 @@ public boolean validateLevelCompletion(User user, World world, ChallengeLevel le } + /** + * This method attempts to complete a level for a user by validating all challenges + * in the level are complete. If the level is completable, it marks the level as complete + * and fires a LevelCompletedEvent. + * + * @param user User who completed the level. + * @param world World where level must be completed. + * @param challenge Challenge whose level should be checked. + * @return The completed ChallengeLevel if level was completed, null otherwise. + */ + @Nullable + public ChallengeLevel tryCompleteLevel(User user, World world, Challenge challenge) + { + ChallengeLevel level = this.getCompletableLevel(user, world, challenge); + if (level != null) + { + this.setLevelComplete(user, world, level); + } + return level; + } + + + /** + * This method attempts to complete a level for a user via admin action. + * Similar to tryCompleteLevel but fires an admin LevelCompletedEvent. + * + * @param user User who had the level completed by admin. + * @param world World where level must be completed. + * @param challenge Challenge whose level should be checked. + * @return The completed ChallengeLevel if level was completed, null otherwise. + */ + @Nullable + public ChallengeLevel tryCompleteLevelAdmin(User user, World world, Challenge challenge) + { + ChallengeLevel level = this.getCompletableLevel(user, world, challenge); + if (level != null) + { + this.setLevelCompleteAdmin(user, world, level); + } + return level; + } + + + /** + * Helper method that checks if a level is completable (all challenges done, not already + * completed, and not a free level). + * + * @param user User to check for. + * @param world World to check in. + * @param challenge Challenge whose level to check. + * @return The ChallengeLevel if completable, null otherwise. + */ + @Nullable + private ChallengeLevel getCompletableLevel(User user, World world, Challenge challenge) + { + String levelID = challenge.getLevel(); + if (levelID.equals(ChallengesManager.FREE)) + { + return null; + } + + ChallengeLevel level = this.getLevel(challenge); + if (level == null) + { + return null; + } + + if (this.isLevelCompleted(user, world, level)) + { + return null; + } + + if (!this.validateLevelCompletion(user, world, level)) + { + return null; + } + + return level; + } + + + /** + * Helper method to set a level as complete by admin and fire an admin event. + * + * @param user User who had the level completed. + * @param world World where level was completed. + * @param level Level to mark as complete. + */ + private void setLevelCompleteAdmin(User user, World world, ChallengeLevel level) + { + String storageID = this.getDataUniqueID(user, Util.getWorld(world)); + + this.setLevelComplete(storageID, level.getUniqueId()); + this.addLogEntry(storageID, new LogEntry.Builder("COMPLETE_LEVEL"). + data(USER_ID, user.getUniqueId().toString()). + data("level", level.getUniqueId()).build()); + + // Fire admin event that admin completes level + Bukkit.getPluginManager().callEvent( + new LevelCompletedEvent(level.getUniqueId(), + user.getUniqueId(), + true)); + } + + /** * This method returns LevelStatus object for given challenge level. * @param uniqueId UUID of user who need to be validated. diff --git a/src/main/java/world/bentobox/challenges/panel/admin/ListUsersPanel.java b/src/main/java/world/bentobox/challenges/panel/admin/ListUsersPanel.java index fd31eefc..9a29dcf7 100644 --- a/src/main/java/world/bentobox/challenges/panel/admin/ListUsersPanel.java +++ b/src/main/java/world/bentobox/challenges/panel/admin/ListUsersPanel.java @@ -222,11 +222,16 @@ protected PanelItem createElementButton(Player player) (status, valueSet) -> { if (Boolean.TRUE.equals(status)) { - valueSet.forEach(challenge -> + valueSet.forEach(challenge -> { manager.setChallengeComplete(player.getUniqueId(), this.world, challenge, - this.user.getUniqueId())); + this.user.getUniqueId()); + // Try to complete the level if all challenges are done + manager.tryCompleteLevelAdmin(User.getInstance(player.getUniqueId()), + this.world, + challenge); + }); } this.build(); diff --git a/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java b/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java index 30071ff5..1281baf5 100644 --- a/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java +++ b/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java @@ -364,60 +364,54 @@ ChallengeResult build(int maxTimes) this.manager.setChallengeComplete(this.user, this.world, this.challenge, result.getFactor()); // Check level completion for non-free challenges - if (!result.wasCompleted() && - !this.challenge.getLevel().equals(ChallengesManager.FREE)) + if (!result.wasCompleted()) { - ChallengeLevel level = this.manager.getLevel(this.challenge); + ChallengeLevel level = this.manager.tryCompleteLevel(this.user, this.world, this.challenge); - if (level != null && !this.manager.isLevelCompleted(this.user, this.world, level)) + if (level != null) { - if (this.manager.validateLevelCompletion(this.user, this.world, level)) + // Item rewards + for (ItemStack reward : level.getRewardItems()) { - // Item rewards - for (ItemStack reward : level.getRewardItems()) - { - // Clone is necessary because otherwise it will chane reward itemstack - // amount. - this.user.getInventory().addItem(reward.clone()).forEach((k, v) -> - this.user.getWorld().dropItem(this.user.getLocation(), v)); - } - - // Money Reward - if (this.addon.isEconomyProvided()) - { - this.addon.getEconomyProvider().deposit(this.user, level.getRewardMoney()); - } + // Clone is necessary because otherwise it will chane reward itemstack + // amount. + this.user.getInventory().addItem(reward.clone()).forEach((k, v) -> + this.user.getWorld().dropItem(this.user.getLocation(), v)); + } - // Experience Reward - this.user.getPlayer().giveExp(level.getRewardExperience()); + // Money Reward + if (this.addon.isEconomyProvided()) + { + this.addon.getEconomyProvider().deposit(this.user, level.getRewardMoney()); + } - // Run commands - this.runCommands(level.getRewardCommands()); + // Experience Reward + this.user.getPlayer().giveExp(level.getRewardExperience()); - Utils.sendMessage(this.user, - this.world, Constants.MESSAGES + "you-completed-level", Constants.PARAMETER_VALUE, - level.getFriendlyName()); + // Run commands + this.runCommands(level.getRewardCommands()); - if (this.addon.getChallengesSettings().isBroadcastMessages()) - { - Bukkit.getOnlinePlayers().stream(). - map(User::getInstance).forEach(user -> Utils.sendMessage(user, - this.world, - Constants.MESSAGES + "name-has-completed-level", - Constants.PARAMETER_NAME, this.user.getName(), - Constants.PARAMETER_VALUE, level.getFriendlyName())); - } + Utils.sendMessage(this.user, + this.world, Constants.MESSAGES + "you-completed-level", Constants.PARAMETER_VALUE, + level.getFriendlyName()); - this.manager.setLevelComplete(this.user, this.world, level); + if (this.addon.getChallengesSettings().isBroadcastMessages()) + { + Bukkit.getOnlinePlayers().stream(). + map(User::getInstance).forEach(user -> Utils.sendMessage(user, + this.world, + Constants.MESSAGES + "name-has-completed-level", + Constants.PARAMETER_NAME, this.user.getName(), + Constants.PARAMETER_VALUE, level.getFriendlyName())); + } - // sends title to player on level completion - if (this.addon.getChallengesSettings().isShowCompletionTitle()) - { - this.user.getPlayer().sendTitle( - this.parseLevel(this.user.getTranslation("challenges.titles.level-title"), level), - this.parseLevel(this.user.getTranslation("challenges.titles.level-subtitle"), level), - 10, this.addon.getChallengesSettings().getTitleShowtime(), 20); - } + // sends title to player on level completion + if (this.addon.getChallengesSettings().isShowCompletionTitle()) + { + this.user.getPlayer().sendTitle( + this.parseLevel(this.user.getTranslation("challenges.titles.level-title"), level), + this.parseLevel(this.user.getTranslation("challenges.titles.level-subtitle"), level), + 10, this.addon.getChallengesSettings().getTitleShowtime(), 20); } } } diff --git a/src/test/java/world/bentobox/challenges/tasks/TryToCompleteTest.java b/src/test/java/world/bentobox/challenges/tasks/TryToCompleteTest.java index 3a0a259c..1d616e51 100644 --- a/src/test/java/world/bentobox/challenges/tasks/TryToCompleteTest.java +++ b/src/test/java/world/bentobox/challenges/tasks/TryToCompleteTest.java @@ -809,14 +809,17 @@ void testLevelCompletionTriggered() { lvl.setFriendlyName("Novice"); lvl.setRewardExperience(200); // Stub both overloads: getLevel(String) used in checkIfCanCompleteChallenge, - // getLevel(Challenge) used in build() for level completion check + // getLevel(Challenge) used in tryCompleteLevel() when(cm.getLevel(GAME_MODE_NAME + "_novice")).thenReturn(lvl); when(cm.getLevel(any(Challenge.class))).thenReturn(lvl); when(cm.isLevelCompleted(any(), any(), any())).thenReturn(false); when(cm.validateLevelCompletion(any(), any(), any())).thenReturn(true); + // Mock tryCompleteLevel to return the level (which triggers reward logic) + when(cm.tryCompleteLevel(any(), any(), any())).thenReturn(lvl); when(inv.addItem(any())).thenReturn(new HashMap<>()); assertTrue(TryToComplete.complete(addon, user, challenge, world, topLabel, permissionPrefix)); - verify(cm).setLevelComplete(any(), any(), eq(lvl)); + // Verify that tryCompleteLevel was called to complete the level + verify(cm).tryCompleteLevel(any(), any(), eq(challenge)); verify(player).giveExp(200); } @@ -830,9 +833,12 @@ void testLevelCompletionAlreadyDone() { when(cm.getLevel(GAME_MODE_NAME + "_novice")).thenReturn(lvl); when(cm.getLevel(any(Challenge.class))).thenReturn(lvl); when(cm.isLevelCompleted(any(), any(), any())).thenReturn(true); + // Mock tryCompleteLevel to return null since level is already completed + when(cm.tryCompleteLevel(any(), any(), any())).thenReturn(null); when(inv.addItem(any())).thenReturn(new HashMap<>()); assertTrue(TryToComplete.complete(addon, user, challenge, world, topLabel, permissionPrefix)); - verify(cm, never()).setLevelComplete(any(), any(), any()); + // Verify tryCompleteLevel was called but didn't complete the level (returned null) + verify(cm).tryCompleteLevel(any(), any(), eq(challenge)); } @Test