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; 020 021import com.plotsquared.core.configuration.ConfigurationSection; 022import com.plotsquared.core.configuration.ConfigurationUtil; 023import com.plotsquared.core.configuration.MemorySection; 024import com.plotsquared.core.configuration.Settings; 025import com.plotsquared.core.configuration.Storage; 026import com.plotsquared.core.configuration.caption.CaptionMap; 027import com.plotsquared.core.configuration.caption.DummyCaptionMap; 028import com.plotsquared.core.configuration.caption.TranslatableCaption; 029import com.plotsquared.core.configuration.caption.load.CaptionLoader; 030import com.plotsquared.core.configuration.caption.load.DefaultCaptionProvider; 031import com.plotsquared.core.configuration.file.YamlConfiguration; 032import com.plotsquared.core.configuration.serialization.ConfigurationSerialization; 033import com.plotsquared.core.database.DBFunc; 034import com.plotsquared.core.database.Database; 035import com.plotsquared.core.database.MySQL; 036import com.plotsquared.core.database.SQLManager; 037import com.plotsquared.core.database.SQLite; 038import com.plotsquared.core.generator.GeneratorWrapper; 039import com.plotsquared.core.generator.HybridPlotWorld; 040import com.plotsquared.core.generator.HybridUtils; 041import com.plotsquared.core.generator.IndependentPlotGenerator; 042import com.plotsquared.core.inject.factory.HybridPlotWorldFactory; 043import com.plotsquared.core.listener.PlotListener; 044import com.plotsquared.core.location.Location; 045import com.plotsquared.core.player.PlayerMetaDataKeys; 046import com.plotsquared.core.plot.BlockBucket; 047import com.plotsquared.core.plot.Plot; 048import com.plotsquared.core.plot.PlotArea; 049import com.plotsquared.core.plot.PlotAreaTerrainType; 050import com.plotsquared.core.plot.PlotAreaType; 051import com.plotsquared.core.plot.PlotCluster; 052import com.plotsquared.core.plot.PlotId; 053import com.plotsquared.core.plot.PlotManager; 054import com.plotsquared.core.plot.expiration.ExpireManager; 055import com.plotsquared.core.plot.expiration.ExpiryTask; 056import com.plotsquared.core.plot.flag.GlobalFlagContainer; 057import com.plotsquared.core.plot.world.PlotAreaManager; 058import com.plotsquared.core.plot.world.SinglePlotArea; 059import com.plotsquared.core.plot.world.SinglePlotAreaManager; 060import com.plotsquared.core.util.EventDispatcher; 061import com.plotsquared.core.util.FileUtils; 062import com.plotsquared.core.util.LegacyConverter; 063import com.plotsquared.core.util.MathMan; 064import com.plotsquared.core.util.ReflectionUtils; 065import com.plotsquared.core.util.task.TaskManager; 066import com.plotsquared.core.uuid.UUIDPipeline; 067import com.sk89q.worldedit.WorldEdit; 068import com.sk89q.worldedit.event.platform.PlatformReadyEvent; 069import com.sk89q.worldedit.math.BlockVector2; 070import com.sk89q.worldedit.util.eventbus.EventHandler; 071import com.sk89q.worldedit.util.eventbus.Subscribe; 072import org.apache.logging.log4j.LogManager; 073import org.apache.logging.log4j.Logger; 074import org.checkerframework.checker.nullness.qual.MonotonicNonNull; 075import org.checkerframework.checker.nullness.qual.NonNull; 076import org.checkerframework.checker.nullness.qual.Nullable; 077 078import java.io.BufferedReader; 079import java.io.File; 080import java.io.FileInputStream; 081import java.io.FileOutputStream; 082import java.io.IOException; 083import java.io.InputStream; 084import java.io.InputStreamReader; 085import java.io.ObjectInputStream; 086import java.io.ObjectOutputStream; 087import java.net.URI; 088import java.net.URISyntaxException; 089import java.net.URL; 090import java.nio.file.Files; 091import java.nio.file.StandardOpenOption; 092import java.sql.SQLException; 093import java.util.ArrayDeque; 094import java.util.ArrayList; 095import java.util.Arrays; 096import java.util.Collection; 097import java.util.Collections; 098import java.util.Comparator; 099import java.util.HashMap; 100import java.util.HashSet; 101import java.util.Iterator; 102import java.util.List; 103import java.util.Locale; 104import java.util.Map; 105import java.util.Map.Entry; 106import java.util.Objects; 107import java.util.Set; 108import java.util.concurrent.Executors; 109import java.util.function.Consumer; 110import java.util.regex.Pattern; 111import java.util.zip.ZipEntry; 112import java.util.zip.ZipInputStream; 113 114/** 115 * An implementation of the core, with a static getter for easy access. 116 */ 117@SuppressWarnings({"WeakerAccess"}) 118public class PlotSquared { 119 120 private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + PlotSquared.class.getSimpleName()); 121 private static @MonotonicNonNull PlotSquared instance; 122 123 // Implementation 124 private final PlotPlatform<?> platform; 125 // Current thread 126 private final Thread thread; 127 // UUID pipelines 128 private final UUIDPipeline impromptuUUIDPipeline = 129 new UUIDPipeline(Executors.newCachedThreadPool()); 130 private final UUIDPipeline backgroundUUIDPipeline = 131 new UUIDPipeline(Executors.newSingleThreadExecutor()); 132 // Localization 133 private final Map<String, CaptionMap> captionMaps = new HashMap<>(); 134 public HashMap<String, HashMap<PlotId, Plot>> plots_tmp; 135 private CaptionLoader captionLoader; 136 // WorldEdit instance 137 private WorldEdit worldedit; 138 private File configFile; 139 private File worldsFile; 140 private YamlConfiguration worldConfiguration; 141 // Temporary hold the plots/clusters before the worlds load 142 private HashMap<String, Set<PlotCluster>> clustersTmp; 143 private YamlConfiguration config; 144 // Platform / Version / Update URL 145 private PlotVersion version; 146 // Files and configuration 147 private File jarFile = null; // This file 148 private File storageFile; 149 private EventDispatcher eventDispatcher; 150 private PlotListener plotListener; 151 152 private boolean weInitialised; 153 154 /** 155 * Initialize PlotSquared with the desired Implementation class. 156 * 157 * @param iPlotMain Implementation of {@link PlotPlatform} used 158 * @param platform The platform being used 159 */ 160 public PlotSquared( 161 final @NonNull PlotPlatform<?> iPlotMain, 162 final @NonNull String platform 163 ) { 164 if (instance != null) { 165 throw new IllegalStateException("Cannot re-initialize the PlotSquared singleton"); 166 } 167 instance = this; 168 169 this.thread = Thread.currentThread(); 170 this.platform = iPlotMain; 171 Settings.PLATFORM = platform; 172 173 // Initialize the class 174 PlayerMetaDataKeys.load(); 175 176 // 177 // Register configuration serializable classes 178 // 179 ConfigurationSerialization.registerClass(BlockBucket.class, "BlockBucket"); 180 181 // load configs before reading from settings 182 if (!setupConfigs()) { 183 return; 184 } 185 186 this.captionLoader = CaptionLoader.of( 187 Locale.ENGLISH, 188 CaptionLoader.patternExtractor(Pattern.compile("messages_(.*)\\.json")), 189 DefaultCaptionProvider.forClassLoaderFormatString( 190 this.getClass().getClassLoader(), 191 "lang/messages_%s.json" // the path in our jar file 192 ), 193 TranslatableCaption.DEFAULT_NAMESPACE 194 ); 195 // Load caption map 196 try { 197 this.loadCaptionMap(); 198 } catch (final Exception e) { 199 LOGGER.error("Failed to load caption map", e); 200 LOGGER.error("Shutting down server to prevent further issues"); 201 this.platform.shutdownServer(); 202 throw new RuntimeException("Abort loading PlotSquared"); 203 } 204 205 // Setup the global flag container 206 GlobalFlagContainer.setup(); 207 208 try { 209 new ReflectionUtils(this.platform.serverNativePackage()); 210 try { 211 URL logurl = PlotSquared.class.getProtectionDomain().getCodeSource().getLocation(); 212 this.jarFile = new File( 213 URI.create( 214 logurl.toURI().toString().split("\\!")[0].replaceAll("jar:file", "file")) 215 .getPath()); 216 } catch (URISyntaxException | SecurityException e) { 217 e.printStackTrace(); 218 this.jarFile = new File(this.platform.getDirectory().getParentFile(), "PlotSquared.jar"); 219 if (!this.jarFile.exists()) { 220 this.jarFile = new File( 221 this.platform.getDirectory().getParentFile(), 222 "PlotSquared-" + platform + ".jar" 223 ); 224 } 225 } 226 227 this.worldedit = WorldEdit.getInstance(); 228 WorldEdit.getInstance().getEventBus().register(new WEPlatformReadyListener()); 229 230 // Create Event utility class 231 this.eventDispatcher = new EventDispatcher(this.worldedit); 232 // Create plot listener 233 this.plotListener = new PlotListener(this.eventDispatcher); 234 235 // Copy files 236 copyFile("town.template", Settings.Paths.TEMPLATES); 237 copyFile("bridge.template", Settings.Paths.TEMPLATES); 238 copyFile("skyblock.template", Settings.Paths.TEMPLATES); 239 showDebug(); 240 } catch (Throwable e) { 241 e.printStackTrace(); 242 } 243 } 244 245 /** 246 * Gets an instance of PlotSquared. 247 * 248 * @return instance of PlotSquared 249 */ 250 public static @NonNull PlotSquared get() { 251 return PlotSquared.instance; 252 } 253 254 /** 255 * Get the platform specific implementation of PlotSquared 256 * 257 * @return Platform implementation 258 */ 259 public static @NonNull PlotPlatform<?> platform() { 260 if (instance != null && instance.platform != null) { 261 return instance.platform; 262 } 263 throw new IllegalStateException("Plot platform implementation is missing"); 264 } 265 266 public void loadCaptionMap() throws Exception { 267 this.platform.copyCaptionMaps(); 268 // Setup localization 269 CaptionMap captionMap; 270 if (Settings.Enabled_Components.PER_USER_LOCALE) { 271 captionMap = this.captionLoader.loadAll(this.platform.getDirectory().toPath().resolve("lang")); 272 } else { 273 String fileName = "messages_" + Settings.Enabled_Components.DEFAULT_LOCALE + ".json"; 274 captionMap = this.captionLoader.loadOrCreateSingle(this.platform 275 .getDirectory() 276 .toPath() 277 .resolve("lang") 278 .resolve(fileName)); 279 } 280 this.captionMaps.put(TranslatableCaption.DEFAULT_NAMESPACE, captionMap); 281 LOGGER.info( 282 "Loaded caption map for namespace 'plotsquared': {}", 283 this.captionMaps.get(TranslatableCaption.DEFAULT_NAMESPACE).getClass().getCanonicalName() 284 ); 285 } 286 287 /** 288 * Get the platform specific {@link PlotAreaManager} instance 289 * 290 * @return Plot area manager 291 */ 292 public @NonNull PlotAreaManager getPlotAreaManager() { 293 return this.platform.plotAreaManager(); 294 } 295 296 public void startExpiryTasks() { 297 if (Settings.Enabled_Components.PLOT_EXPIRY) { 298 ExpireManager expireManager = PlotSquared.platform().expireManager(); 299 expireManager.runAutomatedTask(); 300 for (Settings.Auto_Clear settings : Settings.AUTO_CLEAR.getInstances()) { 301 ExpiryTask task = new ExpiryTask(settings, this.getPlotAreaManager()); 302 expireManager.addTask(task); 303 } 304 } 305 } 306 307 public boolean isMainThread(final @NonNull Thread thread) { 308 return this.thread == thread; 309 } 310 311 /** 312 * Check if `version` is >= `version2`. 313 * 314 * @param version First version 315 * @param version2 Second version 316 * @return {@code true} if `version` is >= `version2` 317 */ 318 public boolean checkVersion( 319 final int[] version, 320 final int... version2 321 ) { 322 return version[0] > version2[0] || version[0] == version2[0] && version[1] > version2[1] 323 || version[0] == version2[0] && version[1] == version2[1] && version[2] >= version2[2]; 324 } 325 326 /** 327 * Gets the current PlotSquared version. 328 * 329 * @return current version in config or null 330 */ 331 public @NonNull PlotVersion getVersion() { 332 return this.version; 333 } 334 335 /** 336 * Gets the server platform this plugin is running on this is running on. 337 * 338 * <p>This will be either <b>Bukkit</b> or <b>Sponge</b></p> 339 * 340 * @return the server implementation 341 */ 342 public @NonNull String getPlatform() { 343 return Settings.PLATFORM; 344 } 345 346 /** 347 * Add a global reference to a plot world. 348 * <p> 349 * You can remove the reference by calling {@link #removePlotArea(PlotArea)} 350 * </p> 351 * 352 * @param plotArea the {@link PlotArea} to add. 353 */ 354 @SuppressWarnings("unchecked") 355 public void addPlotArea(final @NonNull PlotArea plotArea) { 356 HashMap<PlotId, Plot> plots; 357 if (plots_tmp == null || (plots = plots_tmp.remove(plotArea.toString())) == null) { 358 if (plotArea.getType() == PlotAreaType.PARTIAL) { 359 plots = this.plots_tmp != null ? this.plots_tmp.get(plotArea.getWorldName()) : null; 360 if (plots != null) { 361 Iterator<Entry<PlotId, Plot>> iterator = plots.entrySet().iterator(); 362 while (iterator.hasNext()) { 363 Entry<PlotId, Plot> next = iterator.next(); 364 PlotId id = next.getKey(); 365 if (plotArea.contains(id)) { 366 next.getValue().setArea(plotArea); 367 iterator.remove(); 368 } 369 } 370 } 371 } 372 } else { 373 for (Plot entry : plots.values()) { 374 entry.setArea(plotArea); 375 } 376 } 377 Set<PlotCluster> clusters; 378 if (clustersTmp == null || (clusters = clustersTmp.remove(plotArea.toString())) == null) { 379 if (plotArea.getType() == PlotAreaType.PARTIAL) { 380 clusters = this.clustersTmp != null ? 381 this.clustersTmp.get(plotArea.getWorldName()) : 382 null; 383 if (clusters != null) { 384 Iterator<PlotCluster> iterator = clusters.iterator(); 385 while (iterator.hasNext()) { 386 PlotCluster next = iterator.next(); 387 if (next.intersects(plotArea.getMin(), plotArea.getMax())) { 388 next.setArea(plotArea); 389 iterator.remove(); 390 } 391 } 392 } 393 } 394 } else { 395 for (PlotCluster cluster : clusters) { 396 cluster.setArea(plotArea); 397 } 398 } 399 getPlotAreaManager().addPlotArea(plotArea); 400 plotArea.setupBorder(); 401 if (!Settings.Enabled_Components.PERSISTENT_ROAD_REGEN) { 402 return; 403 } 404 File file = new File( 405 this.platform.getDirectory() + File.separator + "persistent_regen_data_" + plotArea.getId() 406 + "_" + plotArea.getWorldName()); 407 if (!file.exists()) { 408 return; 409 } 410 TaskManager.runTaskAsync(() -> { 411 try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) { 412 List<Object> list = (List<Object>) ois.readObject(); 413 ArrayList<int[]> regionInts = (ArrayList<int[]>) list.get(0); 414 ArrayList<int[]> chunkInts = (ArrayList<int[]>) list.get(1); 415 HashSet<BlockVector2> regions = new HashSet<>(); 416 Set<BlockVector2> chunks = new HashSet<>(); 417 regionInts.forEach(l -> regions.add(BlockVector2.at(l[0], l[1]))); 418 chunkInts.forEach(l -> chunks.add(BlockVector2.at(l[0], l[1]))); 419 int height = (int) list.get(2); 420 LOGGER.info( 421 "Incomplete road regeneration found. Restarting in world {} with height {}", 422 plotArea.getWorldName(), 423 height 424 ); 425 LOGGER.info("- Regions: {}", regions.size()); 426 LOGGER.info("- Chunks: {}", chunks.size()); 427 HybridUtils.UPDATE = true; 428 PlotSquared.platform().hybridUtils().scheduleRoadUpdate(plotArea, regions, height, chunks); 429 } catch (IOException | ClassNotFoundException e) { 430 LOGGER.error("Error restarting road regeneration", e); 431 } finally { 432 if (!file.delete()) { 433 LOGGER.error("Error deleting persistent_regen_data_{}. Please delete this file manually", plotArea.getId()); 434 } 435 } 436 }); 437 } 438 439 /** 440 * Remove a plot world reference. 441 * 442 * @param area the {@link PlotArea} to remove 443 */ 444 public void removePlotArea(final @NonNull PlotArea area) { 445 getPlotAreaManager().removePlotArea(area); 446 setPlotsTmp(area); 447 } 448 449 public void removePlotAreas(final @NonNull String world) { 450 for (final PlotArea area : this.getPlotAreaManager().getPlotAreasSet(world)) { 451 if (area.getWorldName().equals(world)) { 452 removePlotArea(area); 453 } 454 } 455 } 456 457 private void setPlotsTmp(final @NonNull PlotArea area) { 458 if (this.plots_tmp == null) { 459 this.plots_tmp = new HashMap<>(); 460 } 461 HashMap<PlotId, Plot> map = 462 this.plots_tmp.computeIfAbsent(area.toString(), k -> new HashMap<>()); 463 for (Plot plot : area.getPlots()) { 464 map.put(plot.getId(), plot); 465 } 466 if (this.clustersTmp == null) { 467 this.clustersTmp = new HashMap<>(); 468 } 469 this.clustersTmp.put(area.toString(), area.getClusters()); 470 } 471 472 public Set<PlotCluster> getClusters(final @NonNull String world) { 473 final Set<PlotCluster> set = new HashSet<>(); 474 for (final PlotArea area : this.getPlotAreaManager().getPlotAreasSet(world)) { 475 set.addAll(area.getClusters()); 476 } 477 return Collections.unmodifiableSet(set); 478 479 } 480 481 public List<Plot> sortPlotsByTemp(Collection<Plot> plots) { 482 int max = 0; 483 int overflowCount = 0; 484 for (Plot plot : plots) { 485 if (plot.temp > 0) { 486 if (plot.temp > max) { 487 max = plot.temp; 488 } 489 } else { 490 overflowCount++; 491 } 492 } 493 Plot[] array = new Plot[max + 1]; 494 List<Plot> overflow = new ArrayList<>(overflowCount); 495 for (Plot plot : plots) { 496 if (plot.temp <= 0) { 497 overflow.add(plot); 498 } else { 499 array[plot.temp] = plot; 500 } 501 } 502 ArrayList<Plot> result = new ArrayList<>(plots.size()); 503 for (Plot plot : array) { 504 if (plot != null) { 505 result.add(plot); 506 } 507 } 508 overflow.sort(Comparator.comparingInt(Plot::hashCode)); 509 result.addAll(overflow); 510 return result; 511 } 512 513 /** 514 * Sort plots by hashcode. 515 * 516 * @param plots the collection of plots to sort 517 * @return the sorted collection 518 */ 519 private ArrayList<Plot> sortPlotsByHash(Collection<Plot> plots) { 520 int hardmax = 256000; 521 int max = 0; 522 int overflowSize = 0; 523 for (Plot plot : plots) { 524 int hash = MathMan.getPositiveId(plot.hashCode()); 525 if (hash > max) { 526 if (hash >= hardmax) { 527 overflowSize++; 528 } else { 529 max = hash; 530 } 531 } 532 } 533 hardmax = Math.min(hardmax, max); 534 Plot[] cache = new Plot[hardmax + 1]; 535 List<Plot> overflow = new ArrayList<>(overflowSize); 536 ArrayList<Plot> extra = new ArrayList<>(); 537 for (Plot plot : plots) { 538 int hash = MathMan.getPositiveId(plot.hashCode()); 539 if (hash < hardmax) { 540 if (hash >= 0) { 541 cache[hash] = plot; 542 } else { 543 extra.add(plot); 544 } 545 } else if (Math.abs(plot.getId().getX()) > 15446 || Math.abs(plot.getId().getY()) > 15446) { 546 extra.add(plot); 547 } else { 548 overflow.add(plot); 549 } 550 } 551 Plot[] overflowArray = overflow.toArray(new Plot[0]); 552 sortPlotsByHash(overflowArray); 553 ArrayList<Plot> result = new ArrayList<>(cache.length + overflowArray.length); 554 for (Plot plot : cache) { 555 if (plot != null) { 556 result.add(plot); 557 } 558 } 559 Collections.addAll(result, overflowArray); 560 result.addAll(extra); 561 return result; 562 } 563 564 /** 565 * Unchecked, use {@link #sortPlots(Collection, SortType, PlotArea)} instead which will in turn call this. 566 * 567 * @param input an array of plots to sort 568 */ 569 @SuppressWarnings("unchecked") 570 private void sortPlotsByHash(final @NonNull Plot @NonNull [] input) { 571 List<Plot>[] bucket = new ArrayList[32]; 572 Arrays.fill(bucket, new ArrayList<>()); 573 boolean maxLength = false; 574 int placement = 1; 575 while (!maxLength) { 576 maxLength = true; 577 for (Plot plot : input) { 578 int tmp = MathMan.getPositiveId(plot.hashCode()) / placement; 579 bucket[tmp & 31].add(plot); 580 if (maxLength && tmp > 0) { 581 maxLength = false; 582 } 583 } 584 int a = 0; 585 for (int i = 0; i < 32; i++) { 586 for (Plot plot : bucket[i]) { 587 input[a++] = plot; 588 } 589 bucket[i].clear(); 590 } 591 placement *= 32; 592 } 593 } 594 595 private @NonNull List<Plot> sortPlotsByTimestamp(final @NonNull Collection<Plot> plots) { 596 int hardMax = 256000; 597 int max = 0; 598 int overflowSize = 0; 599 for (final Plot plot : plots) { 600 int hash = MathMan.getPositiveId(plot.hashCode()); 601 if (hash > max) { 602 if (hash >= hardMax) { 603 overflowSize++; 604 } else { 605 max = hash; 606 } 607 } 608 } 609 hardMax = Math.min(hardMax, max); 610 Plot[] cache = new Plot[hardMax + 1]; 611 List<Plot> overflow = new ArrayList<>(overflowSize); 612 ArrayList<Plot> extra = new ArrayList<>(); 613 for (Plot plot : plots) { 614 int hash = MathMan.getPositiveId(plot.hashCode()); 615 if (hash < hardMax) { 616 if (hash >= 0) { 617 cache[hash] = plot; 618 } else { 619 extra.add(plot); 620 } 621 } else if (Math.abs(plot.getId().getX()) > 15446 || Math.abs(plot.getId().getY()) > 15446) { 622 extra.add(plot); 623 } else { 624 overflow.add(plot); 625 } 626 } 627 Plot[] overflowArray = overflow.toArray(new Plot[0]); 628 sortPlotsByHash(overflowArray); 629 ArrayList<Plot> result = new ArrayList<>(cache.length + overflowArray.length); 630 for (Plot plot : cache) { 631 if (plot != null) { 632 result.add(plot); 633 } 634 } 635 Collections.addAll(result, overflowArray); 636 result.addAll(extra); 637 return result; 638 } 639 640 /** 641 * Sort plots by creation timestamp. 642 * 643 * @param input Plots to sort 644 * @return Sorted list 645 */ 646 private @NonNull List<Plot> sortPlotsByModified(final @NonNull Collection<Plot> input) { 647 List<Plot> list; 648 if (input instanceof List) { 649 list = (List<Plot>) input; 650 } else { 651 list = new ArrayList<>(input); 652 } 653 ExpireManager expireManager = PlotSquared.platform().expireManager(); 654 list.sort(Comparator.comparingLong(a -> expireManager.getTimestamp(a.getOwnerAbs()))); 655 return list; 656 } 657 658 /** 659 * Sort a collection of plots by world (with a priority world), then 660 * by hashcode. 661 * 662 * @param plots the plots to sort 663 * @param type The sorting method to use for each world (timestamp, or hash) 664 * @param priorityArea Use null, "world", or "gibberish" if you 665 * want default world order 666 * @return ArrayList of plot 667 */ 668 public @NonNull List<Plot> sortPlots( 669 final @NonNull Collection<Plot> plots, 670 final @NonNull SortType type, 671 final @Nullable PlotArea priorityArea 672 ) { 673 // group by world 674 // sort each 675 HashMap<PlotArea, Collection<Plot>> map = new HashMap<>(); 676 int totalSize = Arrays.stream(this.getPlotAreaManager().getAllPlotAreas()).mapToInt(PlotArea::getPlotCount).sum(); 677 if (plots.size() == totalSize) { 678 for (PlotArea area : getPlotAreaManager().getAllPlotAreas()) { 679 map.put(area, area.getPlots()); 680 } 681 } else { 682 for (PlotArea area : getPlotAreaManager().getAllPlotAreas()) { 683 map.put(area, new ArrayList<>(0)); 684 } 685 Collection<Plot> lastList = null; 686 PlotArea lastWorld = null; 687 for (Plot plot : plots) { 688 if (lastWorld == plot.getArea()) { 689 lastList.add(plot); 690 } else { 691 lastWorld = plot.getArea(); 692 lastList = map.get(lastWorld); 693 lastList.add(plot); 694 } 695 } 696 } 697 List<PlotArea> areas = Arrays.asList(getPlotAreaManager().getAllPlotAreas()); 698 areas.sort((a, b) -> { 699 if (priorityArea != null) { 700 if (a.equals(priorityArea)) { 701 return -1; 702 } else if (b.equals(priorityArea)) { 703 return 1; 704 } 705 } 706 return a.hashCode() - b.hashCode(); 707 }); 708 ArrayList<Plot> toReturn = new ArrayList<>(plots.size()); 709 for (PlotArea area : areas) { 710 switch (type) { 711 case CREATION_DATE -> toReturn.addAll(sortPlotsByTemp(map.get(area))); 712 case CREATION_DATE_TIMESTAMP -> toReturn.addAll(sortPlotsByTimestamp(map.get(area))); 713 case DISTANCE_FROM_ORIGIN -> toReturn.addAll(sortPlotsByHash(map.get(area))); 714 case LAST_MODIFIED -> toReturn.addAll(sortPlotsByModified(map.get(area))); 715 default -> { 716 } 717 } 718 } 719 return toReturn; 720 } 721 722 public void setPlots(final @NonNull Map<String, HashMap<PlotId, Plot>> plots) { 723 if (this.plots_tmp == null) { 724 this.plots_tmp = new HashMap<>(); 725 } 726 for (final Entry<String, HashMap<PlotId, Plot>> entry : plots.entrySet()) { 727 final String world = entry.getKey(); 728 final PlotArea plotArea = this.getPlotAreaManager().getPlotArea(world, null); 729 if (plotArea == null) { 730 Map<PlotId, Plot> map = this.plots_tmp.computeIfAbsent(world, k -> new HashMap<>()); 731 map.putAll(entry.getValue()); 732 } else { 733 for (Plot plot : entry.getValue().values()) { 734 plot.setArea(plotArea); 735 plotArea.addPlot(plot); 736 } 737 } 738 } 739 } 740 741 /** 742 * Unregisters a plot from local memory without calling the database. 743 * 744 * @param plot the plot to remove 745 * @param callEvent If to call an event about the plot being removed 746 * @return {@code true} if plot existed | {@code false} if it didn't 747 */ 748 public boolean removePlot( 749 final @NonNull Plot plot, 750 final boolean callEvent 751 ) { 752 if (plot == null) { 753 return false; 754 } 755 if (callEvent) { 756 eventDispatcher.callDelete(plot); 757 } 758 if (plot.getArea().removePlot(plot.getId())) { 759 PlotId last = (PlotId) plot.getArea().getMeta("lastPlot"); 760 int last_max = Math.max(Math.abs(last.getX()), Math.abs(last.getY())); 761 int this_max = Math.max(Math.abs(plot.getId().getX()), Math.abs(plot.getId().getY())); 762 if (this_max < last_max) { 763 plot.getArea().setMeta("lastPlot", plot.getId()); 764 } 765 if (callEvent) { 766 eventDispatcher.callPostDelete(plot); 767 } 768 return true; 769 } 770 return false; 771 } 772 773 /** 774 * This method is called by the PlotGenerator class normally. 775 * <ul> 776 * <li>Initializes the PlotArea and PlotManager classes 777 * <li>Registers the PlotArea and PlotManager classes 778 * <li>Loads (and/or generates) the PlotArea configuration 779 * <li>Sets up the world border if configured 780 * </ul> 781 * 782 * <p>If loading an augmented plot world: 783 * <ul> 784 * <li>Creates the AugmentedPopulator classes 785 * <li>Injects the AugmentedPopulator classes if required 786 * </ul> 787 * 788 * @param world the world to load 789 * @param baseGenerator The generator for that world, or null 790 */ 791 public void loadWorld( 792 final @NonNull String world, 793 final @Nullable GeneratorWrapper<?> baseGenerator 794 ) { 795 if (world.equals("CheckingPlotSquaredGenerator")) { 796 return; 797 } 798 // Don't check the return result -> breaks runtime loading of single plot areas on creation 799 this.getPlotAreaManager().addWorld(world); 800 Set<String> worlds; 801 if (this.worldConfiguration.contains("worlds")) { 802 worlds = this.worldConfiguration.getConfigurationSection("worlds").getKeys(false); 803 } else { 804 worlds = new HashSet<>(); 805 } 806 String path = "worlds." + world; 807 ConfigurationSection worldSection = this.worldConfiguration.getConfigurationSection(path); 808 PlotAreaType type; 809 if (worldSection != null) { 810 type = ConfigurationUtil.getType(worldSection); 811 } else { 812 type = PlotAreaType.NORMAL; 813 } 814 if (type == PlotAreaType.NORMAL) { 815 if (getPlotAreaManager().getPlotAreas(world, null).length != 0) { 816 return; 817 } 818 IndependentPlotGenerator plotGenerator; 819 if (baseGenerator != null && baseGenerator.isFull()) { 820 plotGenerator = baseGenerator.getPlotGenerator(); 821 } else if (worldSection != null) { 822 String secondaryGeneratorName = worldSection.getString("generator.plugin"); 823 GeneratorWrapper<?> secondaryGenerator = 824 this.platform.getGenerator(world, secondaryGeneratorName); 825 if (secondaryGenerator != null && secondaryGenerator.isFull()) { 826 plotGenerator = secondaryGenerator.getPlotGenerator(); 827 } else { 828 String primaryGeneratorName = worldSection.getString("generator.init"); 829 GeneratorWrapper<?> primaryGenerator = 830 this.platform.getGenerator(world, primaryGeneratorName); 831 if (primaryGenerator != null && primaryGenerator.isFull()) { 832 plotGenerator = primaryGenerator.getPlotGenerator(); 833 } else { 834 return; 835 } 836 } 837 } else { 838 return; 839 } 840 // Conventional plot generator 841 PlotArea plotArea = plotGenerator.getNewPlotArea(world, null, null, null); 842 PlotManager plotManager = plotArea.getPlotManager(); 843 LOGGER.info("Detected world load for '{}'", world); 844 LOGGER.info("- generator: {}>{}", baseGenerator, plotGenerator); 845 LOGGER.info("- plot world: {}", plotArea.getClass().getCanonicalName()); 846 LOGGER.info("- plot area manager: {}", plotManager.getClass().getCanonicalName()); 847 if (!this.worldConfiguration.contains(path)) { 848 this.worldConfiguration.createSection(path); 849 worldSection = this.worldConfiguration.getConfigurationSection(path); 850 } 851 plotArea.saveConfiguration(worldSection); 852 plotArea.loadDefaultConfiguration(worldSection); 853 try { 854 this.worldConfiguration.save(this.worldsFile); 855 } catch (IOException e) { 856 e.printStackTrace(); 857 } 858 // Now add it 859 addPlotArea(plotArea); 860 plotGenerator.initialize(plotArea); 861 } else { 862 if (!worlds.contains(world)) { 863 return; 864 } 865 ConfigurationSection areasSection = worldSection.getConfigurationSection("areas"); 866 if (areasSection == null) { 867 if (getPlotAreaManager().getPlotAreas(world, null).length != 0) { 868 return; 869 } 870 LOGGER.info("Detected world load for '{}'", world); 871 String gen_string = worldSection.getString("generator.plugin", platform.pluginName()); 872 if (type == PlotAreaType.PARTIAL) { 873 Set<PlotCluster> clusters = 874 this.clustersTmp != null ? this.clustersTmp.get(world) : new HashSet<>(); 875 if (clusters == null) { 876 throw new IllegalArgumentException("No cluster exists for world: " + world); 877 } 878 ArrayDeque<PlotArea> toLoad = new ArrayDeque<>(); 879 for (PlotCluster cluster : clusters) { 880 PlotId pos1 = cluster.getP1(); // Cluster pos1 881 PlotId pos2 = cluster.getP2(); // Cluster pos2 882 String name = cluster.getName(); // Cluster name 883 String fullId = name + "-" + pos1 + "-" + pos2; 884 worldSection.createSection("areas." + fullId); 885 DBFunc.replaceWorld(world, world + ";" + name, pos1, pos2); // NPE 886 LOGGER.info("- {}-{}-{}", name, pos1, pos2); 887 GeneratorWrapper<?> areaGen = this.platform.getGenerator(world, gen_string); 888 if (areaGen == null) { 889 throw new IllegalArgumentException("Invalid Generator: " + gen_string); 890 } 891 PlotArea pa = 892 areaGen.getPlotGenerator().getNewPlotArea(world, name, pos1, pos2); 893 pa.saveConfiguration(worldSection); 894 pa.loadDefaultConfiguration(worldSection); 895 try { 896 this.worldConfiguration.save(this.worldsFile); 897 } catch (IOException e) { 898 e.printStackTrace(); 899 } 900 LOGGER.info("| generator: {}>{}", baseGenerator, areaGen); 901 LOGGER.info("| plot world: {}", pa.getClass().getCanonicalName()); 902 LOGGER.info("| manager: {}", pa.getPlotManager().getClass().getCanonicalName()); 903 LOGGER.info("Note: Area created for cluster '{}' (invalid or old configuration?)", name); 904 areaGen.getPlotGenerator().initialize(pa); 905 areaGen.augment(pa); 906 toLoad.add(pa); 907 } 908 for (PlotArea area : toLoad) { 909 addPlotArea(area); 910 } 911 return; 912 } 913 GeneratorWrapper<?> areaGen = this.platform.getGenerator(world, gen_string); 914 if (areaGen == null) { 915 throw new IllegalArgumentException("Invalid Generator: " + gen_string); 916 } 917 PlotArea pa = areaGen.getPlotGenerator().getNewPlotArea(world, null, null, null); 918 LOGGER.info("- generator: {}>{}", baseGenerator, areaGen); 919 LOGGER.info("- plot world: {}", pa.getClass().getCanonicalName()); 920 LOGGER.info("- plot area manager: {}", pa.getPlotManager().getClass().getCanonicalName()); 921 if (!this.worldConfiguration.contains(path)) { 922 this.worldConfiguration.createSection(path); 923 worldSection = this.worldConfiguration.getConfigurationSection(path); 924 } 925 pa.saveConfiguration(worldSection); 926 pa.loadDefaultConfiguration(worldSection); 927 try { 928 this.worldConfiguration.save(this.worldsFile); 929 } catch (IOException e) { 930 e.printStackTrace(); 931 } 932 areaGen.getPlotGenerator().initialize(pa); 933 areaGen.augment(pa); 934 addPlotArea(pa); 935 return; 936 } 937 if (type == PlotAreaType.AUGMENTED) { 938 throw new IllegalArgumentException( 939 "Invalid type for multi-area world. Expected `PARTIAL`, got `" 940 + PlotAreaType.AUGMENTED + "`"); 941 } 942 for (String areaId : areasSection.getKeys(false)) { 943 LOGGER.info("- {}", areaId); 944 String[] split = areaId.split("(?<=[^;-])-"); 945 if (split.length != 3) { 946 throw new IllegalArgumentException("Invalid Area identifier: " + areaId 947 + ". Expected form `<name>-<pos1>-<pos2>`"); 948 } 949 String name = split[0]; 950 PlotId pos1 = PlotId.fromString(split[1]); 951 PlotId pos2 = PlotId.fromString(split[2]); 952 if (name.isEmpty()) { 953 throw new IllegalArgumentException("Invalid Area identifier: " + areaId 954 + ". Expected form `<name>-<x1;z1>-<x2;z2>`"); 955 } 956 final PlotArea existing = this.getPlotAreaManager().getPlotArea(world, name); 957 if (existing != null && name.equals(existing.getId())) { 958 continue; 959 } 960 ConfigurationSection section = areasSection.getConfigurationSection(areaId); 961 YamlConfiguration clone = new YamlConfiguration(); 962 for (String key : section.getKeys(true)) { 963 if (section.get(key) instanceof MemorySection) { 964 continue; 965 } 966 if (!clone.contains(key)) { 967 clone.set(key, section.get(key)); 968 } 969 } 970 for (String key : worldSection.getKeys(true)) { 971 if (worldSection.get(key) instanceof MemorySection) { 972 continue; 973 } 974 if (!key.startsWith("areas") && !clone.contains(key)) { 975 clone.set(key, worldSection.get(key)); 976 } 977 } 978 String gen_string = clone.getString("generator.plugin", platform.pluginName()); 979 GeneratorWrapper<?> areaGen = this.platform.getGenerator(world, gen_string); 980 if (areaGen == null) { 981 throw new IllegalArgumentException("Invalid Generator: " + gen_string); 982 } 983 PlotArea pa = areaGen.getPlotGenerator().getNewPlotArea(world, name, pos1, pos2); 984 pa.saveConfiguration(clone); 985 // netSections is the combination of 986 for (String key : clone.getKeys(true)) { 987 if (clone.get(key) instanceof MemorySection) { 988 continue; 989 } 990 if (!worldSection.contains(key)) { 991 worldSection.set(key, clone.get(key)); 992 } else { 993 Object value = worldSection.get(key); 994 if (!Objects.equals(value, clone.get(key))) { 995 section.set(key, clone.get(key)); 996 } 997 } 998 } 999 pa.loadDefaultConfiguration(clone); 1000 try { 1001 this.worldConfiguration.save(this.worldsFile); 1002 } catch (IOException e) { 1003 e.printStackTrace(); 1004 } 1005 LOGGER.info("Detected area load for '{}'", world); 1006 LOGGER.info("| generator: {}>{}", baseGenerator, areaGen); 1007 LOGGER.info("| plot world: {}", pa); 1008 LOGGER.info("| manager: {}", pa.getPlotManager()); 1009 areaGen.getPlotGenerator().initialize(pa); 1010 areaGen.augment(pa); 1011 addPlotArea(pa); 1012 } 1013 } 1014 } 1015 1016 /** 1017 * Setup the configuration for a plot world based on world arguments. 1018 * <p> 1019 * 1020 * <i>e.g. /mv create <world> normal -g PlotSquared:<args></i> 1021 * 1022 * @param world The name of the world 1023 * @param args The arguments 1024 * @param generator the plot generator 1025 * @return boolean | if valid arguments were provided 1026 */ 1027 public boolean setupPlotWorld( 1028 final @NonNull String world, 1029 final @Nullable String args, 1030 final @NonNull IndependentPlotGenerator generator 1031 ) { 1032 if (args != null && !args.isEmpty()) { 1033 // save configuration 1034 1035 final List<String> validArguments = Arrays 1036 .asList("s=", "size=", "g=", "gap=", "h=", "height=", "minh=", "minheight=", "maxh=", "maxheight=", 1037 "f=", "floor=", "m=", "main=", "w=", "wall=", "b=", "border=" 1038 ); 1039 1040 // Calculate the number of expected arguments 1041 int expected = (int) validArguments.stream() 1042 .filter(validArgument -> args.toLowerCase(Locale.ENGLISH).contains(validArgument)) 1043 .count(); 1044 1045 String[] split = args.toLowerCase(Locale.ENGLISH).split(",(?![^\\(\\[]*[\\]\\)])"); 1046 1047 if (split.length > expected) { 1048 // This means we have multi-block block buckets 1049 String[] combinedArgs = new String[expected]; 1050 int index = 0; 1051 1052 StringBuilder argBuilder = new StringBuilder(); 1053 outer: 1054 for (final String string : split) { 1055 for (final String validArgument : validArguments) { 1056 if (string.contains(validArgument)) { 1057 if (!argBuilder.toString().isEmpty()) { 1058 combinedArgs[index++] = argBuilder.toString(); 1059 argBuilder = new StringBuilder(); 1060 } 1061 argBuilder.append(string); 1062 continue outer; 1063 } 1064 } 1065 if (argBuilder.toString().charAt(argBuilder.length() - 1) != '=') { 1066 argBuilder.append(","); 1067 } 1068 argBuilder.append(string); 1069 } 1070 1071 if (!argBuilder.toString().isEmpty()) { 1072 combinedArgs[index] = argBuilder.toString(); 1073 } 1074 1075 split = combinedArgs; 1076 } 1077 1078 final HybridPlotWorldFactory hybridPlotWorldFactory = this.platform 1079 .injector() 1080 .getInstance(HybridPlotWorldFactory.class); 1081 final HybridPlotWorld plotWorld = hybridPlotWorldFactory.create(world, null, generator, null, null); 1082 1083 for (String element : split) { 1084 String[] pair = element.split("="); 1085 if (pair.length != 2) { 1086 LOGGER.error("No value provided for '{}'", element); 1087 return false; 1088 } 1089 String key = pair[0].toLowerCase(); 1090 String value = pair[1]; 1091 try { 1092 String base = "worlds." + world + "."; 1093 switch (key) { 1094 case "s", "size" -> this.worldConfiguration.set( 1095 base + "plot.size", 1096 ConfigurationUtil.INTEGER.parseString(value).shortValue() 1097 ); 1098 case "g", "gap" -> this.worldConfiguration.set( 1099 base + "road.width", 1100 ConfigurationUtil.INTEGER.parseString(value).shortValue() 1101 ); 1102 case "h", "height" -> { 1103 this.worldConfiguration.set( 1104 base + "road.height", 1105 ConfigurationUtil.INTEGER.parseString(value).shortValue() 1106 ); 1107 this.worldConfiguration.set( 1108 base + "plot.height", 1109 ConfigurationUtil.INTEGER.parseString(value).shortValue() 1110 ); 1111 this.worldConfiguration.set( 1112 base + "wall.height", 1113 ConfigurationUtil.INTEGER.parseString(value).shortValue() 1114 ); 1115 } 1116 case "minh", "minheight" -> this.worldConfiguration.set( 1117 base + "world.min_gen_height", 1118 ConfigurationUtil.INTEGER.parseString(value).shortValue() 1119 ); 1120 case "maxh", "maxheight" -> this.worldConfiguration.set( 1121 base + "world.max_gen_height", 1122 ConfigurationUtil.INTEGER.parseString(value).shortValue() 1123 ); 1124 case "f", "floor" -> this.worldConfiguration.set( 1125 base + "plot.floor", 1126 ConfigurationUtil.BLOCK_BUCKET.parseString(value).toString() 1127 ); 1128 case "m", "main" -> this.worldConfiguration.set( 1129 base + "plot.filling", 1130 ConfigurationUtil.BLOCK_BUCKET.parseString(value).toString() 1131 ); 1132 case "w", "wall" -> this.worldConfiguration.set( 1133 base + "wall.filling", 1134 ConfigurationUtil.BLOCK_BUCKET.parseString(value).toString() 1135 ); 1136 case "b", "border" -> this.worldConfiguration.set( 1137 base + "wall.block", 1138 ConfigurationUtil.BLOCK_BUCKET.parseString(value).toString() 1139 ); 1140 default -> { 1141 LOGGER.error("Key not found: {}", element); 1142 return false; 1143 } 1144 } 1145 } catch (Exception e) { 1146 LOGGER.error("Invalid value '{}' for arg '{}'", value, element); 1147 e.printStackTrace(); 1148 return false; 1149 } 1150 } 1151 try { 1152 ConfigurationSection section = 1153 this.worldConfiguration.getConfigurationSection("worlds." + world); 1154 plotWorld.saveConfiguration(section); 1155 plotWorld.loadDefaultConfiguration(section); 1156 this.worldConfiguration.save(this.worldsFile); 1157 } catch (IOException e) { 1158 e.printStackTrace(); 1159 } 1160 } 1161 return true; 1162 } 1163 1164 /** 1165 * Copies a file from inside the jar to a location 1166 * 1167 * @param file Name of the file inside PlotSquared.jar 1168 * @param folder The output location relative to /plugins/PlotSquared/ 1169 */ 1170 public void copyFile( 1171 final @NonNull String file, 1172 final @NonNull String folder 1173 ) { 1174 try { 1175 File output = this.platform.getDirectory(); 1176 if (!output.exists()) { 1177 output.mkdirs(); 1178 } 1179 File newFile = FileUtils.getFile(output, folder + File.separator + file); 1180 if (newFile.exists()) { 1181 return; 1182 } 1183 try (InputStream stream = this.platform.getClass().getResourceAsStream(file)) { 1184 byte[] buffer = new byte[2048]; 1185 if (stream == null) { 1186 try (ZipInputStream zis = new ZipInputStream( 1187 new FileInputStream(this.jarFile))) { 1188 ZipEntry ze = zis.getNextEntry(); 1189 while (ze != null) { 1190 String name = ze.getName(); 1191 if (name.equals(file)) { 1192 new File(newFile.getParent()).mkdirs(); 1193 try (FileOutputStream fos = new FileOutputStream(newFile)) { 1194 int len; 1195 while ((len = zis.read(buffer)) > 0) { 1196 fos.write(buffer, 0, len); 1197 } 1198 } 1199 ze = null; 1200 } else { 1201 ze = zis.getNextEntry(); 1202 } 1203 } 1204 zis.closeEntry(); 1205 } 1206 return; 1207 } 1208 newFile.createNewFile(); 1209 try (FileOutputStream fos = new FileOutputStream(newFile)) { 1210 int len; 1211 while ((len = stream.read(buffer)) > 0) { 1212 fos.write(buffer, 0, len); 1213 } 1214 } 1215 } 1216 } catch (IOException e) { 1217 LOGGER.error("Could not save {}", file); 1218 e.printStackTrace(); 1219 } 1220 } 1221 1222 /** 1223 * Safely closes the database connection. 1224 */ 1225 public void disable() { 1226 try { 1227 eventDispatcher.unregisterAll(); 1228 checkRoadRegenPersistence(); 1229 // Validate that all data in the db is correct 1230 final HashSet<Plot> plots = new HashSet<>(); 1231 try { 1232 forEachPlotRaw(plots::add); 1233 } catch (final Exception ignored) { 1234 } 1235 DBFunc.validatePlots(plots); 1236 1237 // Close the connection 1238 DBFunc.close(); 1239 } catch (NullPointerException throwable) { 1240 LOGGER.error("Could not close database connection", throwable); 1241 throwable.printStackTrace(); 1242 } 1243 } 1244 1245 /** 1246 * Handle road regen persistence 1247 */ 1248 private void checkRoadRegenPersistence() { 1249 if (!HybridUtils.UPDATE || !Settings.Enabled_Components.PERSISTENT_ROAD_REGEN || ( 1250 HybridUtils.regions.isEmpty() && HybridUtils.chunks.isEmpty())) { 1251 return; 1252 } 1253 LOGGER.info("Road regeneration incomplete. Saving incomplete regions to disk"); 1254 LOGGER.info("- regions: {}", HybridUtils.regions.size()); 1255 LOGGER.info("- chunks: {}", HybridUtils.chunks.size()); 1256 ArrayList<int[]> regions = new ArrayList<>(); 1257 ArrayList<int[]> chunks = new ArrayList<>(); 1258 for (BlockVector2 r : HybridUtils.regions) { 1259 regions.add(new int[]{r.getBlockX(), r.getBlockZ()}); 1260 } 1261 for (BlockVector2 c : HybridUtils.chunks) { 1262 chunks.add(new int[]{c.getBlockX(), c.getBlockZ()}); 1263 } 1264 List<Object> list = new ArrayList<>(); 1265 list.add(regions); 1266 list.add(chunks); 1267 list.add(HybridUtils.height); 1268 File file = new File( 1269 this.platform.getDirectory() + File.separator + "persistent_regen_data_" + HybridUtils.area 1270 .getId() + "_" + HybridUtils.area.getWorldName()); 1271 if (file.exists() && !file.delete()) { 1272 LOGGER.error("persistent_regene_data file already exists and could not be deleted"); 1273 return; 1274 } 1275 try (ObjectOutputStream oos = new ObjectOutputStream( 1276 Files.newOutputStream(file.toPath(), StandardOpenOption.CREATE_NEW))) { 1277 oos.writeObject(list); 1278 } catch (IOException e) { 1279 LOGGER.error("Error creating persistent_region_data file", e); 1280 } 1281 } 1282 1283 /** 1284 * Set up the database connection. 1285 */ 1286 public void setupDatabase() { 1287 try { 1288 if (DBFunc.dbManager != null) { 1289 DBFunc.dbManager.close(); 1290 } 1291 Database database; 1292 if (Storage.MySQL.USE) { 1293 database = new MySQL(Storage.MySQL.HOST, Storage.MySQL.PORT, Storage.MySQL.DATABASE, 1294 Storage.MySQL.USER, Storage.MySQL.PASSWORD 1295 ); 1296 } else if (Storage.SQLite.USE) { 1297 File file = FileUtils.getFile(platform.getDirectory(), Storage.SQLite.DB + ".db"); 1298 database = new SQLite(file); 1299 } else { 1300 LOGGER.error("No storage type is set. Disabling PlotSquared"); 1301 this.platform.shutdown(); //shutdown used instead of disable because no database is set 1302 return; 1303 } 1304 DBFunc.dbManager = new SQLManager( 1305 database, 1306 Storage.PREFIX, 1307 this.eventDispatcher, 1308 this.plotListener, 1309 this.worldConfiguration 1310 ); 1311 this.plots_tmp = DBFunc.getPlots(); 1312 if (getPlotAreaManager() instanceof SinglePlotAreaManager) { 1313 SinglePlotArea area = ((SinglePlotAreaManager) getPlotAreaManager()).getArea(); 1314 addPlotArea(area); 1315 ConfigurationSection section = worldConfiguration.getConfigurationSection("worlds.*"); 1316 if (section == null) { 1317 section = worldConfiguration.createSection("worlds.*"); 1318 } 1319 area.saveConfiguration(section); 1320 area.loadDefaultConfiguration(section); 1321 } 1322 this.clustersTmp = DBFunc.getClusters(); 1323 LOGGER.info("Connection to database established. Type: {}", Storage.MySQL.USE ? "MySQL" : "SQLite"); 1324 } catch (ClassNotFoundException | SQLException e) { 1325 LOGGER.error( 1326 "Failed to open database connection ({}). Disabling PlotSquared", 1327 Storage.MySQL.USE ? "MySQL" : "SQLite" 1328 ); 1329 LOGGER.error("==== Here is an ugly stacktrace, if you are interested in those things ==="); 1330 e.printStackTrace(); 1331 LOGGER.error("==== End of stacktrace ===="); 1332 LOGGER.error( 1333 "Please go to the {} 'storage.yml' and configure the database correctly", 1334 platform.pluginName() 1335 ); 1336 this.platform.shutdown(); //shutdown used instead of disable because of database error 1337 } 1338 } 1339 1340 /** 1341 * Setup the default configuration. 1342 */ 1343 public void setupConfig() { 1344 String lastVersionString = this.getConfig().getString("version"); 1345 if (lastVersionString != null) { 1346 String[] split = lastVersionString.split("\\."); 1347 int[] lastVersion = new int[]{Integer.parseInt(split[0]), Integer.parseInt(split[1]), 1348 Integer.parseInt(split[2])}; 1349 if (checkVersion(new int[]{3, 4, 0}, lastVersion)) { 1350 Settings.convertLegacy(configFile); 1351 if (getConfig().contains("worlds")) { 1352 ConfigurationSection worldSection = 1353 getConfig().getConfigurationSection("worlds"); 1354 worldConfiguration.set("worlds", worldSection); 1355 try { 1356 worldConfiguration.save(worldsFile); 1357 } catch (IOException e) { 1358 LOGGER.error("Failed to save worlds.yml", e); 1359 e.printStackTrace(); 1360 } 1361 } 1362 Settings.save(configFile); 1363 } 1364 } 1365 Settings.load(configFile); 1366 //Sets the version information for the settings.yml file 1367 try (InputStream stream = getClass().getResourceAsStream("/plugin.properties")) { 1368 try (BufferedReader br = new BufferedReader(new InputStreamReader(stream))) { 1369 String versionString = br.readLine(); 1370 String commitString = br.readLine(); 1371 String dateString = br.readLine(); 1372 this.version = PlotVersion.tryParse(versionString, commitString, dateString); 1373 } 1374 } catch (IOException throwable) { 1375 throwable.printStackTrace(); 1376 } 1377 Settings.save(configFile); 1378 config = YamlConfiguration.loadConfiguration(configFile); 1379 } 1380 1381 /** 1382 * Setup all configuration files<br> 1383 * - Config: settings.yml<br> 1384 * - Storage: storage.yml<br> 1385 * 1386 * @return success or not 1387 */ 1388 public boolean setupConfigs() { 1389 File folder = new File(this.platform.getDirectory(), "config"); 1390 if (!folder.exists() && !folder.mkdirs()) { 1391 LOGGER.error("Failed to create the {} config folder. Please create it manually", this.platform.getDirectory()); 1392 } 1393 try { 1394 this.worldsFile = new File(folder, "worlds.yml"); 1395 if (!this.worldsFile.exists() && !this.worldsFile.createNewFile()) { 1396 LOGGER.error("Could not create the worlds file. Please create 'worlds.yml' manually"); 1397 } 1398 this.worldConfiguration = YamlConfiguration.loadConfiguration(this.worldsFile); 1399 1400 if (this.worldConfiguration.contains("worlds")) { 1401 if (!this.worldConfiguration.contains("configuration_version") || ( 1402 !this.worldConfiguration.getString("configuration_version") 1403 .equalsIgnoreCase(LegacyConverter.CONFIGURATION_VERSION) && !this.worldConfiguration 1404 .getString("configuration_version").equalsIgnoreCase("v5"))) { 1405 // Conversion needed 1406 LOGGER.info("A legacy configuration file was detected. Conversion will be attempted."); 1407 try { 1408 com.google.common.io.Files 1409 .copy(this.worldsFile, new File(folder, "worlds.yml.old")); 1410 LOGGER.info("A copy of worlds.yml has been saved in the file worlds.yml.old"); 1411 final ConfigurationSection worlds = 1412 this.worldConfiguration.getConfigurationSection("worlds"); 1413 final LegacyConverter converter = new LegacyConverter(worlds); 1414 converter.convert(); 1415 this.worldConfiguration.set("worlds", worlds); 1416 this.setConfigurationVersion(LegacyConverter.CONFIGURATION_VERSION); 1417 LOGGER.info( 1418 "The conversion has finished. PlotSquared will now be disabled and the new configuration file will be used at next startup. Please review the new worlds.yml file. Please note that schematics will not be converted, as we are now using WorldEdit to handle schematics. You need to re-generate the schematics."); 1419 } catch (final Exception e) { 1420 LOGGER.error("Failed to convert the legacy configuration file. See stack trace for information.", e); 1421 } 1422 // Disable plugin 1423 this.platform.shutdown(); 1424 return false; 1425 } 1426 } else { 1427 this.worldConfiguration.set("configuration_version", LegacyConverter.CONFIGURATION_VERSION); 1428 } 1429 } catch (IOException ignored) { 1430 LOGGER.error("Failed to save worlds.yml"); 1431 } 1432 try { 1433 this.configFile = new File(folder, "settings.yml"); 1434 if (!this.configFile.exists() && !this.configFile.createNewFile()) { 1435 LOGGER.error("Could not create the settings file. Please create 'settings.yml' manually"); 1436 } 1437 this.config = YamlConfiguration.loadConfiguration(this.configFile); 1438 setupConfig(); 1439 } catch (IOException ignored) { 1440 LOGGER.error("Failed to save settings.yml"); 1441 } 1442 try { 1443 this.storageFile = new File(folder, "storage.yml"); 1444 if (!this.storageFile.exists() && !this.storageFile.createNewFile()) { 1445 LOGGER.error("Could not create the storage settings file. Please create 'storage.yml' manually"); 1446 } 1447 YamlConfiguration.loadConfiguration(this.storageFile); 1448 setupStorage(); 1449 } catch (IOException ignored) { 1450 LOGGER.error("Failed to save storage.yml"); 1451 } 1452 return true; 1453 } 1454 1455 public @NonNull String getConfigurationVersion() { 1456 return this.worldConfiguration.get("configuration_version", LegacyConverter.CONFIGURATION_VERSION) 1457 .toString(); 1458 } 1459 1460 public void setConfigurationVersion(final @NonNull String newVersion) throws IOException { 1461 this.worldConfiguration.set("configuration_version", newVersion); 1462 this.worldConfiguration.save(this.worldsFile); 1463 } 1464 1465 /** 1466 * Setup the storage file (load + save missing nodes). 1467 */ 1468 private void setupStorage() { 1469 Storage.load(storageFile); 1470 Storage.save(storageFile); 1471 YamlConfiguration.loadConfiguration(storageFile); 1472 } 1473 1474 /** 1475 * Show startup debug information. 1476 */ 1477 private void showDebug() { 1478 if (Settings.DEBUG) { 1479 Map<String, Object> components = Settings.getFields(Settings.Enabled_Components.class); 1480 for (Entry<String, Object> component : components.entrySet()) { 1481 LOGGER.info("Key: {} | Value: {}", component.getKey(), component.getValue()); 1482 } 1483 } 1484 } 1485 1486 public void forEachPlotRaw(final @NonNull Consumer<Plot> consumer) { 1487 for (final PlotArea area : this.getPlotAreaManager().getAllPlotAreas()) { 1488 area.getPlots().forEach(consumer); 1489 } 1490 if (this.plots_tmp != null) { 1491 for (final HashMap<PlotId, Plot> entry : this.plots_tmp.values()) { 1492 entry.values().forEach(consumer); 1493 } 1494 } 1495 } 1496 1497 /** 1498 * Check if the chunk uses vanilla/non-PlotSquared generation 1499 * 1500 * @param world World name 1501 * @param chunkCoordinates Chunk coordinates 1502 * @return {@code true} if the chunk uses non-standard generation, {@code false} if not 1503 */ 1504 public boolean isNonStandardGeneration( 1505 final @NonNull String world, 1506 final @NonNull BlockVector2 chunkCoordinates 1507 ) { 1508 final Location location = Location.at(world, chunkCoordinates.getBlockX() << 4, 64, chunkCoordinates.getBlockZ() << 4); 1509 final PlotArea area = getPlotAreaManager().getApplicablePlotArea(location); 1510 if (area == null) { 1511 return true; 1512 } 1513 return area.getTerrain() != PlotAreaTerrainType.NONE; 1514 } 1515 1516 public @NonNull YamlConfiguration getConfig() { 1517 return config; 1518 } 1519 1520 public @NonNull UUIDPipeline getImpromptuUUIDPipeline() { 1521 return this.impromptuUUIDPipeline; 1522 } 1523 1524 public @NonNull UUIDPipeline getBackgroundUUIDPipeline() { 1525 return this.backgroundUUIDPipeline; 1526 } 1527 1528 public @NonNull WorldEdit getWorldEdit() { 1529 return this.worldedit; 1530 } 1531 1532 public @NonNull File getConfigFile() { 1533 return this.configFile; 1534 } 1535 1536 public @NonNull File getWorldsFile() { 1537 return this.worldsFile; 1538 } 1539 1540 public @NonNull YamlConfiguration getWorldConfiguration() { 1541 return this.worldConfiguration; 1542 } 1543 1544 /** 1545 * Get the caption map belonging to a namespace. If none exists, a dummy 1546 * caption map will be returned. 1547 * <p> 1548 * You can register a caption map by calling {@link #registerCaptionMap(String, CaptionMap)} 1549 * </p> 1550 * 1551 * @param namespace Namespace 1552 * @return Map instance 1553 */ 1554 public @NonNull CaptionMap getCaptionMap(final @NonNull String namespace) { 1555 return this.captionMaps.computeIfAbsent( 1556 namespace.toLowerCase(Locale.ENGLISH), 1557 missingNamespace -> new DummyCaptionMap() 1558 ); 1559 } 1560 1561 /** 1562 * Register a caption map. The namespace needs to be equal to the namespace used for 1563 * the {@link TranslatableCaption}s inside the map. 1564 * 1565 * @param namespace Namespace 1566 * @param captionMap Map instance 1567 */ 1568 public void registerCaptionMap( 1569 final @NonNull String namespace, 1570 final @NonNull CaptionMap captionMap 1571 ) { 1572 if (namespace.equalsIgnoreCase(TranslatableCaption.DEFAULT_NAMESPACE)) { 1573 throw new IllegalArgumentException("Cannot replace default caption map"); 1574 } 1575 this.captionMaps.put(namespace.toLowerCase(Locale.ENGLISH), captionMap); 1576 } 1577 1578 public @NonNull EventDispatcher getEventDispatcher() { 1579 return this.eventDispatcher; 1580 } 1581 1582 public @NonNull PlotListener getPlotListener() { 1583 return this.plotListener; 1584 } 1585 1586 /** 1587 * Get if the {@link PlatformReadyEvent} has been sent by WorldEdit. There is no way to query this within WorldEdit itself. 1588 */ 1589 public boolean isWeInitialised() { 1590 return weInitialised; 1591 } 1592 1593 /** 1594 * Different ways of sorting {@link Plot plots} 1595 */ 1596 public enum SortType { 1597 /** 1598 * Sort plots by their creation, using their index in the database 1599 */ 1600 CREATION_DATE, 1601 /** 1602 * Sort plots by their creation timestamp 1603 */ 1604 CREATION_DATE_TIMESTAMP, 1605 /** 1606 * Sort plots by when they were last modified 1607 */ 1608 LAST_MODIFIED, 1609 /** 1610 * Sort plots based on their distance from the origin of the world 1611 */ 1612 DISTANCE_FROM_ORIGIN 1613 } 1614 1615 private final class WEPlatformReadyListener { 1616 1617 @SuppressWarnings("unused") 1618 @Subscribe(priority = EventHandler.Priority.VERY_EARLY) 1619 public void onPlatformReady(PlatformReadyEvent event) { 1620 weInitialised = true; 1621 WorldEdit.getInstance().getEventBus().unregister(WEPlatformReadyListener.this); 1622 } 1623 1624 } 1625 1626}