001/* 002 * PlotSquared, a land and world management plugin for Minecraft. 003 * Copyright (C) IntellectualSites <https://intellectualsites.com> 004 * Copyright (C) IntellectualSites team and contributors 005 * 006 * This program is free software: you can redistribute it and/or modify 007 * it under the terms of the GNU General Public License as published by 008 * the Free Software Foundation, either version 3 of the License, or 009 * (at your option) any later version. 010 * 011 * This program is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 014 * GNU General Public License for more details. 015 * 016 * You should have received a copy of the GNU General Public License 017 * along with this program. If not, see <https://www.gnu.org/licenses/>. 018 */ 019package com.plotsquared.core.plot.expiration; 020 021import com.google.inject.Inject; 022import com.plotsquared.core.PlotSquared; 023import com.plotsquared.core.configuration.caption.Caption; 024import com.plotsquared.core.configuration.caption.TranslatableCaption; 025import com.plotsquared.core.database.DBFunc; 026import com.plotsquared.core.events.PlotFlagAddEvent; 027import com.plotsquared.core.events.PlotUnlinkEvent; 028import com.plotsquared.core.events.Result; 029import com.plotsquared.core.player.MetaDataAccess; 030import com.plotsquared.core.player.OfflinePlotPlayer; 031import com.plotsquared.core.player.PlayerMetaDataKeys; 032import com.plotsquared.core.player.PlotPlayer; 033import com.plotsquared.core.plot.Plot; 034import com.plotsquared.core.plot.PlotArea; 035import com.plotsquared.core.plot.PlotAreaType; 036import com.plotsquared.core.plot.flag.GlobalFlagContainer; 037import com.plotsquared.core.plot.flag.PlotFlag; 038import com.plotsquared.core.plot.flag.implementations.AnalysisFlag; 039import com.plotsquared.core.plot.flag.implementations.KeepFlag; 040import com.plotsquared.core.plot.flag.implementations.ServerPlotFlag; 041import com.plotsquared.core.util.EventDispatcher; 042import com.plotsquared.core.util.query.PlotQuery; 043import com.plotsquared.core.util.task.RunnableVal; 044import com.plotsquared.core.util.task.RunnableVal3; 045import com.plotsquared.core.util.task.TaskManager; 046import com.plotsquared.core.util.task.TaskTime; 047import net.kyori.adventure.text.Component; 048import net.kyori.adventure.text.minimessage.tag.Tag; 049import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; 050import org.checkerframework.checker.nullness.qual.NonNull; 051 052import java.util.ArrayDeque; 053import java.util.ArrayList; 054import java.util.Collection; 055import java.util.Collections; 056import java.util.HashSet; 057import java.util.Iterator; 058import java.util.Objects; 059import java.util.UUID; 060import java.util.concurrent.ConcurrentHashMap; 061import java.util.concurrent.ConcurrentLinkedDeque; 062 063public class ExpireManager { 064 065 private final ConcurrentHashMap<UUID, Long> dates_cache; 066 private final ConcurrentHashMap<UUID, Long> account_age_cache; 067 private final EventDispatcher eventDispatcher; 068 private final ArrayDeque<ExpiryTask> tasks; 069 private volatile HashSet<Plot> plotsToDelete; 070 /** 071 * 0 = stopped, 1 = stopping, 2 = running 072 */ 073 private int running; 074 075 @Inject 076 public ExpireManager(final @NonNull EventDispatcher eventDispatcher) { 077 this.tasks = new ArrayDeque<>(); 078 this.dates_cache = new ConcurrentHashMap<>(); 079 this.account_age_cache = new ConcurrentHashMap<>(); 080 this.eventDispatcher = eventDispatcher; 081 } 082 083 public void addTask(ExpiryTask task) { 084 this.tasks.add(task); 085 } 086 087 public void handleJoin(PlotPlayer<?> pp) { 088 storeDate(pp.getUUID(), System.currentTimeMillis()); 089 if (plotsToDelete != null && !plotsToDelete.isEmpty()) { 090 for (Plot plot : pp.getPlots()) { 091 plotsToDelete.remove(plot); 092 } 093 } 094 confirmExpiry(pp); 095 } 096 097 public void handleEntry(PlotPlayer<?> pp, Plot plot) { 098 if (plotsToDelete != null && !plotsToDelete.isEmpty() && pp 099 .hasPermission("plots.admin.command.autoclear") && plotsToDelete.contains(plot)) { 100 if (!isExpired(new ArrayDeque<>(tasks), plot).isEmpty()) { 101 confirmExpiry(pp); 102 } else { 103 plotsToDelete.remove(plot); 104 confirmExpiry(pp); 105 } 106 } 107 } 108 109 /** 110 * Gets the account last joined - first joined (or Long.MAX_VALUE) 111 * 112 * @param uuid player uuid 113 * @return result 114 */ 115 public long getAccountAge(UUID uuid) { 116 Long value = this.account_age_cache.get(uuid); 117 return value == null ? Long.MAX_VALUE : value; 118 } 119 120 public long getTimestamp(UUID uuid) { 121 Long value = this.dates_cache.get(uuid); 122 return value == null ? 0 : value; 123 } 124 125 public void updateExpired(Plot plot) { 126 if (plotsToDelete != null && !plotsToDelete.isEmpty() && plotsToDelete.contains(plot)) { 127 if (isExpired(new ArrayDeque<>(tasks), plot).isEmpty()) { 128 plotsToDelete.remove(plot); 129 } 130 } 131 } 132 133 public void confirmExpiry(final PlotPlayer<?> pp) { 134 TaskManager.runTask(() -> { 135 try (final MetaDataAccess<Boolean> metaDataAccess = pp.accessTemporaryMetaData( 136 PlayerMetaDataKeys.TEMPORARY_IGNORE_EXPIRE_TASK)) { 137 if (metaDataAccess.isPresent()) { 138 return; 139 } 140 if (plotsToDelete != null && !plotsToDelete.isEmpty() && pp.hasPermission("plots.admin.command.autoclear")) { 141 final int num = plotsToDelete.size(); 142 while (!plotsToDelete.isEmpty()) { 143 Iterator<Plot> iter = plotsToDelete.iterator(); 144 final Plot current = iter.next(); 145 if (!isExpired(new ArrayDeque<>(tasks), current).isEmpty()) { 146 metaDataAccess.set(true); 147 current.getCenter(pp::teleport); 148 metaDataAccess.remove(); 149 Caption msg = TranslatableCaption.of("expiry.expired_options_clicky"); 150 TagResolver resolver = TagResolver.builder() 151 .tag("num", Tag.inserting(Component.text(num))) 152 .tag("are_or_is", Tag.inserting(Component.text(num > 1 ? "plots are" : "plot is"))) 153 .tag("list_cmd", Tag.preProcessParsed("/plot list expired")) 154 .tag("plot", Tag.inserting(Component.text(current.toString()))) 155 .tag("cmd_del", Tag.preProcessParsed("/plot delete")) 156 .tag("cmd_keep_1d", Tag.preProcessParsed("/plot flag set keep 1d")) 157 .tag("cmd_keep", Tag.preProcessParsed("/plot flag set keep true")) 158 .tag("cmd_no_show_expir", Tag.preProcessParsed("/plot toggle clear-confirmation")) 159 .build(); 160 pp.sendMessage(msg, resolver); 161 return; 162 } else { 163 iter.remove(); 164 } 165 } 166 plotsToDelete.clear(); 167 } 168 } 169 }); 170 } 171 172 173 public boolean cancelTask() { 174 if (this.running != 2) { 175 return false; 176 } 177 this.running = 1; 178 return true; 179 } 180 181 public boolean runAutomatedTask() { 182 return runTask(new RunnableVal3<>() { 183 @Override 184 public void run(Plot plot, Runnable runnable, Boolean confirm) { 185 if (confirm) { 186 if (plotsToDelete == null) { 187 plotsToDelete = new HashSet<>(); 188 } 189 plotsToDelete.add(plot); 190 runnable.run(); 191 } else { 192 deleteWithMessage(plot, runnable); 193 } 194 } 195 }); 196 } 197 198 public Collection<ExpiryTask> isExpired(ArrayDeque<ExpiryTask> applicable, Plot plot) { 199 // Filter out invalid worlds 200 for (int i = 0; i < applicable.size(); i++) { 201 ExpiryTask et = applicable.poll(); 202 if (et.applies(plot.getArea())) { 203 applicable.add(et); 204 } 205 } 206 if (applicable.isEmpty()) { 207 return new ArrayList<>(); 208 } 209 210 // Don't delete server plots 211 if (plot.getFlag(ServerPlotFlag.class)) { 212 return new ArrayList<>(); 213 } 214 215 // Filter out non old plots 216 boolean shouldCheckAccountAge = false; 217 for (int i = 0; i < applicable.size(); i++) { 218 ExpiryTask et = applicable.poll(); 219 if (et.applies(getAge(plot, et.shouldDeleteForUnknownOwner()))) { 220 applicable.add(et); 221 shouldCheckAccountAge |= et.getSettings().SKIP_ACCOUNT_AGE_DAYS != -1; 222 } 223 } 224 if (applicable.isEmpty()) { 225 return new ArrayList<>(); 226 } 227 // Check account age 228 if (shouldCheckAccountAge) { 229 for (int i = 0; i < applicable.size(); i++) { 230 ExpiryTask et = applicable.poll(); 231 long accountAge = getAge(plot, et.shouldDeleteForUnknownOwner()); 232 if (et.appliesAccountAge(accountAge)) { 233 applicable.add(et); 234 } 235 } 236 if (applicable.isEmpty()) { 237 return new ArrayList<>(); 238 } 239 } 240 241 // Run applicable non confirming tasks 242 for (int i = 0; i < applicable.size(); i++) { 243 ExpiryTask expiryTask = applicable.poll(); 244 if (!expiryTask.needsAnalysis() || plot.getArea().getType() != PlotAreaType.NORMAL) { 245 if (!expiryTask.requiresConfirmation()) { 246 return Collections.singletonList(expiryTask); 247 } 248 } 249 applicable.add(expiryTask); 250 } 251 // Run applicable confirming tasks 252 for (int i = 0; i < applicable.size(); i++) { 253 ExpiryTask expiryTask = applicable.poll(); 254 if (!expiryTask.needsAnalysis() || plot.getArea().getType() != PlotAreaType.NORMAL) { 255 return Collections.singletonList(expiryTask); 256 } 257 applicable.add(expiryTask); 258 } 259 return applicable; 260 } 261 262 public ArrayDeque<ExpiryTask> getTasks(PlotArea area) { 263 ArrayDeque<ExpiryTask> queue = new ArrayDeque<>(tasks); 264 queue.removeIf(expiryTask -> !expiryTask.applies(area)); 265 return queue; 266 } 267 268 public void passesComplexity( 269 PlotAnalysis analysis, Collection<ExpiryTask> applicable, 270 RunnableVal<Boolean> success, Runnable failure 271 ) { 272 if (analysis != null) { 273 // Run non confirming tasks 274 for (ExpiryTask et : applicable) { 275 if (!et.requiresConfirmation() && et.applies(analysis)) { 276 success.run(false); 277 return; 278 } 279 } 280 for (ExpiryTask et : applicable) { 281 if (et.applies(analysis)) { 282 success.run(true); 283 return; 284 } 285 } 286 failure.run(); 287 } 288 } 289 290 public boolean runTask(final RunnableVal3<Plot, Runnable, Boolean> expiredTask) { 291 if (this.running != 0) { 292 return false; 293 } 294 this.running = 2; 295 TaskManager.runTaskAsync(new Runnable() { 296 private ConcurrentLinkedDeque<Plot> plots = null; 297 298 @Override 299 public void run() { 300 final Runnable task = this; 301 if (ExpireManager.this.running != 2) { 302 ExpireManager.this.running = 0; 303 return; 304 } 305 if (plots == null) { 306 plots = new ConcurrentLinkedDeque<>(PlotQuery.newQuery().allPlots().asList()); 307 } 308 while (!plots.isEmpty()) { 309 if (ExpireManager.this.running != 2) { 310 ExpireManager.this.running = 0; 311 return; 312 } 313 Plot plot = plots.poll(); 314 PlotArea area = plot.getArea(); 315 final Plot newPlot = area.getPlot(plot.getId()); 316 final ArrayDeque<ExpiryTask> applicable = new ArrayDeque<>(tasks); 317 final Collection<ExpiryTask> expired = isExpired(applicable, newPlot); 318 if (expired.isEmpty()) { 319 continue; 320 } 321 for (ExpiryTask expiryTask : expired) { 322 if (!expiryTask.needsAnalysis()) { 323 expiredTask.run(newPlot, () -> TaskManager.getPlatformImplementation() 324 .taskLaterAsync(task, TaskTime.ticks(1L)), 325 expiryTask.requiresConfirmation() 326 ); 327 return; 328 } 329 } 330 final RunnableVal<PlotAnalysis> handleAnalysis = 331 new RunnableVal<>() { 332 @Override 333 public void run(final PlotAnalysis changed) { 334 passesComplexity(changed, expired, new RunnableVal<>() { 335 @Override 336 public void run(Boolean confirmation) { 337 expiredTask.run( 338 newPlot, 339 () -> TaskManager 340 .getPlatformImplementation() 341 .taskLaterAsync(task, TaskTime.ticks(1L)), 342 confirmation 343 ); 344 } 345 }, () -> { 346 PlotFlag<?, ?> plotFlag = GlobalFlagContainer.getInstance() 347 .getFlag(AnalysisFlag.class) 348 .createFlagInstance(changed.asList()); 349 PlotFlagAddEvent event = 350 eventDispatcher.callFlagAdd(plotFlag, plot); 351 if (event.getEventResult() == Result.DENY) { 352 return; 353 } 354 newPlot.setFlag(event.getFlag()); 355 TaskManager.runTaskLaterAsync(task, TaskTime.seconds(1L)); 356 }); 357 } 358 }; 359 final Runnable doAnalysis = 360 () -> PlotSquared.platform().hybridUtils().analyzePlot(newPlot, handleAnalysis); 361 362 PlotAnalysis analysis = newPlot.getComplexity(null); 363 if (analysis != null) { 364 passesComplexity(analysis, expired, new RunnableVal<>() { 365 @Override 366 public void run(Boolean value) { 367 doAnalysis.run(); 368 } 369 }, () -> TaskManager.getPlatformImplementation().taskLaterAsync(task, TaskTime.ticks(1L))); 370 } else { 371 doAnalysis.run(); 372 } 373 return; 374 } 375 if (plots.isEmpty()) { 376 ExpireManager.this.running = 3; 377 TaskManager.runTaskLater(() -> { 378 if (ExpireManager.this.running == 3) { 379 ExpireManager.this.running = 2; 380 runTask(expiredTask); 381 } 382 }, TaskTime.ticks(86400000L)); 383 } else { 384 TaskManager.runTaskLaterAsync(task, TaskTime.seconds(10L)); 385 } 386 } 387 }); 388 return true; 389 } 390 391 public void storeDate(UUID uuid, long time) { 392 Long existing = this.dates_cache.put(uuid, time); 393 if (existing != null) { 394 long diff = time - existing; 395 if (diff > 0) { 396 Long account_age = this.account_age_cache.get(uuid); 397 if (account_age != null) { 398 this.account_age_cache.put(uuid, account_age + diff); 399 } 400 } 401 } 402 } 403 404 public HashSet<Plot> getPendingExpired() { 405 return plotsToDelete == null ? new HashSet<>() : plotsToDelete; 406 } 407 408 public void deleteWithMessage(Plot plot, Runnable whenDone) { 409 if (plot.isMerged()) { 410 PlotUnlinkEvent event = this.eventDispatcher 411 .callUnlink(plot.getArea(), plot, true, false, 412 PlotUnlinkEvent.REASON.EXPIRE_DELETE 413 ); 414 if (event.getEventResult() != Result.DENY && plot.getPlotModificationManager().unlinkPlot( 415 event.isCreateRoad(), 416 event.isCreateSign() 417 )) { 418 this.eventDispatcher.callPostUnlink(plot, PlotUnlinkEvent.REASON.EXPIRE_DELETE); 419 } 420 } 421 for (UUID helper : plot.getTrusted()) { 422 PlotPlayer<?> player = PlotSquared.platform().playerManager().getPlayerIfExists(helper); 423 if (player != null) { 424 player.sendMessage( 425 TranslatableCaption.of("trusted.plot_removed_user"), 426 TagResolver.resolver("plot", Tag.inserting(Component.text(plot.toString()))) 427 ); 428 } 429 } 430 for (UUID helper : plot.getMembers()) { 431 PlotPlayer<?> player = PlotSquared.platform().playerManager().getPlayerIfExists(helper); 432 if (player != null) { 433 player.sendMessage( 434 TranslatableCaption.of("trusted.plot_removed_user"), 435 TagResolver.resolver("plot", Tag.inserting(Component.text(plot.toString()))) 436 ); 437 } 438 } 439 plot.getPlotModificationManager().deletePlot(null, whenDone); 440 } 441 442 /** 443 * Get the age (last play time) of the passed player 444 * 445 * @param uuid the uuid of the owner to check against 446 * @param shouldDeleteUnknownOwner {@code true} if an unknown player should be counted as never online 447 * @return the millis since the player was last online, or {@link Long#MAX_VALUE} if player was never online 448 * @since 6.4.0 449 */ 450 public long getAge(UUID uuid, final boolean shouldDeleteUnknownOwner) { 451 if (PlotSquared.platform().playerManager().getPlayerIfExists(uuid) != null) { 452 return 0; 453 } 454 Long last = this.dates_cache.get(uuid); 455 if (last == null) { 456 OfflinePlotPlayer opp = PlotSquared.platform().playerManager().getOfflinePlayer(uuid); 457 if (opp != null && (last = opp.getLastPlayed()) != 0) { 458 this.dates_cache.put(uuid, last); 459 } else { 460 return shouldDeleteUnknownOwner ? Long.MAX_VALUE : 0; 461 } 462 } 463 if (last == 0) { 464 return 0; 465 } 466 return System.currentTimeMillis() - last; 467 } 468 469 public long getAge(Plot plot, final boolean shouldDeleteUnknownOwner) { 470 if (!plot.hasOwner() || Objects.equals(DBFunc.EVERYONE, plot.getOwner()) 471 || PlotSquared.platform().playerManager().getPlayerIfExists(plot.getOwner()) != null || plot.getRunning() > 0) { 472 return 0; 473 } 474 475 final Object value = plot.getFlag(KeepFlag.class); 476 if (!value.equals(false)) { 477 if (value instanceof Boolean) { 478 if (Boolean.TRUE.equals(value)) { 479 return 0; 480 } 481 } else if (value instanceof Long) { 482 if ((Long) value > System.currentTimeMillis()) { 483 return 0; 484 } 485 } else { // Invalid? 486 return 0; 487 } 488 } 489 long min = Long.MAX_VALUE; 490 for (UUID owner : plot.getOwners()) { 491 long age = getAge(owner, shouldDeleteUnknownOwner); 492 if (age < min) { 493 min = age; 494 } 495 } 496 return min; 497 } 498 499}