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