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