Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ paperweight.reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArt
group = "world.bentobox" // From <groupId>

// Base properties from <properties>
val buildVersion = "3.17.0"
val buildVersion = "3.17.1"
val buildNumberDefault = "-LOCAL" // Local build identifier
val snapshotSuffix = "-SNAPSHOT" // Indicates development/snapshot version

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public void setup() {
new AdminRangeResetCommand(this);
new AdminRangeAddCommand(this);
new AdminRangeRemoveCommand(this);
new AdminRangeRemoveBonusCommand(this);
new AdminRangePurgeBonusCommand(this);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package world.bentobox.bentobox.api.commands.admin.range;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;

import org.bukkit.Bukkit;
import org.eclipse.jdt.annotation.Nullable;

import world.bentobox.bentobox.api.commands.CompositeCommand;
import world.bentobox.bentobox.api.events.island.IslandEvent;
import world.bentobox.bentobox.api.localization.TextVariables;
import world.bentobox.bentobox.api.user.User;
import world.bentobox.bentobox.database.objects.BonusRangeRecord;
import world.bentobox.bentobox.database.objects.Island;
import world.bentobox.bentobox.util.Util;

/**
* Admin command to remove every bonus range carrying a given id from <b>all</b>
* islands in this gamemode's world.
* <p>
* Addons tag the bonus ranges they grant with their own name (the bonus
* {@code uniqueId}); for example the Upgrades addon stores them under the id
* {@code "Upgrades"}. When such an addon is removed, the bonus ranges it added
* remain on every island it ever touched. This command purges them in one go.
* <p>
* Because a server can hold a large number of islands, the scan that finds the
* affected islands runs asynchronously (off the main thread). The admin is then
* shown the count and must re-run the command with {@code confirm} to apply it.
* The actual mutation and event firing happen back on the main thread, on the
* live cached island instances.
*
* @author tastybento
* @since 3.17.1
*/
public class AdminRangePurgeBonusCommand extends CompositeCommand {

/** True while an async scan is running, to prevent overlapping runs. */
volatile boolean inPurge;
/** True once a scan has found islands and is awaiting a {@code confirm}. */
boolean toBeConfirmed;
/** The bonus id the pending confirmation is for. */
@Nullable
String pendingId;
/** The unique ids of the islands the pending confirmation will purge. */
List<String> pendingIslandIds = List.of();

/**
* Admin command to remove a bonus range id from every island.
* @param parent - parent range command
*/
public AdminRangePurgeBonusCommand(CompositeCommand parent) {
super(parent, "purgebonus");
}

@Override
public void setup() {
setPermission("admin.range.purgebonus");
setOnlyPlayer(false);
setParametersHelp("commands.admin.range.purgebonus.parameters");
setDescription("commands.admin.range.purgebonus.description");
}

@Override
public boolean canExecute(User user, String label, List<String> args) {
if (inPurge) {
user.sendMessage("commands.admin.range.purgebonus.in-progress");
return false;
}
// Expect "<id>" or "<id> confirm"
if (args.isEmpty() || args.size() > 2
|| (args.size() == 2 && !args.get(1).equalsIgnoreCase("confirm"))) {

Check failure on line 73 in src/main/java/world/bentobox/bentobox/api/commands/admin/range/AdminRangePurgeBonusCommand.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "confirm" 3 times.

See more on https://sonarcloud.io/project/issues?id=BentoBoxWorld_BentoBox&issues=AZ6A0n67Qzzn1nxVxX_7&open=AZ6A0n67Qzzn1nxVxX_7&pullRequest=2988
showHelp(this, user);
return false;
}
return true;
}

@Override
public boolean execute(User user, String label, List<String> args) {

Check failure on line 81 in src/main/java/world/bentobox/bentobox/api/commands/admin/range/AdminRangePurgeBonusCommand.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to not always return the same value.

See more on https://sonarcloud.io/project/issues?id=BentoBoxWorld_BentoBox&issues=AZ6A0n67Qzzn1nxVxX_6&open=AZ6A0n67Qzzn1nxVxX_6&pullRequest=2988
String id = args.get(0);
boolean confirm = args.size() == 2 && args.get(1).equalsIgnoreCase("confirm");
// Apply a pending purge if this is the matching confirmation
if (confirm && toBeConfirmed && id.equals(pendingId)) {
List<String> ids = pendingIslandIds;
toBeConfirmed = false;
pendingId = null;
pendingIslandIds = List.of();
applyPurge(user, id, ids);
return true;
}
// Otherwise (re)scan asynchronously and prompt for confirmation
inPurge = true;
getPlugin().getIslands().getIslandsASync().thenAccept(all -> {
List<String> ids = findIslandIds(all, id);
Bukkit.getScheduler().runTask(getPlugin(), () -> {
inPurge = false;
if (ids.isEmpty()) {
user.sendMessage("commands.admin.range.purgebonus.none", "[id]", id);
return;
}
pendingId = id;
pendingIslandIds = ids;
toBeConfirmed = true;
user.sendMessage("commands.admin.range.purgebonus.warning", "[id]", id, TextVariables.NUMBER,
String.valueOf(ids.size()));
user.sendMessage("commands.admin.range.purgebonus.confirm");
});
}).exceptionally(ex -> {
getPlugin().logStacktrace(ex);
Bukkit.getScheduler().runTask(getPlugin(), () -> {
inPurge = false;
user.sendMessage("commands.admin.range.purgebonus.failed");
});
return null;
});
return true;
}

/**
* @return the unique ids of the islands in this world that carry a bonus range
* with the given id.
*/
List<String> findIslandIds(Collection<Island> islands, String id) {
return islands.stream().filter(i -> getWorld().equals(i.getWorld()))
.filter(i -> i.getBonusRangeRecord(id).isPresent()).map(Island::getUniqueId).toList();
}

/**
* Removes the bonus range id from every still-matching island, firing a range
* change event per island whose effective protection range changed. Runs on the
* main thread and operates on the live cached island instances, which are
* persisted automatically via {@code setChanged()}.
*
* @param user the admin running the command
* @param id the bonus range uniqueId to purge
* @param ids the unique ids of the islands found during the async scan
*/
void applyPurge(User user, String id, List<String> ids) {
int changed = 0;
for (String uid : ids) {
Island island = getIslands().getIslandById(uid).orElse(null);
// Re-check on the live instance in case it changed since the scan
if (island == null || island.getBonusRangeRecord(id).isEmpty()) {
continue;
}
int oldRange = island.getProtectionRange();
island.clearBonusRange(id);
int newRange = island.getProtectionRange();
if (oldRange != newRange) {
IslandEvent.builder()
.island(island)
.location(island.getCenter())
.reason(IslandEvent.Reason.RANGE_CHANGE)
.involvedPlayer(island.getOwner())
.admin(true)
.protectionRange(newRange, oldRange)
.build();
}
changed++;
}
getPlugin().log("Purged bonus range '" + id + "' from " + changed + " island(s) in "
+ getWorld().getName());
user.sendMessage("commands.admin.range.purgebonus.success", "[id]", id, TextVariables.NUMBER,
String.valueOf(changed));
}

@Override
public Optional<List<String>> tabComplete(User user, String alias, List<String> args) {
if (args.size() <= 1) {
String lastArg = !args.isEmpty() ? args.getLast() : "";
// Suggest the bonus ids that exist on currently-loaded islands in this world
Collection<Island> cached = getIslands().getIslandCache().getCachedIslands();
List<String> ids = cached.stream().filter(i -> getWorld().equals(i.getWorld()))
.flatMap(i -> i.getBonusRanges().stream()).map(BonusRangeRecord::getUniqueId).distinct().sorted()
.toList();
return Optional.of(Util.tabLimit(new ArrayList<>(ids), lastArg));
} else if (args.size() == 2) {
return Optional.of(Util.tabLimit(new ArrayList<>(List.of("confirm")), args.getLast()));
}
return Optional.empty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package world.bentobox.bentobox.api.commands.admin.range;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;

import org.eclipse.jdt.annotation.Nullable;

import world.bentobox.bentobox.api.commands.CompositeCommand;
import world.bentobox.bentobox.api.commands.ConfirmableCommand;
import world.bentobox.bentobox.api.events.island.IslandEvent;
import world.bentobox.bentobox.api.localization.TextVariables;
import world.bentobox.bentobox.api.user.User;
import world.bentobox.bentobox.database.objects.BonusRangeRecord;
import world.bentobox.bentobox.database.objects.Island;
import world.bentobox.bentobox.util.Util;

/**
* Admin command to remove bonus ranges from a player's island.
* <p>
* Bonus ranges are static values added to (or subtracted from) the protection
* range of an island. They are stored in the {@link Island} as
* {@link BonusRangeRecord}s and are typically granted by addons. Admins need a
* way to clear them - for example after the addon that granted them has been
* removed.
* <p>
* Usage: {@code /<admin> range removebonus <player> [id]}. With no id, every
* bonus range is removed. With an id, only the bonus ranges sharing that unique
* id are removed. The available ids are offered through tab completion so the
* admin does not have to guess them.
*
* @author tastybento
* @since 3.17.1
*/
public class AdminRangeRemoveBonusCommand extends ConfirmableCommand {

private Island targetIsland;
private @Nullable UUID targetUUID;
private @Nullable String bonusId;

/**
* Admin command to remove bonus ranges from a player's island.
* @param parent - parent range command
*/
public AdminRangeRemoveBonusCommand(CompositeCommand parent) {
super(parent, "removebonus");
}

@Override
public void setup() {
setPermission("admin.range.removebonus");
setOnlyPlayer(false);
setParametersHelp("commands.admin.range.removebonus.parameters");
setDescription("commands.admin.range.removebonus.description");
}

@Override
public boolean canExecute(User user, String label, List<String> args) {
// Expect the player's name and an optional bonus id
if (args.isEmpty() || args.size() > 2) {
showHelp(this, user);
return false;
}
// Get target player
targetUUID = Util.getUUID(args.get(0));
if (targetUUID == null) {
user.sendMessage("general.errors.unknown-player", TextVariables.NAME, args.get(0));
return false;
}
// Target must have an island in this world
targetIsland = getIslands().getIsland(getWorld(), targetUUID);
if (targetIsland == null) {
user.sendMessage("general.errors.player-has-no-island");
return false;
}
// Nothing to do if there are no bonus ranges at all
if (targetIsland.getBonusRanges().isEmpty()) {
user.sendMessage("commands.admin.range.removebonus.no-bonus");
return false;
}
// A specific bonus id was supplied - it must exist on this island
bonusId = args.size() == 2 ? args.get(1) : null;
if (bonusId != null && targetIsland.getBonusRangeRecord(bonusId).isEmpty()) {
user.sendMessage("commands.admin.range.removebonus.unknown-bonus", "[id]", bonusId);
return false;
}
return true;
}

@Override
public boolean execute(User user, String label, List<String> args) {
Objects.requireNonNull(targetIsland);
Objects.requireNonNull(targetUUID);
askConfirmation(user, () -> removeBonusRanges(user, args.get(0)));
return true;
}

/**
* Removes the bonus range(s) from the target island. If {@link #bonusId} is
* set, only that id's bonus ranges are removed, otherwise all of them are.
* Fires a range change event if the effective protection range changed and
* notifies the user. The island is persisted automatically via {@code setChanged()}.
*
* @param user the admin running the command
* @param name the target player's name (for the feedback message)
*/
void removeBonusRanges(User user, String name) {
int oldRange = targetIsland.getProtectionRange();

int removed;
if (bonusId == null) {
// Remove every bonus range
removed = targetIsland.getBonusRanges().stream().mapToInt(BonusRangeRecord::getRange).sum();
targetIsland.clearAllBonusRanges();
user.sendMessage("commands.admin.range.removebonus.success", TextVariables.NUMBER,
String.valueOf(removed), TextVariables.NAME, name);
} else {
// Remove only the bonus ranges for the given id
removed = targetIsland.getBonusRange(bonusId);
targetIsland.clearBonusRange(bonusId);
user.sendMessage("commands.admin.range.removebonus.success-id", "[id]", bonusId,
TextVariables.NUMBER, String.valueOf(removed), TextVariables.NAME, name);
}

// The effective protection range may have changed - notify addons (not cancellable)
int newRange = targetIsland.getProtectionRange();
if (oldRange != newRange) {
IslandEvent.builder()
.island(targetIsland)
.location(targetIsland.getCenter())
.reason(IslandEvent.Reason.RANGE_CHANGE)
.involvedPlayer(targetUUID)
.admin(true)
.protectionRange(newRange, oldRange)
.build();
}
}

@Override
public Optional<List<String>> tabComplete(User user, String alias, List<String> args) {
String lastArg = !args.isEmpty() ? args.getLast() : "";
if (args.size() <= 1) {
// Don't show every player on the server. Require at least the first letter
if (lastArg.isEmpty()) {
return Optional.empty();
}
return Optional.of(Util.tabLimit(new ArrayList<>(Util.getOnlinePlayerList(user)), lastArg));
} else if (args.size() == 2) {
// Offer the bonus ids that exist on the target player's island
UUID uuid = Util.getUUID(args.get(0));
if (uuid != null) {
Island island = getIslands().getIsland(getWorld(), uuid);
if (island != null) {
List<String> ids = island.getBonusRanges().stream().map(BonusRangeRecord::getUniqueId).distinct()
.toList();
return Optional.of(Util.tabLimit(new ArrayList<>(ids), lastArg));
}
}
}
return Optional.empty();
}
}
16 changes: 16 additions & 0 deletions src/main/resources/locales/cs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,22 @@ commands:
success: >-
<green>Úspěšně zmenšena chráněná oblast ostrova hráče </green><aqua>[name]</aqua><green>na </green><aqua></aqua>
[total] <gray>(</gray><aqua>-[number]</aqua><gray>)</gray><green>.</green>
removebonus:
parameters: '<hráč> [id]'
description: 'odebere bonusové rozsahy z ostrova (všechny nebo jeden podle id)'
no-bonus: '<red>Tento ostrov nemá žádné bonusové rozsahy k odebrání!</red>'
unknown-bonus: '<red>Na tomto ostrově neexistuje bonusový rozsah s id </red><aqua>[id]</aqua><red>!</red>'
success: '<green>Odebráno </green><aqua>[number]</aqua><green> z bonusového rozsahu ostrova hráče </green><aqua>[name]</aqua><green>.</green>'
success-id: '<green>Odebrán bonusový rozsah </green><aqua>[id]</aqua><green> (</green><aqua>[number]</aqua><green>) z ostrova hráče </green><aqua>[name]</aqua><green>.</green>'
purgebonus:
parameters: '<id>'
description: 'odebere bonusový rozsah s daným id ze všech ostrovů (např. po odebrání addonu, který jej přidal)'
none: '<red>Žádný ostrov nemá bonusový rozsah s id </red><aqua>[id]</aqua><red>!</red>'
warning: '<gold>Tímto odeberete bonusový rozsah </gold><aqua>[id]</aqua><gold> z </gold><aqua>[number]</aqua><gold> ostrovů.</gold>'
success: '<green>Odebrán bonusový rozsah </green><aqua>[id]</aqua><green> z </green><aqua>[number]</aqua><green> ostrovů.</green>'
confirm: '<gold>Spusťte příkaz znovu s </gold><aqua>confirm</aqua><gold> pro potvrzení.</gold>'
in-progress: '<red>Čištění bonusových rozsahů již probíhá. Počkejte prosím.</red>'
failed: '<red>Skenování ostrovů selhalo. Zkontrolujte konzoli.</red>'
register:
parameters: <Player>
description: registrovat hráče na nevlastněný ostrov, na kterém se nacházíš
Expand Down
Loading
Loading