/* * PlotSquared, a land and world management plugin for Minecraft. * Copyright (C) IntellectualSites * Copyright (C) IntellectualSites team and contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.plotsquared.core; import com.plotsquared.core.configuration.ConfigurationSection; import com.plotsquared.core.configuration.ConfigurationUtil; import com.plotsquared.core.configuration.MemorySection; import com.plotsquared.core.configuration.Settings; import com.plotsquared.core.configuration.Storage; import com.plotsquared.core.configuration.caption.CaptionMap; import com.plotsquared.core.configuration.caption.DummyCaptionMap; import com.plotsquared.core.configuration.caption.TranslatableCaption; import com.plotsquared.core.configuration.caption.load.CaptionLoader; import com.plotsquared.core.configuration.caption.load.DefaultCaptionProvider; import com.plotsquared.core.configuration.file.YamlConfiguration; import com.plotsquared.core.configuration.serialization.ConfigurationSerialization; import com.plotsquared.core.database.DBFunc; import com.plotsquared.core.database.Database; import com.plotsquared.core.database.MySQL; import com.plotsquared.core.database.SQLManager; import com.plotsquared.core.database.SQLite; import com.plotsquared.core.generator.GeneratorWrapper; import com.plotsquared.core.generator.HybridPlotWorld; import com.plotsquared.core.generator.HybridUtils; import com.plotsquared.core.generator.IndependentPlotGenerator; import com.plotsquared.core.inject.factory.HybridPlotWorldFactory; import com.plotsquared.core.listener.PlotListener; import com.plotsquared.core.location.Location; import com.plotsquared.core.player.PlayerMetaDataKeys; import com.plotsquared.core.plot.BlockBucket; import com.plotsquared.core.plot.Plot; import com.plotsquared.core.plot.PlotArea; import com.plotsquared.core.plot.PlotAreaTerrainType; import com.plotsquared.core.plot.PlotAreaType; import com.plotsquared.core.plot.PlotCluster; import com.plotsquared.core.plot.PlotId; import com.plotsquared.core.plot.PlotManager; import com.plotsquared.core.plot.expiration.ExpireManager; import com.plotsquared.core.plot.expiration.ExpiryTask; import com.plotsquared.core.plot.flag.GlobalFlagContainer; import com.plotsquared.core.plot.world.PlotAreaManager; import com.plotsquared.core.plot.world.SinglePlotArea; import com.plotsquared.core.plot.world.SinglePlotAreaManager; import com.plotsquared.core.util.EventDispatcher; import com.plotsquared.core.util.FileUtils; import com.plotsquared.core.util.LegacyConverter; import com.plotsquared.core.util.MathMan; import com.plotsquared.core.util.ReflectionUtils; import com.plotsquared.core.util.task.TaskManager; import com.plotsquared.core.uuid.UUIDPipeline; import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.event.platform.PlatformReadyEvent; import com.sk89q.worldedit.math.BlockVector2; import com.sk89q.worldedit.util.eventbus.EventHandler; import com.sk89q.worldedit.util.eventbus.Subscribe; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Files; import java.nio.file.StandardOpenOption; import java.sql.SQLException; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Set; import java.util.concurrent.Executors; import java.util.function.Consumer; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; /** * An implementation of the core, with a static getter for easy access. */ @SuppressWarnings({"WeakerAccess"}) public class PlotSquared { private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + PlotSquared.class.getSimpleName()); private static @MonotonicNonNull PlotSquared instance; // Implementation private final PlotPlatform platform; // Current thread private final Thread thread; // UUID pipelines private final UUIDPipeline impromptuUUIDPipeline = new UUIDPipeline(Executors.newCachedThreadPool()); private final UUIDPipeline backgroundUUIDPipeline = new UUIDPipeline(Executors.newSingleThreadExecutor()); // Localization private final Map captionMaps = new HashMap<>(); public HashMap> plots_tmp; private CaptionLoader captionLoader; // WorldEdit instance private WorldEdit worldedit; private File configFile; private File worldsFile; private YamlConfiguration worldConfiguration; // Temporary hold the plots/clusters before the worlds load private HashMap> clustersTmp; private YamlConfiguration config; // Platform / Version / Update URL private PlotVersion version; // Files and configuration private File jarFile = null; // This file private File storageFile; private EventDispatcher eventDispatcher; private PlotListener plotListener; private boolean weInitialised; /** * Initialize PlotSquared with the desired Implementation class. * * @param iPlotMain Implementation of {@link PlotPlatform} used * @param platform The platform being used */ public PlotSquared( final @NonNull PlotPlatform iPlotMain, final @NonNull String platform ) { if (instance != null) { throw new IllegalStateException("Cannot re-initialize the PlotSquared singleton"); } instance = this; this.thread = Thread.currentThread(); this.platform = iPlotMain; Settings.PLATFORM = platform; // Initialize the class PlayerMetaDataKeys.load(); // // Register configuration serializable classes // ConfigurationSerialization.registerClass(BlockBucket.class, "BlockBucket"); // load configs before reading from settings if (!setupConfigs()) { return; } this.captionLoader = CaptionLoader.of( Locale.ENGLISH, CaptionLoader.patternExtractor(Pattern.compile("messages_(.*)\\.json")), DefaultCaptionProvider.forClassLoaderFormatString( this.getClass().getClassLoader(), "lang/messages_%s.json" // the path in our jar file ), TranslatableCaption.DEFAULT_NAMESPACE ); // Load caption map try { this.loadCaptionMap(); } catch (final Exception e) { LOGGER.error("Failed to load caption map", e); } // Setup the global flag container GlobalFlagContainer.setup(); try { new ReflectionUtils(this.platform.serverNativePackage()); try { URL logurl = PlotSquared.class.getProtectionDomain().getCodeSource().getLocation(); this.jarFile = new File( new URL(logurl.toURI().toString().split("\\!")[0].replaceAll("jar:file", "file")) .toURI().getPath()); } catch (MalformedURLException | URISyntaxException | SecurityException e) { e.printStackTrace(); this.jarFile = new File(this.platform.getDirectory().getParentFile(), "PlotSquared.jar"); if (!this.jarFile.exists()) { this.jarFile = new File( this.platform.getDirectory().getParentFile(), "PlotSquared-" + platform + ".jar" ); } } this.worldedit = WorldEdit.getInstance(); WorldEdit.getInstance().getEventBus().register(new WEPlatformReadyListener()); // Create Event utility class this.eventDispatcher = new EventDispatcher(this.worldedit); // Create plot listener this.plotListener = new PlotListener(this.eventDispatcher); // Copy files copyFile("town.template", Settings.Paths.TEMPLATES); copyFile("bridge.template", Settings.Paths.TEMPLATES); copyFile("skyblock.template", Settings.Paths.TEMPLATES); showDebug(); } catch (Throwable e) { e.printStackTrace(); } } /** * Gets an instance of PlotSquared. * * @return instance of PlotSquared */ public static @NonNull PlotSquared get() { return PlotSquared.instance; } /** * Get the platform specific implementation of PlotSquared * * @return Platform implementation */ public static @NonNull PlotPlatform platform() { if (instance != null && instance.platform != null) { return instance.platform; } throw new IllegalStateException("Plot platform implementation is missing"); } public void loadCaptionMap() throws Exception { this.platform.copyCaptionMaps(); // Setup localization CaptionMap captionMap; if (Settings.Enabled_Components.PER_USER_LOCALE) { captionMap = this.captionLoader.loadAll(this.platform.getDirectory().toPath().resolve("lang")); } else { String fileName = "messages_" + Settings.Enabled_Components.DEFAULT_LOCALE + ".json"; captionMap = this.captionLoader.loadSingle(this.platform.getDirectory().toPath().resolve("lang").resolve(fileName)); } this.captionMaps.put(TranslatableCaption.DEFAULT_NAMESPACE, captionMap); LOGGER.info( "Loaded caption map for namespace 'plotsquared': {}", this.captionMaps.get(TranslatableCaption.DEFAULT_NAMESPACE).getClass().getCanonicalName() ); } /** * Get the platform specific {@link PlotAreaManager} instance * * @return Plot area manager */ public @NonNull PlotAreaManager getPlotAreaManager() { return this.platform.plotAreaManager(); } public void startExpiryTasks() { if (Settings.Enabled_Components.PLOT_EXPIRY) { ExpireManager.IMP = new ExpireManager(this.eventDispatcher); ExpireManager.IMP.runAutomatedTask(); for (Settings.Auto_Clear settings : Settings.AUTO_CLEAR.getInstances()) { ExpiryTask task = new ExpiryTask(settings, this.getPlotAreaManager()); ExpireManager.IMP.addTask(task); } } } public boolean isMainThread(final @NonNull Thread thread) { return this.thread == thread; } /** * Check if `version` is >= `version2`. * * @param version First version * @param version2 Second version * @return {@code true} if `version` is >= `version2` */ public boolean checkVersion( final int[] version, final int... version2 ) { return version[0] > version2[0] || version[0] == version2[0] && version[1] > version2[1] || version[0] == version2[0] && version[1] == version2[1] && version[2] >= version2[2]; } /** * Gets the current PlotSquared version. * * @return current version in config or null */ public @NonNull PlotVersion getVersion() { return this.version; } /** * Gets the server platform this plugin is running on this is running on. * *

This will be either Bukkit or Sponge

* * @return the server implementation */ public @NonNull String getPlatform() { return Settings.PLATFORM; } /** * Add a global reference to a plot world. *

* You can remove the reference by calling {@link #removePlotArea(PlotArea)} *

* * @param plotArea the {@link PlotArea} to add. */ @SuppressWarnings("unchecked") public void addPlotArea(final @NonNull PlotArea plotArea) { HashMap plots; if (plots_tmp == null || (plots = plots_tmp.remove(plotArea.toString())) == null) { if (plotArea.getType() == PlotAreaType.PARTIAL) { plots = this.plots_tmp != null ? this.plots_tmp.get(plotArea.getWorldName()) : null; if (plots != null) { Iterator> iterator = plots.entrySet().iterator(); while (iterator.hasNext()) { Entry next = iterator.next(); PlotId id = next.getKey(); if (plotArea.contains(id)) { next.getValue().setArea(plotArea); iterator.remove(); } } } } } else { for (Plot entry : plots.values()) { entry.setArea(plotArea); } } Set clusters; if (clustersTmp == null || (clusters = clustersTmp.remove(plotArea.toString())) == null) { if (plotArea.getType() == PlotAreaType.PARTIAL) { clusters = this.clustersTmp != null ? this.clustersTmp.get(plotArea.getWorldName()) : null; if (clusters != null) { Iterator iterator = clusters.iterator(); while (iterator.hasNext()) { PlotCluster next = iterator.next(); if (next.intersects(plotArea.getMin(), plotArea.getMax())) { next.setArea(plotArea); iterator.remove(); } } } } } else { for (PlotCluster cluster : clusters) { cluster.setArea(plotArea); } } getPlotAreaManager().addPlotArea(plotArea); plotArea.setupBorder(); if (!Settings.Enabled_Components.PERSISTENT_ROAD_REGEN) { return; } File file = new File( this.platform.getDirectory() + File.separator + "persistent_regen_data_" + plotArea.getId() + "_" + plotArea.getWorldName()); if (!file.exists()) { return; } TaskManager.runTaskAsync(() -> { try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) { List list = (List) ois.readObject(); ArrayList regionInts = (ArrayList) list.get(0); ArrayList chunkInts = (ArrayList) list.get(1); HashSet regions = new HashSet<>(); Set chunks = new HashSet<>(); regionInts.forEach(l -> regions.add(BlockVector2.at(l[0], l[1]))); chunkInts.forEach(l -> chunks.add(BlockVector2.at(l[0], l[1]))); int height = (int) list.get(2); LOGGER.info( "Incomplete road regeneration found. Restarting in world {} with height {}", plotArea.getWorldName(), height ); LOGGER.info("- Regions: {}", regions.size()); LOGGER.info("- Chunks: {}", chunks.size()); HybridUtils.UPDATE = true; PlotSquared.platform().hybridUtils().scheduleRoadUpdate(plotArea, regions, height, chunks); } catch (IOException | ClassNotFoundException e) { LOGGER.error("Error restarting road regeneration", e); } finally { if (!file.delete()) { LOGGER.error("Error deleting persistent_regen_data_{}. Please delete this file manually", plotArea.getId()); } } }); } /** * Remove a plot world reference. * * @param area the {@link PlotArea} to remove */ public void removePlotArea(final @NonNull PlotArea area) { getPlotAreaManager().removePlotArea(area); setPlotsTmp(area); } public void removePlotAreas(final @NonNull String world) { for (final PlotArea area : this.getPlotAreaManager().getPlotAreasSet(world)) { if (area.getWorldName().equals(world)) { removePlotArea(area); } } } private void setPlotsTmp(final @NonNull PlotArea area) { if (this.plots_tmp == null) { this.plots_tmp = new HashMap<>(); } HashMap map = this.plots_tmp.computeIfAbsent(area.toString(), k -> new HashMap<>()); for (Plot plot : area.getPlots()) { map.put(plot.getId(), plot); } if (this.clustersTmp == null) { this.clustersTmp = new HashMap<>(); } this.clustersTmp.put(area.toString(), area.getClusters()); } public Set getClusters(final @NonNull String world) { final Set set = new HashSet<>(); for (final PlotArea area : this.getPlotAreaManager().getPlotAreasSet(world)) { set.addAll(area.getClusters()); } return Collections.unmodifiableSet(set); } public List sortPlotsByTemp(Collection plots) { int max = 0; int overflowCount = 0; for (Plot plot : plots) { if (plot.temp > 0) { if (plot.temp > max) { max = plot.temp; } } else { overflowCount++; } } Plot[] array = new Plot[max + 1]; List overflow = new ArrayList<>(overflowCount); for (Plot plot : plots) { if (plot.temp <= 0) { overflow.add(plot); } else { array[plot.temp] = plot; } } ArrayList result = new ArrayList<>(plots.size()); for (Plot plot : array) { if (plot != null) { result.add(plot); } } overflow.sort(Comparator.comparingInt(Plot::hashCode)); result.addAll(overflow); return result; } /** * Sort plots by hashcode. * * @param plots the collection of plots to sort * @return the sorted collection */ private ArrayList sortPlotsByHash(Collection plots) { int hardmax = 256000; int max = 0; int overflowSize = 0; for (Plot plot : plots) { int hash = MathMan.getPositiveId(plot.hashCode()); if (hash > max) { if (hash >= hardmax) { overflowSize++; } else { max = hash; } } } hardmax = Math.min(hardmax, max); Plot[] cache = new Plot[hardmax + 1]; List overflow = new ArrayList<>(overflowSize); ArrayList extra = new ArrayList<>(); for (Plot plot : plots) { int hash = MathMan.getPositiveId(plot.hashCode()); if (hash < hardmax) { if (hash >= 0) { cache[hash] = plot; } else { extra.add(plot); } } else if (Math.abs(plot.getId().getX()) > 15446 || Math.abs(plot.getId().getY()) > 15446) { extra.add(plot); } else { overflow.add(plot); } } Plot[] overflowArray = overflow.toArray(new Plot[0]); sortPlotsByHash(overflowArray); ArrayList result = new ArrayList<>(cache.length + overflowArray.length); for (Plot plot : cache) { if (plot != null) { result.add(plot); } } Collections.addAll(result, overflowArray); result.addAll(extra); return result; } /** * Unchecked, use {@link #sortPlots(Collection, SortType, PlotArea)} instead which will in turn call this. * * @param input an array of plots to sort */ @SuppressWarnings("unchecked") private void sortPlotsByHash(final @NonNull Plot @NonNull [] input) { List[] bucket = new ArrayList[32]; Arrays.fill(bucket, new ArrayList<>()); boolean maxLength = false; int placement = 1; while (!maxLength) { maxLength = true; for (Plot plot : input) { int tmp = MathMan.getPositiveId(plot.hashCode()) / placement; bucket[tmp & 31].add(plot); if (maxLength && tmp > 0) { maxLength = false; } } int a = 0; for (int i = 0; i < 32; i++) { for (Plot plot : bucket[i]) { input[a++] = plot; } bucket[i].clear(); } placement *= 32; } } private @NonNull List sortPlotsByTimestamp(final @NonNull Collection plots) { int hardMax = 256000; int max = 0; int overflowSize = 0; for (final Plot plot : plots) { int hash = MathMan.getPositiveId(plot.hashCode()); if (hash > max) { if (hash >= hardMax) { overflowSize++; } else { max = hash; } } } hardMax = Math.min(hardMax, max); Plot[] cache = new Plot[hardMax + 1]; List overflow = new ArrayList<>(overflowSize); ArrayList extra = new ArrayList<>(); for (Plot plot : plots) { int hash = MathMan.getPositiveId(plot.hashCode()); if (hash < hardMax) { if (hash >= 0) { cache[hash] = plot; } else { extra.add(plot); } } else if (Math.abs(plot.getId().getX()) > 15446 || Math.abs(plot.getId().getY()) > 15446) { extra.add(plot); } else { overflow.add(plot); } } Plot[] overflowArray = overflow.toArray(new Plot[0]); sortPlotsByHash(overflowArray); ArrayList result = new ArrayList<>(cache.length + overflowArray.length); for (Plot plot : cache) { if (plot != null) { result.add(plot); } } Collections.addAll(result, overflowArray); result.addAll(extra); return result; } /** * Sort plots by creation timestamp. * * @param input Plots to sort * @return Sorted list */ private @NonNull List sortPlotsByModified(final @NonNull Collection input) { List list; if (input instanceof List) { list = (List) input; } else { list = new ArrayList<>(input); } list.sort(Comparator.comparingLong(a -> ExpireManager.IMP.getTimestamp(a.getOwnerAbs()))); return list; } /** * Sort a collection of plots by world (with a priority world), then * by hashcode. * * @param plots the plots to sort * @param type The sorting method to use for each world (timestamp, or hash) * @param priorityArea Use null, "world", or "gibberish" if you * want default world order * @return ArrayList of plot */ public @NonNull List sortPlots( final @NonNull Collection plots, final @NonNull SortType type, final @Nullable PlotArea priorityArea ) { // group by world // sort each HashMap> map = new HashMap<>(); int totalSize = Arrays.stream(this.getPlotAreaManager().getAllPlotAreas()).mapToInt(PlotArea::getPlotCount).sum(); if (plots.size() == totalSize) { for (PlotArea area : getPlotAreaManager().getAllPlotAreas()) { map.put(area, area.getPlots()); } } else { for (PlotArea area : getPlotAreaManager().getAllPlotAreas()) { map.put(area, new ArrayList<>(0)); } Collection lastList = null; PlotArea lastWorld = null; for (Plot plot : plots) { if (lastWorld == plot.getArea()) { lastList.add(plot); } else { lastWorld = plot.getArea(); lastList = map.get(lastWorld); lastList.add(plot); } } } List areas = Arrays.asList(getPlotAreaManager().getAllPlotAreas()); areas.sort((a, b) -> { if (priorityArea != null) { if (a.equals(priorityArea)) { return -1; } else if (b.equals(priorityArea)) { return 1; } } return a.hashCode() - b.hashCode(); }); ArrayList toReturn = new ArrayList<>(plots.size()); for (PlotArea area : areas) { switch (type) { case CREATION_DATE -> toReturn.addAll(sortPlotsByTemp(map.get(area))); case CREATION_DATE_TIMESTAMP -> toReturn.addAll(sortPlotsByTimestamp(map.get(area))); case DISTANCE_FROM_ORIGIN -> toReturn.addAll(sortPlotsByHash(map.get(area))); case LAST_MODIFIED -> toReturn.addAll(sortPlotsByModified(map.get(area))); default -> { } } } return toReturn; } public void setPlots(final @NonNull Map> plots) { if (this.plots_tmp == null) { this.plots_tmp = new HashMap<>(); } for (final Entry> entry : plots.entrySet()) { final String world = entry.getKey(); final PlotArea plotArea = this.getPlotAreaManager().getPlotArea(world, null); if (plotArea == null) { Map map = this.plots_tmp.computeIfAbsent(world, k -> new HashMap<>()); map.putAll(entry.getValue()); } else { for (Plot plot : entry.getValue().values()) { plot.setArea(plotArea); plotArea.addPlot(plot); } } } } /** * Unregisters a plot from local memory without calling the database. * * @param plot the plot to remove * @param callEvent If to call an event about the plot being removed * @return {@code true} if plot existed | {@code false} if it didn't */ public boolean removePlot( final @NonNull Plot plot, final boolean callEvent ) { if (plot == null) { return false; } if (callEvent) { eventDispatcher.callDelete(plot); } if (plot.getArea().removePlot(plot.getId())) { PlotId last = (PlotId) plot.getArea().getMeta("lastPlot"); int last_max = Math.max(Math.abs(last.getX()), Math.abs(last.getY())); int this_max = Math.max(Math.abs(plot.getId().getX()), Math.abs(plot.getId().getY())); if (this_max < last_max) { plot.getArea().setMeta("lastPlot", plot.getId()); } if (callEvent) { eventDispatcher.callPostDelete(plot); } return true; } return false; } /** * This method is called by the PlotGenerator class normally. *
    *
  • Initializes the PlotArea and PlotManager classes *
  • Registers the PlotArea and PlotManager classes *
  • Loads (and/or generates) the PlotArea configuration *
  • Sets up the world border if configured *
* *

If loading an augmented plot world: *

    *
  • Creates the AugmentedPopulator classes *
  • Injects the AugmentedPopulator classes if required *
* * @param world the world to load * @param baseGenerator The generator for that world, or null */ public void loadWorld( final @NonNull String world, final @Nullable GeneratorWrapper baseGenerator ) { if (world.equals("CheckingPlotSquaredGenerator")) { return; } this.getPlotAreaManager().addWorld(world); Set worlds; if (this.worldConfiguration.contains("worlds")) { worlds = this.worldConfiguration.getConfigurationSection("worlds").getKeys(false); } else { worlds = new HashSet<>(); } String path = "worlds." + world; ConfigurationSection worldSection = this.worldConfiguration.getConfigurationSection(path); PlotAreaType type; if (worldSection != null) { type = ConfigurationUtil.getType(worldSection); } else { type = PlotAreaType.NORMAL; } if (type == PlotAreaType.NORMAL) { if (getPlotAreaManager().getPlotAreas(world, null).length != 0) { return; } IndependentPlotGenerator plotGenerator; if (baseGenerator != null && baseGenerator.isFull()) { plotGenerator = baseGenerator.getPlotGenerator(); } else if (worldSection != null) { String secondaryGeneratorName = worldSection.getString("generator.plugin"); GeneratorWrapper secondaryGenerator = this.platform.getGenerator(world, secondaryGeneratorName); if (secondaryGenerator != null && secondaryGenerator.isFull()) { plotGenerator = secondaryGenerator.getPlotGenerator(); } else { String primaryGeneratorName = worldSection.getString("generator.init"); GeneratorWrapper primaryGenerator = this.platform.getGenerator(world, primaryGeneratorName); if (primaryGenerator != null && primaryGenerator.isFull()) { plotGenerator = primaryGenerator.getPlotGenerator(); } else { return; } } } else { return; } // Conventional plot generator PlotArea plotArea = plotGenerator.getNewPlotArea(world, null, null, null); PlotManager plotManager = plotArea.getPlotManager(); LOGGER.info("Detected world load for '{}'", world); LOGGER.info("- generator: {}>{}", baseGenerator, plotGenerator); LOGGER.info("- plot world: {}", plotArea.getClass().getCanonicalName()); LOGGER.info("- plot area manager: {}", plotManager.getClass().getCanonicalName()); if (!this.worldConfiguration.contains(path)) { this.worldConfiguration.createSection(path); worldSection = this.worldConfiguration.getConfigurationSection(path); } plotArea.saveConfiguration(worldSection); plotArea.loadDefaultConfiguration(worldSection); try { this.worldConfiguration.save(this.worldsFile); } catch (IOException e) { e.printStackTrace(); } // Now add it addPlotArea(plotArea); plotGenerator.initialize(plotArea); } else { if (!worlds.contains(world)) { return; } ConfigurationSection areasSection = worldSection.getConfigurationSection("areas"); if (areasSection == null) { if (getPlotAreaManager().getPlotAreas(world, null).length != 0) { return; } LOGGER.info("Detected world load for '{}'", world); String gen_string = worldSection.getString("generator.plugin", platform.pluginName()); if (type == PlotAreaType.PARTIAL) { Set clusters = this.clustersTmp != null ? this.clustersTmp.get(world) : new HashSet<>(); if (clusters == null) { throw new IllegalArgumentException("No cluster exists for world: " + world); } ArrayDeque toLoad = new ArrayDeque<>(); for (PlotCluster cluster : clusters) { PlotId pos1 = cluster.getP1(); // Cluster pos1 PlotId pos2 = cluster.getP2(); // Cluster pos2 String name = cluster.getName(); // Cluster name String fullId = name + "-" + pos1 + "-" + pos2; worldSection.createSection("areas." + fullId); DBFunc.replaceWorld(world, world + ";" + name, pos1, pos2); // NPE LOGGER.info("- {}-{}-{}", name, pos1, pos2); GeneratorWrapper areaGen = this.platform.getGenerator(world, gen_string); if (areaGen == null) { throw new IllegalArgumentException("Invalid Generator: " + gen_string); } PlotArea pa = areaGen.getPlotGenerator().getNewPlotArea(world, name, pos1, pos2); pa.saveConfiguration(worldSection); pa.loadDefaultConfiguration(worldSection); try { this.worldConfiguration.save(this.worldsFile); } catch (IOException e) { e.printStackTrace(); } LOGGER.info("| generator: {}>{}", baseGenerator, areaGen); LOGGER.info("| plot world: {}", pa.getClass().getCanonicalName()); LOGGER.info("| manager: {}", pa.getPlotManager().getClass().getCanonicalName()); LOGGER.info("Note: Area created for cluster '{}' (invalid or old configuration?)", name); areaGen.getPlotGenerator().initialize(pa); areaGen.augment(pa); toLoad.add(pa); } for (PlotArea area : toLoad) { addPlotArea(area); } return; } GeneratorWrapper areaGen = this.platform.getGenerator(world, gen_string); if (areaGen == null) { throw new IllegalArgumentException("Invalid Generator: " + gen_string); } PlotArea pa = areaGen.getPlotGenerator().getNewPlotArea(world, null, null, null); LOGGER.info("- generator: {}>{}", baseGenerator, areaGen); LOGGER.info("- plot world: {}", pa.getClass().getCanonicalName()); LOGGER.info("- plot area manager: {}", pa.getPlotManager().getClass().getCanonicalName()); if (!this.worldConfiguration.contains(path)) { this.worldConfiguration.createSection(path); worldSection = this.worldConfiguration.getConfigurationSection(path); } pa.saveConfiguration(worldSection); pa.loadDefaultConfiguration(worldSection); try { this.worldConfiguration.save(this.worldsFile); } catch (IOException e) { e.printStackTrace(); } areaGen.getPlotGenerator().initialize(pa); areaGen.augment(pa); addPlotArea(pa); return; } if (type == PlotAreaType.AUGMENTED) { throw new IllegalArgumentException( "Invalid type for multi-area world. Expected `PARTIAL`, got `" + PlotAreaType.AUGMENTED + "`"); } for (String areaId : areasSection.getKeys(false)) { LOGGER.info("- {}", areaId); String[] split = areaId.split("(?<=[^;-])-"); if (split.length != 3) { throw new IllegalArgumentException("Invalid Area identifier: " + areaId + ". Expected form `--`"); } String name = split[0]; PlotId pos1 = PlotId.fromString(split[1]); PlotId pos2 = PlotId.fromString(split[2]); if (name.isEmpty()) { throw new IllegalArgumentException("Invalid Area identifier: " + areaId + ". Expected form `--`"); } final PlotArea existing = this.getPlotAreaManager().getPlotArea(world, name); if (existing != null && name.equals(existing.getId())) { continue; } ConfigurationSection section = areasSection.getConfigurationSection(areaId); YamlConfiguration clone = new YamlConfiguration(); for (String key : section.getKeys(true)) { if (section.get(key) instanceof MemorySection) { continue; } if (!clone.contains(key)) { clone.set(key, section.get(key)); } } for (String key : worldSection.getKeys(true)) { if (worldSection.get(key) instanceof MemorySection) { continue; } if (!key.startsWith("areas") && !clone.contains(key)) { clone.set(key, worldSection.get(key)); } } String gen_string = clone.getString("generator.plugin", platform.pluginName()); GeneratorWrapper areaGen = this.platform.getGenerator(world, gen_string); if (areaGen == null) { throw new IllegalArgumentException("Invalid Generator: " + gen_string); } PlotArea pa = areaGen.getPlotGenerator().getNewPlotArea(world, name, pos1, pos2); pa.saveConfiguration(clone); // netSections is the combination of for (String key : clone.getKeys(true)) { if (clone.get(key) instanceof MemorySection) { continue; } if (!worldSection.contains(key)) { worldSection.set(key, clone.get(key)); } else { Object value = worldSection.get(key); if (!Objects.equals(value, clone.get(key))) { section.set(key, clone.get(key)); } } } pa.loadDefaultConfiguration(clone); try { this.worldConfiguration.save(this.worldsFile); } catch (IOException e) { e.printStackTrace(); } LOGGER.info("Detected area load for '{}'", world); LOGGER.info("| generator: {}>{}", baseGenerator, areaGen); LOGGER.info("| plot world: {}", pa); LOGGER.info("| manager: {}", pa.getPlotManager()); areaGen.getPlotGenerator().initialize(pa); areaGen.augment(pa); addPlotArea(pa); } } } /** * Setup the configuration for a plot world based on world arguments. * * * e.g. /mv create <world> normal -g PlotSquared:<args> * * @param world The name of the world * @param args The arguments * @param generator the plot generator * @return boolean | if valid arguments were provided */ public boolean setupPlotWorld( final @NonNull String world, final @Nullable String args, final @NonNull IndependentPlotGenerator generator ) { if (args != null && !args.isEmpty()) { // save configuration final List validArguments = Arrays .asList("s=", "size=", "g=", "gap=", "h=", "height=", "minh=", "minheight=", "maxh=", "maxheight=", "f=", "floor=", "m=", "main=", "w=", "wall=", "b=", "border=" ); // Calculate the number of expected arguments int expected = (int) validArguments.stream() .filter(validArgument -> args.toLowerCase(Locale.ENGLISH).contains(validArgument)) .count(); String[] split = args.toLowerCase(Locale.ENGLISH).split(",(?![^\\(\\[]*[\\]\\)])"); if (split.length > expected) { // This means we have multi-block block buckets String[] combinedArgs = new String[expected]; int index = 0; StringBuilder argBuilder = new StringBuilder(); outer: for (final String string : split) { for (final String validArgument : validArguments) { if (string.contains(validArgument)) { if (!argBuilder.toString().isEmpty()) { combinedArgs[index++] = argBuilder.toString(); argBuilder = new StringBuilder(); } argBuilder.append(string); continue outer; } } if (argBuilder.toString().charAt(argBuilder.length() - 1) != '=') { argBuilder.append(","); } argBuilder.append(string); } if (!argBuilder.toString().isEmpty()) { combinedArgs[index] = argBuilder.toString(); } split = combinedArgs; } final HybridPlotWorldFactory hybridPlotWorldFactory = this.platform .injector() .getInstance(HybridPlotWorldFactory.class); final HybridPlotWorld plotWorld = hybridPlotWorldFactory.create(world, null, generator, null, null); for (String element : split) { String[] pair = element.split("="); if (pair.length != 2) { LOGGER.error("No value provided for '{}'", element); return false; } String key = pair[0].toLowerCase(); String value = pair[1]; try { String base = "worlds." + world + "."; switch (key) { case "s", "size" -> this.worldConfiguration.set( base + "plot.size", ConfigurationUtil.INTEGER.parseString(value).shortValue() ); case "g", "gap" -> this.worldConfiguration.set( base + "road.width", ConfigurationUtil.INTEGER.parseString(value).shortValue() ); case "h", "height" -> { this.worldConfiguration.set( base + "road.height", ConfigurationUtil.INTEGER.parseString(value).shortValue() ); this.worldConfiguration.set( base + "plot.height", ConfigurationUtil.INTEGER.parseString(value).shortValue() ); this.worldConfiguration.set( base + "wall.height", ConfigurationUtil.INTEGER.parseString(value).shortValue() ); } case "minh", "minheight" -> this.worldConfiguration.set( base + "world.min_gen_height", ConfigurationUtil.INTEGER.parseString(value).shortValue() ); case "maxh", "maxheight" -> this.worldConfiguration.set( base + "world.max_gen_height", ConfigurationUtil.INTEGER.parseString(value).shortValue() ); case "f", "floor" -> this.worldConfiguration.set( base + "plot.floor", ConfigurationUtil.BLOCK_BUCKET.parseString(value).toString() ); case "m", "main" -> this.worldConfiguration.set( base + "plot.filling", ConfigurationUtil.BLOCK_BUCKET.parseString(value).toString() ); case "w", "wall" -> this.worldConfiguration.set( base + "wall.filling", ConfigurationUtil.BLOCK_BUCKET.parseString(value).toString() ); case "b", "border" -> this.worldConfiguration.set( base + "wall.block", ConfigurationUtil.BLOCK_BUCKET.parseString(value).toString() ); default -> { LOGGER.error("Key not found: {}", element); return false; } } } catch (Exception e) { LOGGER.error("Invalid value '{}' for arg '{}'", value, element); e.printStackTrace(); return false; } } try { ConfigurationSection section = this.worldConfiguration.getConfigurationSection("worlds." + world); plotWorld.saveConfiguration(section); plotWorld.loadDefaultConfiguration(section); this.worldConfiguration.save(this.worldsFile); } catch (IOException e) { e.printStackTrace(); } } return true; } /** * Copies a file from inside the jar to a location * * @param file Name of the file inside PlotSquared.jar * @param folder The output location relative to /plugins/PlotSquared/ */ public void copyFile( final @NonNull String file, final @NonNull String folder ) { try { File output = this.platform.getDirectory(); if (!output.exists()) { output.mkdirs(); } File newFile = FileUtils.getFile(output, folder + File.separator + file); if (newFile.exists()) { return; } try (InputStream stream = this.platform.getClass().getResourceAsStream(file)) { byte[] buffer = new byte[2048]; if (stream == null) { try (ZipInputStream zis = new ZipInputStream( new FileInputStream(this.jarFile))) { ZipEntry ze = zis.getNextEntry(); while (ze != null) { String name = ze.getName(); if (name.equals(file)) { new File(newFile.getParent()).mkdirs(); try (FileOutputStream fos = new FileOutputStream(newFile)) { int len; while ((len = zis.read(buffer)) > 0) { fos.write(buffer, 0, len); } } ze = null; } else { ze = zis.getNextEntry(); } } zis.closeEntry(); } return; } newFile.createNewFile(); try (FileOutputStream fos = new FileOutputStream(newFile)) { int len; while ((len = stream.read(buffer)) > 0) { fos.write(buffer, 0, len); } } } } catch (IOException e) { LOGGER.error("Could not save {}", file); e.printStackTrace(); } } /** * Safely closes the database connection. */ public void disable() { try { eventDispatcher.unregisterAll(); checkRoadRegenPersistence(); // Validate that all data in the db is correct final HashSet plots = new HashSet<>(); try { forEachPlotRaw(plots::add); } catch (final Exception ignored) { } DBFunc.validatePlots(plots); // Close the connection DBFunc.close(); } catch (NullPointerException throwable) { LOGGER.error("Could not close database connection", throwable); throwable.printStackTrace(); } } /** * Handle road regen persistence */ private void checkRoadRegenPersistence() { if (!HybridUtils.UPDATE || !Settings.Enabled_Components.PERSISTENT_ROAD_REGEN || ( HybridUtils.regions.isEmpty() && HybridUtils.chunks.isEmpty())) { return; } LOGGER.info("Road regeneration incomplete. Saving incomplete regions to disk"); LOGGER.info("- regions: {}", HybridUtils.regions.size()); LOGGER.info("- chunks: {}", HybridUtils.chunks.size()); ArrayList regions = new ArrayList<>(); ArrayList chunks = new ArrayList<>(); for (BlockVector2 r : HybridUtils.regions) { regions.add(new int[]{r.getBlockX(), r.getBlockZ()}); } for (BlockVector2 c : HybridUtils.chunks) { chunks.add(new int[]{c.getBlockX(), c.getBlockZ()}); } List list = new ArrayList<>(); list.add(regions); list.add(chunks); list.add(HybridUtils.height); File file = new File( this.platform.getDirectory() + File.separator + "persistent_regen_data_" + HybridUtils.area .getId() + "_" + HybridUtils.area.getWorldName()); if (file.exists() && !file.delete()) { LOGGER.error("persistent_regene_data file already exists and could not be deleted"); return; } try (ObjectOutputStream oos = new ObjectOutputStream( Files.newOutputStream(file.toPath(), StandardOpenOption.CREATE_NEW))) { oos.writeObject(list); } catch (IOException e) { LOGGER.error("Error creating persistent_region_data file", e); } } /** * Setup the database connection. */ public void setupDatabase() { try { if (DBFunc.dbManager != null) { DBFunc.dbManager.close(); } Database database; if (Storage.MySQL.USE) { database = new MySQL(Storage.MySQL.HOST, Storage.MySQL.PORT, Storage.MySQL.DATABASE, Storage.MySQL.USER, Storage.MySQL.PASSWORD ); } else if (Storage.SQLite.USE) { File file = FileUtils.getFile(platform.getDirectory(), Storage.SQLite.DB + ".db"); database = new SQLite(file); } else { LOGGER.error("No storage type is set. Disabling PlotSquared"); this.platform.shutdown(); //shutdown used instead of disable because no database is set return; } DBFunc.dbManager = new SQLManager( database, Storage.PREFIX, this.eventDispatcher, this.plotListener, this.worldConfiguration ); this.plots_tmp = DBFunc.getPlots(); if (getPlotAreaManager() instanceof SinglePlotAreaManager) { SinglePlotArea area = ((SinglePlotAreaManager) getPlotAreaManager()).getArea(); addPlotArea(area); ConfigurationSection section = worldConfiguration.getConfigurationSection("worlds.*"); if (section == null) { section = worldConfiguration.createSection("worlds.*"); } area.saveConfiguration(section); area.loadDefaultConfiguration(section); } this.clustersTmp = DBFunc.getClusters(); LOGGER.info("Connection to database established. Type: {}", Storage.MySQL.USE ? "MySQL" : "SQLite"); } catch (ClassNotFoundException | SQLException e) { LOGGER.error( "Failed to open database connection ({}). Disabling PlotSquared", Storage.MySQL.USE ? "MySQL" : "SQLite" ); LOGGER.error("==== Here is an ugly stacktrace, if you are interested in those things ==="); e.printStackTrace(); LOGGER.error("==== End of stacktrace ===="); LOGGER.error( "Please go to the {} 'storage.yml' and configure the database correctly", platform.pluginName() ); this.platform.shutdown(); //shutdown used instead of disable because of database error } } /** * Setup the default configuration. */ public void setupConfig() { String lastVersionString = this.getConfig().getString("version"); if (lastVersionString != null) { String[] split = lastVersionString.split("\\."); int[] lastVersion = new int[]{Integer.parseInt(split[0]), Integer.parseInt(split[1]), Integer.parseInt(split[2])}; if (checkVersion(new int[]{3, 4, 0}, lastVersion)) { Settings.convertLegacy(configFile); if (getConfig().contains("worlds")) { ConfigurationSection worldSection = getConfig().getConfigurationSection("worlds"); worldConfiguration.set("worlds", worldSection); try { worldConfiguration.save(worldsFile); } catch (IOException e) { LOGGER.error("Failed to save worlds.yml", e); e.printStackTrace(); } } Settings.save(configFile); } } Settings.load(configFile); //Sets the version information for the settings.yml file try (InputStream stream = getClass().getResourceAsStream("/plugin.properties")) { try (BufferedReader br = new BufferedReader(new InputStreamReader(stream))) { String versionString = br.readLine(); String commitString = br.readLine(); String dateString = br.readLine(); this.version = PlotVersion.tryParse(versionString, commitString, dateString); } } catch (IOException throwable) { throwable.printStackTrace(); } Settings.save(configFile); config = YamlConfiguration.loadConfiguration(configFile); } /** * Setup all configuration files
* - Config: settings.yml
* - Storage: storage.yml
* * @return success or not */ public boolean setupConfigs() { File folder = new File(this.platform.getDirectory(), "config"); if (!folder.exists() && !folder.mkdirs()) { LOGGER.error("Failed to create the {} config folder. Please create it manually", this.platform.getDirectory()); } try { this.worldsFile = new File(folder, "worlds.yml"); if (!this.worldsFile.exists() && !this.worldsFile.createNewFile()) { LOGGER.error("Could not create the worlds file. Please create 'worlds.yml' manually"); } this.worldConfiguration = YamlConfiguration.loadConfiguration(this.worldsFile); if (this.worldConfiguration.contains("worlds")) { if (!this.worldConfiguration.contains("configuration_version") || ( !this.worldConfiguration.getString("configuration_version") .equalsIgnoreCase(LegacyConverter.CONFIGURATION_VERSION) && !this.worldConfiguration .getString("configuration_version").equalsIgnoreCase("v5"))) { // Conversion needed LOGGER.info("A legacy configuration file was detected. Conversion will be attempted."); try { com.google.common.io.Files .copy(this.worldsFile, new File(folder, "worlds.yml.old")); LOGGER.info("A copy of worlds.yml has been saved in the file worlds.yml.old"); final ConfigurationSection worlds = this.worldConfiguration.getConfigurationSection("worlds"); final LegacyConverter converter = new LegacyConverter(worlds); converter.convert(); this.worldConfiguration.set("worlds", worlds); this.setConfigurationVersion(LegacyConverter.CONFIGURATION_VERSION); LOGGER.info( "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."); } catch (final Exception e) { LOGGER.error("Failed to convert the legacy configuration file. See stack trace for information.", e); } // Disable plugin this.platform.shutdown(); return false; } } else { this.worldConfiguration.set("configuration_version", LegacyConverter.CONFIGURATION_VERSION); } } catch (IOException ignored) { LOGGER.error("Failed to save worlds.yml"); } try { this.configFile = new File(folder, "settings.yml"); if (!this.configFile.exists() && !this.configFile.createNewFile()) { LOGGER.error("Could not create the settings file. Please create 'settings.yml' manually"); } this.config = YamlConfiguration.loadConfiguration(this.configFile); setupConfig(); } catch (IOException ignored) { LOGGER.error("Failed to save settings.yml"); } try { this.storageFile = new File(folder, "storage.yml"); if (!this.storageFile.exists() && !this.storageFile.createNewFile()) { LOGGER.error("Could not create the storage settings file. Please create 'storage.yml' manually"); } YamlConfiguration.loadConfiguration(this.storageFile); setupStorage(); } catch (IOException ignored) { LOGGER.error("Failed to save storage.yml"); } return true; } public @NonNull String getConfigurationVersion() { return this.worldConfiguration.get("configuration_version", LegacyConverter.CONFIGURATION_VERSION) .toString(); } public void setConfigurationVersion(final @NonNull String newVersion) throws IOException { this.worldConfiguration.set("configuration_version", newVersion); this.worldConfiguration.save(this.worldsFile); } /** * Setup the storage file (load + save missing nodes). */ private void setupStorage() { Storage.load(storageFile); Storage.save(storageFile); YamlConfiguration.loadConfiguration(storageFile); } /** * Show startup debug information. */ private void showDebug() { if (Settings.DEBUG) { Map components = Settings.getFields(Settings.Enabled_Components.class); for (Entry component : components.entrySet()) { LOGGER.info("Key: {} | Value: {}", component.getKey(), component.getValue()); } } } public void forEachPlotRaw(final @NonNull Consumer consumer) { for (final PlotArea area : this.getPlotAreaManager().getAllPlotAreas()) { area.getPlots().forEach(consumer); } if (this.plots_tmp != null) { for (final HashMap entry : this.plots_tmp.values()) { entry.values().forEach(consumer); } } } /** * Check if the chunk uses vanilla/non-PlotSquared generation * * @param world World name * @param chunkCoordinates Chunk coordinates * @return {@code true} if the chunk uses non-standard generation, {@code false} if not */ public boolean isNonStandardGeneration( final @NonNull String world, final @NonNull BlockVector2 chunkCoordinates ) { final Location location = Location.at(world, chunkCoordinates.getBlockX() << 4, 64, chunkCoordinates.getBlockZ() << 4); final PlotArea area = getPlotAreaManager().getApplicablePlotArea(location); if (area == null) { return true; } return area.getTerrain() != PlotAreaTerrainType.NONE; } public @NonNull YamlConfiguration getConfig() { return config; } public @NonNull UUIDPipeline getImpromptuUUIDPipeline() { return this.impromptuUUIDPipeline; } public @NonNull UUIDPipeline getBackgroundUUIDPipeline() { return this.backgroundUUIDPipeline; } public @NonNull WorldEdit getWorldEdit() { return this.worldedit; } public @NonNull File getConfigFile() { return this.configFile; } public @NonNull File getWorldsFile() { return this.worldsFile; } public @NonNull YamlConfiguration getWorldConfiguration() { return this.worldConfiguration; } /** * Get the caption map belonging to a namespace. If none exists, a dummy * caption map will be returned. *

* You can register a caption map by calling {@link #registerCaptionMap(String, CaptionMap)} *

* * @param namespace Namespace * @return Map instance */ public @NonNull CaptionMap getCaptionMap(final @NonNull String namespace) { return this.captionMaps.computeIfAbsent( namespace.toLowerCase(Locale.ENGLISH), missingNamespace -> new DummyCaptionMap() ); } /** * Register a caption map. The namespace needs to be equal to the namespace used for * the {@link TranslatableCaption}s inside the map. * * @param namespace Namespace * @param captionMap Map instance */ public void registerCaptionMap( final @NonNull String namespace, final @NonNull CaptionMap captionMap ) { if (namespace.equalsIgnoreCase(TranslatableCaption.DEFAULT_NAMESPACE)) { throw new IllegalArgumentException("Cannot replace default caption map"); } this.captionMaps.put(namespace.toLowerCase(Locale.ENGLISH), captionMap); } public @NonNull EventDispatcher getEventDispatcher() { return this.eventDispatcher; } public @NonNull PlotListener getPlotListener() { return this.plotListener; } /** * Get if the {@link PlatformReadyEvent} has been sent by WorldEdit. There is no way to query this within WorldEdit itself. */ public boolean isWeInitialised() { return weInitialised; } /** * Different ways of sorting {@link Plot plots} */ public enum SortType { /** * Sort plots by their creation, using their index in the database */ CREATION_DATE, /** * Sort plots by their creation timestamp */ CREATION_DATE_TIMESTAMP, /** * Sort plots by when they were last modified */ LAST_MODIFIED, /** * Sort plots based on their distance from the origin of the world */ DISTANCE_FROM_ORIGIN } private final class WEPlatformReadyListener { @SuppressWarnings("unused") @Subscribe(priority = EventHandler.Priority.VERY_EARLY) public void onPlatformReady(PlatformReadyEvent event) { weInitialised = true; WorldEdit.getInstance().getEventBus().unregister(WEPlatformReadyListener.this); } } }