Skip to content

Commit c5b1a90

Browse files
authored
Merge pull request #2541 from BentoBoxWorld/better_purging
Better purging
2 parents 9a24ee9 + 2cb0651 commit c5b1a90

27 files changed

+321
-69
lines changed

pom.xml

+2-2
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,10 @@
7373
<postgresql.version>42.2.18</postgresql.version>
7474
<hikaricp.version>5.0.1</hikaricp.version>
7575
<!-- More visible way to change dependency versions -->
76-
<spigot.version>1.20.5-R0.1-SNAPSHOT</spigot.version>
76+
<spigot.version>1.21.3-R0.1-SNAPSHOT</spigot.version>
7777
<!-- Might differ from the last Spigot release for short periods
7878
of time -->
79-
<paper.version>1.20.6-R0.1-SNAPSHOT</paper.version>
79+
<paper.version>1.21.1-R0.1-SNAPSHOT</paper.version>
8080
<bstats.version>3.0.0</bstats.version>
8181
<vault.version>1.7.1</vault.version>
8282
<placeholderapi.version>2.10.9</placeholderapi.version>

src/main/java/world/bentobox/bentobox/BentoBox.java

+1
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ private void completeSetup(long loadTime) {
207207
registerListeners();
208208

209209
// Load islands from database - need to wait until all the worlds are loaded
210+
log("Loading islands from database...");
210211
try {
211212
islandsManager.load();
212213
} catch (Exception e) {

src/main/java/world/bentobox/bentobox/api/commands/admin/purge/AdminPurgeCommand.java

+59-22
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import java.util.Iterator;
66
import java.util.List;
77
import java.util.Set;
8+
import java.util.UUID;
9+
import java.util.concurrent.CompletableFuture;
810

911
import org.bukkit.Bukkit;
1012
import org.bukkit.event.EventHandler;
@@ -21,8 +23,10 @@
2123

2224
public class AdminPurgeCommand extends CompositeCommand implements Listener {
2325

26+
private static final Long YEAR2000 = 946713600L;
2427
private int count;
2528
private boolean inPurge;
29+
private boolean scanning;
2630
private boolean toBeConfirmed;
2731
private Iterator<String> it;
2832
private User user;
@@ -47,6 +51,10 @@ public void setup() {
4751

4852
@Override
4953
public boolean canExecute(User user, String label, List<String> args) {
54+
if (scanning) {
55+
user.sendMessage("commands.admin.purge.scanning-in-progress");
56+
return false;
57+
}
5058
if (inPurge) {
5159
user.sendMessage("commands.admin.purge.purge-in-progress", TextVariables.LABEL, this.getTopLabel());
5260
return false;
@@ -75,13 +83,21 @@ public boolean execute(User user, String label, List<String> args) {
7583
user.sendMessage("commands.admin.purge.days-one-or-more");
7684
return false;
7785
}
78-
islands = getOldIslands(days);
79-
user.sendMessage("commands.admin.purge.purgable-islands", TextVariables.NUMBER, String.valueOf(islands.size()));
80-
if (!islands.isEmpty()) {
81-
toBeConfirmed = true;
82-
user.sendMessage("commands.admin.purge.confirm", TextVariables.LABEL, this.getTopLabel());
83-
return false;
84-
}
86+
user.sendMessage("commands.admin.purge.scanning");
87+
scanning = true;
88+
getOldIslands(days).thenAccept(islandSet -> {
89+
user.sendMessage("commands.admin.purge.purgable-islands", TextVariables.NUMBER,
90+
String.valueOf(islandSet.size()));
91+
if (!islandSet.isEmpty()) {
92+
toBeConfirmed = true;
93+
user.sendMessage("commands.admin.purge.confirm", TextVariables.LABEL, this.getTopLabel());
94+
islands = islandSet;
95+
} else {
96+
user.sendMessage("commands.admin.purge.none-found");
97+
}
98+
scanning = false;
99+
});
100+
85101
} catch (NumberFormatException e) {
86102
user.sendMessage("commands.admin.purge.number-error");
87103
return false;
@@ -125,40 +141,61 @@ void onIslandDeleted(IslandDeletedEvent e) {
125141
* @param days days
126142
* @return set of islands
127143
*/
128-
Set<String> getOldIslands(int days) {
129-
long currentTimeMillis = System.currentTimeMillis();
130-
long daysInMilliseconds = (long) days * 1000 * 3600 * 24;
131-
Set<String> oldIslands = new HashSet<>();
132-
144+
CompletableFuture<Set<String>> getOldIslands(int days) {
145+
CompletableFuture<Set<String>> result = new CompletableFuture<>();
133146
// Process islands in one pass, logging and adding to the set if applicable
134-
getPlugin().getIslands().getIslands().stream()
147+
getPlugin().getIslands().getIslandsASync().thenAccept(list -> {
148+
user.sendMessage("commands.admin.purge.total-islands", TextVariables.NUMBER, String.valueOf(list.size()));
149+
Set<String> oldIslands = new HashSet<>();
150+
list.stream()
135151
.filter(i -> !i.isSpawn()).filter(i -> !i.getPurgeProtected())
136152
.filter(i -> i.getWorld() != null) // to handle currently unloaded world islands
137-
.filter(i -> i.getWorld().equals(this.getWorld())).filter(Island::isOwned).filter(
138-
i -> i.getMemberSet().stream()
139-
.allMatch(member -> (currentTimeMillis
140-
- Bukkit.getOfflinePlayer(member).getLastPlayed()) > daysInMilliseconds))
153+
.filter(i -> i.getWorld().equals(this.getWorld())) // Island needs to be in this world
154+
.filter(Island::isOwned) // The island needs to be owned
155+
.filter(i -> i.getMemberSet().stream().allMatch(member -> checkLastLoginTimestamp(days, member)))
141156
.forEach(i -> {
142157
// Add the unique island ID to the set
143158
oldIslands.add(i.getUniqueId());
144-
BentoBox.getInstance().log("Will purge island at " + Util.xyz(i.getCenter().toVector()) + " in "
159+
getPlugin().log("Will purge island at " + Util.xyz(i.getCenter().toVector()) + " in "
145160
+ i.getWorld().getName());
146161
// Log each member's last login information
147162
i.getMemberSet().forEach(member -> {
148-
Date lastLogin = new Date(Bukkit.getOfflinePlayer(member).getLastPlayed());
163+
Long timestamp = getPlayers().getLastLoginTimestamp(member);
164+
Date lastLogin = new Date(timestamp);
149165
BentoBox.getInstance()
150166
.log("Player " + BentoBox.getInstance().getPlayers().getName(member)
151167
+ " last logged in "
152-
+ (int) ((currentTimeMillis - Bukkit.getOfflinePlayer(member).getLastPlayed())
153-
/ 1000 / 3600 / 24)
168+
+ (int) ((System.currentTimeMillis() - timestamp) / 1000 / 3600 / 24)
154169
+ " days ago. " + lastLogin);
155170
});
156171
BentoBox.getInstance().log("+-----------------------------------------+");
157172
});
173+
result.complete(oldIslands);
174+
});
175+
return result;
176+
}
158177

159-
return oldIslands;
178+
private boolean checkLastLoginTimestamp(int days, UUID member) {
179+
long daysInMilliseconds = days * 24L * 3600 * 1000; // Calculate days in milliseconds
180+
Long lastLoginTimestamp = getPlayers().getLastLoginTimestamp(member);
181+
// If no valid last login time is found or it's before the year 2000, try to fetch from Bukkit
182+
if (lastLoginTimestamp == null || lastLoginTimestamp < YEAR2000) {
183+
lastLoginTimestamp = Bukkit.getOfflinePlayer(member).getLastPlayed();
184+
185+
// If still invalid, set the current timestamp to mark the user for eventual purging
186+
if (lastLoginTimestamp < YEAR2000) {
187+
getPlayers().setLoginTimeStamp(member, System.currentTimeMillis());
188+
return false; // User will be purged in the future
189+
} else {
190+
// Otherwise, update the last login timestamp with the valid value from Bukkit
191+
getPlayers().setLoginTimeStamp(member, lastLoginTimestamp);
192+
}
193+
}
194+
// Check if the difference between now and the last login is greater than the allowed days
195+
return System.currentTimeMillis() - lastLoginTimestamp > daysInMilliseconds;
160196
}
161197

198+
162199
/**
163200
* @return the inPurge
164201
*/

src/main/java/world/bentobox/bentobox/database/AbstractDatabaseHandler.java

+21
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,31 @@ protected AbstractDatabaseHandler() {}
134134
@Nullable
135135
public abstract T loadObject(@NonNull String uniqueId) throws InstantiationException, IllegalAccessException, InvocationTargetException, ClassNotFoundException, IntrospectionException, NoSuchMethodException;
136136

137+
/**
138+
* Loads all the records in this table and returns a list of them async
139+
* @return CompletableFuture List of <T>
140+
* @since 2.7.0
141+
*/
142+
public CompletableFuture<List<T>> loadObjectsASync() {
143+
CompletableFuture<List<T>> completableFuture = new CompletableFuture<>();
144+
145+
Bukkit.getScheduler().runTaskAsynchronously(BentoBox.getInstance(), () -> {
146+
try {
147+
completableFuture.complete(loadObjects()); // Complete the future with the result
148+
} catch (Exception e) {
149+
completableFuture.completeExceptionally(e); // Complete exceptionally if an error occurs
150+
plugin.logError("Failed to load objects asynchronously: " + e.getMessage());
151+
}
152+
});
153+
154+
return completableFuture;
155+
}
156+
137157
/**
138158
* Save T into the corresponding database
139159
*
140160
* @param instance that should be inserted into the database
161+
* @return completable future that is true if saved
141162
*/
142163
public abstract CompletableFuture<Boolean> saveObject(T instance) throws IllegalAccessException, InvocationTargetException, IntrospectionException ;
143164

src/main/java/world/bentobox/bentobox/database/Database.java

+7
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,13 @@ public static Set<Class<? extends DataObject>> getDataobjects() {
166166
return dataObjects;
167167
}
168168

169+
/**
170+
* Load all objects async
171+
* @return CompletableFuture<List<T>>
172+
*/
173+
public @NonNull CompletableFuture<List<T>> loadObjectsASync() {
174+
return handler.loadObjectsASync();
175+
}
169176

170177

171178
}

src/main/java/world/bentobox/bentobox/database/objects/Island.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.bukkit.Bukkit;
2020
import org.bukkit.GameMode;
2121
import org.bukkit.Location;
22+
import org.bukkit.Material;
2223
import org.bukkit.World;
2324
import org.bukkit.World.Environment;
2425
import org.bukkit.entity.Player;
@@ -1512,7 +1513,7 @@ public void setGameMode(String gameMode) {
15121513
*/
15131514
public boolean hasNetherIsland() {
15141515
World nether = BentoBox.getInstance().getIWM().getNetherWorld(getWorld());
1515-
return nether != null && !getCenter().toVector().toLocation(nether).getBlock().getType().isAir();
1516+
return nether != null && (getCenter().toVector().toLocation(nether).getBlock().getType() != Material.AIR);
15161517
}
15171518

15181519
/**
@@ -1536,7 +1537,7 @@ public boolean isNetherIslandEnabled() {
15361537
*/
15371538
public boolean hasEndIsland() {
15381539
World end = BentoBox.getInstance().getIWM().getEndWorld(getWorld());
1539-
return end != null && !getCenter().toVector().toLocation(end).getBlock().getType().isAir();
1540+
return end != null && (getCenter().toVector().toLocation(end).getBlock().getType() != Material.AIR);
15401541
}
15411542

15421543
/**

src/main/java/world/bentobox/bentobox/database/objects/Players.java

+18
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.bukkit.Bukkit;
1111
import org.bukkit.World;
1212
import org.bukkit.entity.Player;
13+
import org.eclipse.jdt.annotation.Nullable;
1314

1415
import com.google.gson.annotations.Expose;
1516

@@ -37,6 +38,8 @@ public class Players implements DataObject, MetaDataAble {
3738
private String locale = "";
3839
@Expose
3940
private Map<String, Integer> deaths = new HashMap<>();
41+
@Expose
42+
private Long lastLogin;
4043

4144
/**
4245
* This variable stores set of worlds where user inventory must be cleared.
@@ -292,5 +295,20 @@ public void setMetaData(Map<String, MetaDataValue> metaData) {
292295
this.metaData = metaData;
293296
}
294297

298+
/**
299+
* @return the lastLogin, Unix timestamp, or null if never logged in since this was tracked
300+
* @since 2.6.0
301+
*/
302+
@Nullable
303+
public Long getLastLogin() {
304+
return lastLogin;
305+
}
306+
307+
/**
308+
* @param lastLogin the lastLogin to set
309+
*/
310+
public void setLastLogin(Long lastLogin) {
311+
this.lastLogin = lastLogin;
312+
}
295313

296314
}

src/main/java/world/bentobox/bentobox/listeners/JoinLeaveListener.java

+3
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ public void onPlayerJoin(final PlayerJoinEvent event) {
6464
// don't exist
6565
players.getPlayer(playerUUID);
6666

67+
// Set the login
68+
players.setLoginTimeStamp(user);
69+
6770
// Reset island resets if required
6871
plugin.getIWM().getOverWorlds().stream()
6972
.filter(w -> event.getPlayer().getLastPlayed() < plugin.getIWM().getResetEpoch(w))

src/main/java/world/bentobox/bentobox/managers/IslandsManager.java

+10
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,16 @@ public Collection<Island> getIslands() {
458458
return handler.loadObjects().stream().toList();
459459
}
460460

461+
/**
462+
* Loads all existing islands from the database without caching async
463+
*
464+
* @return CompletableFuture<List> of every island
465+
* @since 2.7.0
466+
*/
467+
public CompletableFuture<List<Island>> getIslandsASync() {
468+
return handler.loadObjectsASync();
469+
}
470+
461471
/**
462472
* Returns an <strong>unmodifiable collection</strong> of all the islands (even
463473
* those who may be unowned) in the specified world.

src/main/java/world/bentobox/bentobox/managers/PlayersManager.java

+40
Original file line numberDiff line numberDiff line change
@@ -420,4 +420,44 @@ public CompletableFuture<Boolean> savePlayer(UUID uuid) {
420420
return CompletableFuture.completedFuture(false);
421421
}
422422

423+
/**
424+
* Records when the user last logged in. Called by the joinleave listener
425+
* @param user user
426+
* @since 2.7.0
427+
*/
428+
public void setLoginTimeStamp(User user) {
429+
if (user.isPlayer() && user.isOnline()) {
430+
setLoginTimeStamp(user.getUniqueId(), System.currentTimeMillis());
431+
}
432+
}
433+
434+
/**
435+
* Set the player's last login time to a timestamp
436+
* @param playerUUID player UUID
437+
* @param timestamp timestamp to set
438+
* @since 2.7.0
439+
*/
440+
public void setLoginTimeStamp(UUID playerUUID, long timestamp) {
441+
Players p = this.getPlayer(playerUUID);
442+
if (p != null) {
443+
p.setLastLogin(timestamp);
444+
this.savePlayer(playerUUID);
445+
}
446+
}
447+
448+
/**
449+
* Get the last login time stamp for this player
450+
* @param uuid player's UUID
451+
* @return timestamp or null if unknown or not recorded yet
452+
* @since 2.7.0
453+
*/
454+
@Nullable
455+
public Long getLastLoginTimestamp(UUID uuid) {
456+
Players p = this.getPlayer(uuid);
457+
if (p != null) {
458+
return p.getLastLogin();
459+
}
460+
return null;
461+
}
462+
423463
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package world.bentobox.bentobox.nms.v1_21_2_R0_1_SNAPSHOT;
2+
3+
/**
4+
* Same as 1.21
5+
*/
6+
public class PasteHandlerImpl extends world.bentobox.bentobox.nms.v1_21_R0_1_SNAPSHOT.PasteHandlerImpl {
7+
// Do nothing special
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package world.bentobox.bentobox.nms.v1_21_2_R0_1_SNAPSHOT;
2+
3+
/**
4+
* Same as 1.21
5+
*/
6+
public class WorldRegeneratorImpl extends world.bentobox.bentobox.nms.v1_21_R0_1_SNAPSHOT.WorldRegeneratorImpl {
7+
// Do nothing special
8+
}

src/main/resources/locales/en-US.yml

+4
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ commands:
9898
purgable-islands: '&a Found &b [number] &a purgable islands.'
9999
purge-in-progress: '&c Purging in progress. Use &b /[label] purge stop &c to
100100
cancel.'
101+
scanning: '&a Scanning islands in the database. This may take a while depending on how many you have...'
102+
scanning-in-progress: '&c Scanning in progress, please wait'
103+
none-found: '&c No islands found to purge.'
104+
total-islands: '&a You have [number] islands in your database in all worlds.'
101105
number-error: '&c Argument must be a number of days'
102106
confirm: '&d Type &b /[label] purge confirm &d to start purging'
103107
completed: '&a Purging stopped.'

src/test/java/world/bentobox/bentobox/AbstractCommonSetup.java

+4-3
Original file line numberDiff line numberDiff line change
@@ -298,13 +298,14 @@ public void checkSpigotMessage(String expectedMessage, int expectedOccurrences)
298298
*/
299299
public EntityExplodeEvent getExplodeEvent(Entity entity, Location l, List<Block> list) {
300300
//return new EntityExplodeEvent(entity, l, list, 0, null);
301-
return new EntityExplodeEvent(entity, l, list, 0);
301+
return new EntityExplodeEvent(entity, l, list, 0, null);
302302
}
303303

304304
public PlayerDeathEvent getPlayerDeathEvent(Player player, List<ItemStack> drops, int droppedExp, int newExp,
305305
int newTotalExp, int newLevel, @Nullable String deathMessage) {
306-
//return new PlayerDeathEvent(player, null, drops, droppedExp, newExp, newTotalExp, newLevel, deathMessage);
307-
return new PlayerDeathEvent(player, drops, droppedExp, newExp, newTotalExp, newLevel, deathMessage);
306+
//Technically this null is not allowed, but it works right now
307+
return new PlayerDeathEvent(player, null, drops, droppedExp, newExp,
308+
newTotalExp, newLevel, deathMessage);
308309
}
309310

310311
}

0 commit comments

Comments
 (0)