33import java .util .ArrayList ;
44import java .util .Collection ;
55import java .util .List ;
6- import java .util .Objects ;
76import java .util .Optional ;
87
8+ import org .bukkit .Bukkit ;
99import org .eclipse .jdt .annotation .Nullable ;
1010
1111import world .bentobox .bentobox .api .commands .CompositeCommand ;
12- import world .bentobox .bentobox .api .commands .ConfirmableCommand ;
1312import world .bentobox .bentobox .api .events .island .IslandEvent ;
1413import world .bentobox .bentobox .api .localization .TextVariables ;
1514import world .bentobox .bentobox .api .user .User ;
2524 * {@code uniqueId}); for example the Upgrades addon stores them under the id
2625 * {@code "Upgrades"}. When such an addon is removed, the bonus ranges it added
2726 * remain on every island it ever touched. This command purges them in one go.
27+ * <p>
28+ * Because a server can hold a large number of islands, the scan that finds the
29+ * affected islands runs asynchronously (off the main thread). The admin is then
30+ * shown the count and must re-run the command with {@code confirm} to apply it.
31+ * The actual mutation and event firing happen back on the main thread, on the
32+ * live cached island instances.
2833 *
2934 * @author tastybento
3035 * @since 3.17.1
3136 */
32- public class AdminRangePurgeBonusCommand extends ConfirmableCommand {
37+ public class AdminRangePurgeBonusCommand extends CompositeCommand {
3338
34- private @ Nullable String bonusId ;
39+ /** True while an async scan is running, to prevent overlapping runs. */
40+ volatile boolean inPurge ;
41+ /** True once a scan has found islands and is awaiting a {@code confirm}. */
42+ boolean toBeConfirmed ;
43+ /** The bonus id the pending confirmation is for. */
44+ @ Nullable
45+ String pendingId ;
46+ /** The unique ids of the islands the pending confirmation will purge. */
47+ List <String > pendingIslandIds = List .of ();
3548
3649 /**
3750 * Admin command to remove a bonus range id from every island.
@@ -51,43 +64,87 @@ public void setup() {
5164
5265 @ Override
5366 public boolean canExecute (User user , String label , List <String > args ) {
54- // A single bonus id is expected
55- if (args .size () != 1 ) {
56- showHelp (this , user );
67+ if (inPurge ) {
68+ user .sendMessage ("commands.admin.range.purgebonus.in-progress" );
5769 return false ;
5870 }
59- bonusId = args . get ( 0 );
60- // There must be at least one island carrying this bonus id
61- if ( islandsWithBonus ( bonusId ). isEmpty ( )) {
62- user . sendMessage ( "commands.admin.range.purgebonus.none" , "[id]" , bonusId );
71+ // Expect "<id>" or "<id> confirm"
72+ if ( args . isEmpty () || args . size () > 2
73+ || ( args . size () == 2 && ! args . get ( 1 ). equalsIgnoreCase ( "confirm" ) )) {
74+ showHelp ( this , user );
6375 return false ;
6476 }
6577 return true ;
6678 }
6779
6880 @ Override
6981 public boolean execute (User user , String label , List <String > args ) {
70- Objects .requireNonNull (bonusId );
71- // Warn how many islands will be affected before the admin confirms
72- int count = islandsWithBonus (bonusId ).size ();
73- user .sendMessage ("commands.admin.range.purgebonus.warning" , "[id]" , bonusId , TextVariables .NUMBER ,
74- String .valueOf (count ));
75- final String id = bonusId ;
76- askConfirmation (user , () -> purge (user , id ));
82+ String id = args .get (0 );
83+ boolean confirm = args .size () == 2 && args .get (1 ).equalsIgnoreCase ("confirm" );
84+ // Apply a pending purge if this is the matching confirmation
85+ if (confirm && toBeConfirmed && id .equals (pendingId )) {
86+ List <String > ids = pendingIslandIds ;
87+ toBeConfirmed = false ;
88+ pendingId = null ;
89+ pendingIslandIds = List .of ();
90+ applyPurge (user , id , ids );
91+ return true ;
92+ }
93+ // Otherwise (re)scan asynchronously and prompt for confirmation
94+ inPurge = true ;
95+ getPlugin ().getIslands ().getIslandsASync ().thenAccept (all -> {
96+ List <String > ids = findIslandIds (all , id );
97+ Bukkit .getScheduler ().runTask (getPlugin (), () -> {
98+ inPurge = false ;
99+ if (ids .isEmpty ()) {
100+ user .sendMessage ("commands.admin.range.purgebonus.none" , "[id]" , id );
101+ return ;
102+ }
103+ pendingId = id ;
104+ pendingIslandIds = ids ;
105+ toBeConfirmed = true ;
106+ user .sendMessage ("commands.admin.range.purgebonus.warning" , "[id]" , id , TextVariables .NUMBER ,
107+ String .valueOf (ids .size ()));
108+ user .sendMessage ("commands.admin.range.purgebonus.confirm" );
109+ });
110+ }).exceptionally (ex -> {
111+ getPlugin ().logStacktrace (ex );
112+ Bukkit .getScheduler ().runTask (getPlugin (), () -> {
113+ inPurge = false ;
114+ user .sendMessage ("commands.admin.range.purgebonus.failed" );
115+ });
116+ return null ;
117+ });
77118 return true ;
78119 }
79120
80121 /**
81- * Removes the bonus range id from every island that carries it, firing a range
82- * change event per island whose effective protection range changed. Each island
83- * is persisted automatically via {@code setChanged()}.
122+ * @return the unique ids of the islands in this world that carry a bonus range
123+ * with the given id.
124+ */
125+ List <String > findIslandIds (Collection <Island > islands , String id ) {
126+ return islands .stream ().filter (i -> getWorld ().equals (i .getWorld ()))
127+ .filter (i -> i .getBonusRangeRecord (id ).isPresent ()).map (Island ::getUniqueId ).toList ();
128+ }
129+
130+ /**
131+ * Removes the bonus range id from every still-matching island, firing a range
132+ * change event per island whose effective protection range changed. Runs on the
133+ * main thread and operates on the live cached island instances, which are
134+ * persisted automatically via {@code setChanged()}.
84135 *
85136 * @param user the admin running the command
86137 * @param id the bonus range uniqueId to purge
138+ * @param ids the unique ids of the islands found during the async scan
87139 */
88- void purge (User user , String id ) {
89- int islandsChanged = 0 ;
90- for (Island island : islandsWithBonus (id )) {
140+ void applyPurge (User user , String id , List <String > ids ) {
141+ int changed = 0 ;
142+ for (String uid : ids ) {
143+ Island island = getIslands ().getIslandById (uid ).orElse (null );
144+ // Re-check on the live instance in case it changed since the scan
145+ if (island == null || island .getBonusRangeRecord (id ).isEmpty ()) {
146+ continue ;
147+ }
91148 int oldRange = island .getProtectionRange ();
92149 island .clearBonusRange (id );
93150 int newRange = island .getProtectionRange ();
@@ -101,21 +158,12 @@ void purge(User user, String id) {
101158 .protectionRange (newRange , oldRange )
102159 .build ();
103160 }
104- islandsChanged ++;
161+ changed ++;
105162 }
106- getPlugin ().log ("Purged bonus range '" + id + "' from " + islandsChanged + " island(s) in "
163+ getPlugin ().log ("Purged bonus range '" + id + "' from " + changed + " island(s) in "
107164 + getWorld ().getName ());
108165 user .sendMessage ("commands.admin.range.purgebonus.success" , "[id]" , id , TextVariables .NUMBER ,
109- String .valueOf (islandsChanged ));
110- }
111-
112- /**
113- * @return the islands in this world that carry a bonus range with the given id.
114- * Uses the island cache so the live, canonical instances are mutated.
115- */
116- private List <Island > islandsWithBonus (String id ) {
117- return getIslands ().getIslandCache ().getIslands (getWorld ()).stream ()
118- .filter (i -> i .getBonusRangeRecord (id ).isPresent ()).toList ();
166+ String .valueOf (changed ));
119167 }
120168
121169 @ Override
@@ -128,6 +176,8 @@ public Optional<List<String>> tabComplete(User user, String alias, List<String>
128176 .flatMap (i -> i .getBonusRanges ().stream ()).map (BonusRangeRecord ::getUniqueId ).distinct ().sorted ()
129177 .toList ();
130178 return Optional .of (Util .tabLimit (new ArrayList <>(ids ), lastArg ));
179+ } else if (args .size () == 2 ) {
180+ return Optional .of (Util .tabLimit (new ArrayList <>(List .of ("confirm" )), args .getLast ()));
131181 }
132182 return Optional .empty ();
133183 }
0 commit comments