From 3ede0447b024b34f62150091707d6a5627d0768f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20S=C3=B6derberg?= Date: Thu, 21 May 2020 20:06:04 +0200 Subject: [PATCH] Fix up the plot area nightmare --- Core/build.gradle | 2 + .../com/plotsquared/core/plot/PlotWorld.java | 105 +++++++ .../plot/world/DefaultPlotAreaManager.java | 269 +++++------------- .../core/plot/world/PlotAreaManager.java | 3 +- .../core/plot/world/ScatteredPlotWorld.java | 110 +++++++ .../core/plot/world/StandardPlotWorld.java | 65 +++++ .../com/plotsquared/core/util/RegionUtil.java | 10 + 7 files changed, 372 insertions(+), 192 deletions(-) create mode 100644 Core/src/main/java/com/plotsquared/core/plot/PlotWorld.java create mode 100644 Core/src/main/java/com/plotsquared/core/plot/world/ScatteredPlotWorld.java create mode 100644 Core/src/main/java/com/plotsquared/core/plot/world/StandardPlotWorld.java diff --git a/Core/build.gradle b/Core/build.gradle index ee6edfda2..8581a39a4 100644 --- a/Core/build.gradle +++ b/Core/build.gradle @@ -16,6 +16,7 @@ dependencies { testAnnotationProcessor("org.projectlombok:lombok:1.18.8") implementation("org.jetbrains.kotlin:kotlin-stdlib:1.3.72") implementation("org.jetbrains:annotations:19.0.0") + implementation 'com.github.davidmoten:rtree:0.8.7' } sourceCompatibility = 1.8 @@ -73,6 +74,7 @@ shadowJar { include(dependency("net.kyori:text-serializer-gson:3.0.2")) include(dependency("net.kyori:text-serializer-legacy:3.0.2")) include(dependency("net.kyori:text-serializer-plain:3.0.2")) + include(dependency("com.github.davidmoten:rtree:0.8.7")) } relocate('net.kyori.text', 'com.plotsquared.formatting.text') relocate("org.json", "com.plotsquared.json") { diff --git a/Core/src/main/java/com/plotsquared/core/plot/PlotWorld.java b/Core/src/main/java/com/plotsquared/core/plot/PlotWorld.java new file mode 100644 index 000000000..ed7b854c2 --- /dev/null +++ b/Core/src/main/java/com/plotsquared/core/plot/PlotWorld.java @@ -0,0 +1,105 @@ +/* + * _____ _ _ _____ _ + * | __ \| | | | / ____| | | + * | |__) | | ___ | |_| (___ __ _ _ _ __ _ _ __ ___ __| | + * | ___/| |/ _ \| __|\___ \ / _` | | | |/ _` | '__/ _ \/ _` | + * | | | | (_) | |_ ____) | (_| | |_| | (_| | | | __/ (_| | + * |_| |_|\___/ \__|_____/ \__, |\__,_|\__,_|_| \___|\__,_| + * | | + * |_| + * PlotSquared plot management system for Minecraft + * Copyright (C) 2020 IntellectualSites + * + * 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.plot; + +import com.plotsquared.core.location.Location; +import com.sk89q.worldedit.regions.CuboidRegion; +import lombok.EqualsAndHashCode; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; + +/** + * A world that contains plots + */ +@EqualsAndHashCode +public abstract class PlotWorld { + + private final String world; + + /** + * Create a new plot world with a given world name + * + * @param world World name + */ + protected PlotWorld(@NotNull final String world) { + this.world = world; + } + + /** + * Get the plot area that contains the given location, or null + * if the location is not a part of a plot area. + * + * @param location Location + * @return Containing plot area, or null + */ + @Nullable public abstract PlotArea getArea(@NotNull final Location location); + + /** + * Get all plot areas in the world + * + * @return All plot areas in the world + */ + @NotNull public abstract Collection getAreas(); + + /** + * Get all plot areas in a specified region + * + * @param region Region + * @return All areas in the region + */ + @NotNull public abstract Collection getAreasInRegion( + @NotNull final CuboidRegion region); + + /** + * Register a new area in the world + * + * @param area Plot area + */ + public void addArea(@NotNull final PlotArea area) { + throw new UnsupportedOperationException("This world type does not allow adding new areas"); + } + + /** + * Remove an area from the world + * + * @param area Plot area + */ + public void removeArea(@NotNull final PlotArea area) { + throw new UnsupportedOperationException("This world type does not allow removing areas"); + } + + /** + * Get the world name + * + * @return World name + */ + public String getWorld() { + return this.world; + } + +} diff --git a/Core/src/main/java/com/plotsquared/core/plot/world/DefaultPlotAreaManager.java b/Core/src/main/java/com/plotsquared/core/plot/world/DefaultPlotAreaManager.java index 833bf3d4f..f86b5b8b6 100644 --- a/Core/src/main/java/com/plotsquared/core/plot/world/DefaultPlotAreaManager.java +++ b/Core/src/main/java/com/plotsquared/core/plot/world/DefaultPlotAreaManager.java @@ -25,143 +25,96 @@ */ package com.plotsquared.core.plot.world; -import com.plotsquared.core.collection.QuadMap; import com.plotsquared.core.location.Location; import com.plotsquared.core.plot.PlotArea; +import com.plotsquared.core.plot.PlotAreaType; +import com.plotsquared.core.plot.PlotWorld; import com.plotsquared.core.util.StringMan; import com.sk89q.worldedit.regions.CuboidRegion; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; import java.util.Set; public class DefaultPlotAreaManager implements PlotAreaManager { final PlotArea[] noPlotAreas = new PlotArea[0]; - // All plot areas mapped by world - private final HashMap plotAreaMap = new HashMap<>(); - // All plot areas mapped by position - private final HashMap> plotAreaGrid = new HashMap<>(); - private final HashSet plotAreaHashCheck = new HashSet<>(); - // All plot areas - private PlotArea[] plotAreas = new PlotArea[0]; + + private final Map plotWorlds = new HashMap<>(); // Optimization if there are no hash collisions private boolean plotAreaHasCollision = false; - private String[] worlds = new String[0]; @Override public PlotArea[] getAllPlotAreas() { - return plotAreas; - } - - @Override public PlotArea getApplicablePlotArea(Location location) { - switch (this.plotAreas.length) { - case 0: - return null; - case 1: - return this.plotAreas[0].getWorldHash() == location.getWorld().hashCode() - && this.plotAreas[0].contains(location) && (!this.plotAreaHasCollision - || location.getWorld().equals(this.plotAreas[0].getWorldName())) ? - this.plotAreas[0] : - null; - case 2: - case 3: - case 4: - case 5: - case 6: - case 7: - case 8: - String world = location.getWorld(); - int hash = world.hashCode(); - for (PlotArea area : this.plotAreas) { - if (hash == area.getWorldHash()) { - if (area.contains(location.getX(), location.getZ()) && ( - !this.plotAreaHasCollision || world.equals(area.getWorldName()))) { - return area; - } - } - } - return null; - default: - PlotArea[] areas = this.plotAreaMap.get(location.getWorld()); - if (areas == null) { - return null; - } - int z; - int x; - switch (areas.length) { - case 1: - return areas[0]; - case 2: - case 3: - case 4: - case 5: - case 6: - case 7: - case 8: - x = location.getX(); - z = location.getZ(); - for (PlotArea area : areas) { - if (area.contains(x, z)) { - return area; - } - } - return null; - default: - QuadMap search = this.plotAreaGrid.get(location.getWorld()); - return search.get(location.getX(), location.getZ()); - } + final Set area = new HashSet<>(); + for (final PlotWorld world : plotWorlds.values()) { + area.addAll(world.getAreas()); } + return area.toArray(new PlotArea[0]); } - @Override public void addPlotArea(PlotArea plotArea) { - HashSet localAreas = - new HashSet<>(Arrays.asList(getPlotAreas(plotArea.getWorldName(), null))); - HashSet globalAreas = new HashSet<>(Arrays.asList(plotAreas)); - localAreas.add(plotArea); - globalAreas.add(plotArea); - this.plotAreas = globalAreas.toArray(new PlotArea[0]); - this.plotAreaMap.put(plotArea.getWorldName(), localAreas.toArray(new PlotArea[0])); - QuadMap map = this.plotAreaGrid.get(plotArea.getWorldName()); - if (map == null) { - map = new QuadMap(Integer.MAX_VALUE, 0, 0) { - @Override public CuboidRegion getRegion(PlotArea value) { - return value.getRegion(); - } - }; - this.plotAreaGrid.put(plotArea.getWorldName(), map); + @Override @Nullable public PlotArea getApplicablePlotArea(final Location location) { + if (location == null) { + return null; } - map.add(plotArea); + final PlotWorld world = this.plotWorlds.get(location.getWorld()); + if (world == null) { + return null; + } + return world.getArea(location); } - @Override public void removePlotArea(PlotArea area) { - ArrayList globalAreas = new ArrayList<>(Arrays.asList(plotAreas)); - globalAreas.remove(area); - this.plotAreas = globalAreas.toArray(new PlotArea[0]); - if (globalAreas.isEmpty()) { - this.plotAreaMap.remove(area.getWorldName()); - this.plotAreaGrid.remove(area.getWorldName()); + @Override public void addPlotArea(final PlotArea plotArea) { + PlotWorld world = this.plotWorlds.get(plotArea.getWorldName()); + if (world != null) { + if (world instanceof StandardPlotWorld && world.getAreas().isEmpty()) { + this.plotWorlds.remove(plotArea.getWorldName()); + } else { + world.addArea(plotArea); + return; + } + } + if (plotArea.getType() != PlotAreaType.PARTIAL) { + world = new StandardPlotWorld(plotArea.getWorldName(), plotArea); } else { - this.plotAreaMap.put(area.getWorldName(), globalAreas.toArray(new PlotArea[0])); - this.plotAreaGrid.get(area.getWorldName()).remove(area); + world = new ScatteredPlotWorld(plotArea.getWorldName()); + world.addArea(plotArea); + } + this.plotWorlds.put(plotArea.getWorldName(), world); + } + + @Override public void removePlotArea(final PlotArea area) { + final PlotWorld world = this.plotWorlds.get(area.getWorldName()); + if (world == null) { + return; + } + if (world instanceof StandardPlotWorld) { + this.plotWorlds.remove(world.getWorld()); + } else { + world.removeArea(area); + if (world.getAreas().isEmpty()) { + this.plotWorlds.remove(world.getWorld()); + } } } - @Override public PlotArea getPlotArea(String world, String id) { - PlotArea[] areas = this.plotAreaMap.get(world); - if (areas == null) { + @Override public PlotArea getPlotArea(final String world, final String id) { + final PlotWorld plotWorld = this.plotWorlds.get(world); + if (plotWorld == null) { return null; } - if (areas.length == 1) { - return areas[0]; - } else if (id == null) { + final List areas = new ArrayList<>(plotWorld.getAreas()); + if (areas.size() == 1) { + return areas.get(0); + } + if (id == null) { return null; } - for (PlotArea area : areas) { + for (final PlotArea area : areas) { if (StringMan.isEqual(id, area.getId())) { return area; } @@ -169,103 +122,37 @@ public class DefaultPlotAreaManager implements PlotAreaManager { return null; } - @Override public PlotArea getPlotArea(@NotNull Location location) { - switch (this.plotAreas.length) { - case 0: - return null; - case 1: - PlotArea pa = this.plotAreas[0]; - if (pa.contains(location)) { - return pa; - } else { - return null; - } - case 2: - case 3: - case 4: - case 5: - case 6: - case 7: - case 8: - String world = location.getWorld(); - int hash = world.hashCode(); - for (PlotArea area : this.plotAreas) { - if (hash == area.getWorldHash()) { - if (area.contains(location.getX(), location.getZ()) && ( - !this.plotAreaHasCollision || world.equals(area.getWorldName()))) { - return area; - } - } - } - return null; - default: - PlotArea[] areas = this.plotAreaMap.get(location.getWorld()); - if (areas == null) { - return null; - } - int x; - int z; - switch (areas.length) { - case 0: - PlotArea a = areas[0]; - return a.contains(location.getX(), location.getZ()) ? a : null; - case 2: - case 3: - case 4: - case 5: - case 6: - case 7: - case 8: - x = location.getX(); - z = location.getZ(); - for (PlotArea area : areas) { - if (area.contains(x, z)) { - return area; - } - } - return null; - default: - QuadMap search = this.plotAreaGrid.get(location.getWorld()); - return search.get(location.getX(), location.getZ()); - } - } + @Override public PlotArea getPlotArea(@NotNull final Location location) { + return this.getApplicablePlotArea(location); } - @Override public PlotArea[] getPlotAreas(String world, CuboidRegion region) { - if (region == null) { - PlotArea[] areas = this.plotAreaMap.get(world); - if (areas == null) { - return noPlotAreas; - } - return areas; - } - QuadMap areas = this.plotAreaGrid.get(world); - if (areas == null) { + @Override public PlotArea[] getPlotAreas(final String world, final CuboidRegion region) { + final PlotWorld plotWorld = this.plotWorlds.get(world); + if (plotWorld == null) { return noPlotAreas; - } else { - Set found = areas.get(region); - return found.toArray(new PlotArea[0]); } + if (region == null) { + return plotWorld.getAreas().toArray(new PlotArea[0]); + } + return plotWorld.getAreasInRegion(region).toArray(new PlotArea[0]); } - @Override public void addWorld(String worldName) { - if (!this.plotAreaHasCollision && !this.plotAreaHashCheck.add(worldName.hashCode())) { - this.plotAreaHasCollision = true; + @Override public void addWorld(final String worldName) { + PlotWorld world = this.plotWorlds.get(worldName); + if (world != null) { + return; } - Set tmp = new LinkedHashSet<>(); - Collections.addAll(tmp, worlds); - tmp.add(worldName); - worlds = tmp.toArray(new String[0]); + // Create a new empty world. When a new area is added + // the world will be re-recreated with the correct type + world = new StandardPlotWorld(worldName, null); + this.plotWorlds.put(worldName, world); } - @Override public void removeWorld(String worldName) { - Set tmp = new LinkedHashSet<>(); - Collections.addAll(tmp, worlds); - tmp.remove(worldName); - worlds = tmp.toArray(new String[0]); + @Override public void removeWorld(final String worldName) { + this.plotWorlds.remove(worldName); } @Override public String[] getAllWorlds() { - return worlds; + return this.plotWorlds.keySet().toArray(new String[0]); } } diff --git a/Core/src/main/java/com/plotsquared/core/plot/world/PlotAreaManager.java b/Core/src/main/java/com/plotsquared/core/plot/world/PlotAreaManager.java index b6d23b3d4..34033cb2b 100644 --- a/Core/src/main/java/com/plotsquared/core/plot/world/PlotAreaManager.java +++ b/Core/src/main/java/com/plotsquared/core/plot/world/PlotAreaManager.java @@ -29,6 +29,7 @@ import com.plotsquared.core.location.Location; import com.plotsquared.core.plot.PlotArea; import com.sk89q.worldedit.regions.CuboidRegion; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public interface PlotAreaManager { @@ -44,7 +45,7 @@ public interface PlotAreaManager { * @param location The location * @return An applicable area, or null */ - PlotArea getApplicablePlotArea(Location location); + @Nullable PlotArea getApplicablePlotArea(Location location); /** * Get the plot area, if there is any, for the given diff --git a/Core/src/main/java/com/plotsquared/core/plot/world/ScatteredPlotWorld.java b/Core/src/main/java/com/plotsquared/core/plot/world/ScatteredPlotWorld.java new file mode 100644 index 000000000..78cb0b7b2 --- /dev/null +++ b/Core/src/main/java/com/plotsquared/core/plot/world/ScatteredPlotWorld.java @@ -0,0 +1,110 @@ +/* + * _____ _ _ _____ _ + * | __ \| | | | / ____| | | + * | |__) | | ___ | |_| (___ __ _ _ _ __ _ _ __ ___ __| | + * | ___/| |/ _ \| __|\___ \ / _` | | | |/ _` | '__/ _ \/ _` | + * | | | | (_) | |_ ____) | (_| | |_| | (_| | | | __/ (_| | + * |_| |_|\___/ \__|_____/ \__, |\__,_|\__,_|_| \___|\__,_| + * | | + * |_| + * PlotSquared plot management system for Minecraft + * Copyright (C) 2020 IntellectualSites + * + * 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.plot.world; + +import com.github.davidmoten.rtree.Entry; +import com.github.davidmoten.rtree.RTree; +import com.github.davidmoten.rtree.geometry.Geometries; +import com.github.davidmoten.rtree.geometry.Geometry; +import com.plotsquared.core.location.Location; +import com.plotsquared.core.plot.PlotArea; +import com.plotsquared.core.plot.PlotWorld; +import com.plotsquared.core.util.RegionUtil; +import com.sk89q.worldedit.regions.CuboidRegion; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import rx.Observable; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +/** + * Plot world that contains several plot areas (clusters) + */ +public class ScatteredPlotWorld extends PlotWorld { + + private final List areas = new LinkedList<>(); + private final Object treeLock = new Object(); + private RTree areaTree; + + /** + * Create a new plot world with a given world name + * + * @param world World name + */ + public ScatteredPlotWorld(@NotNull final String world) { + super(world); + } + + @Override @Nullable public PlotArea getArea(@NotNull final Location location) { + synchronized (this.treeLock) { + final Observable> area = + areaTree.search(Geometries.point(location.getX(), location.getZ())); + if (area.isEmpty().toBlocking().first()) { + return null; + } + return area.toBlocking().first().value(); + } + } + + @Override @NotNull public Collection getAreas() { + return Collections.unmodifiableCollection(this.areas); + } + + @Override public void addArea(@NotNull final PlotArea area) { + this.areas.add(area); + this.buildTree(); + } + + @Override public void removeArea(@NotNull final PlotArea area) { + this.areas.remove(area); + this.buildTree(); + } + + @Override @NotNull public Collection getAreasInRegion(@NotNull final CuboidRegion region) { + synchronized (this.treeLock) { + final List areas = new LinkedList<>(); + this.areaTree.search(RegionUtil.toRectangle(region)).toBlocking().forEach(entry -> areas.add(entry.value())); + return areas; + } + } + + /** + * Rebuild the area tree + */ + private void buildTree() { + synchronized (this.treeLock) { + this.areaTree = RTree.create(); + for (final PlotArea area : areas) { + this.areaTree = this.areaTree.add(area, + RegionUtil.toRectangle(area.getRegion())); + } + } + } + +} diff --git a/Core/src/main/java/com/plotsquared/core/plot/world/StandardPlotWorld.java b/Core/src/main/java/com/plotsquared/core/plot/world/StandardPlotWorld.java new file mode 100644 index 000000000..162cf0e9f --- /dev/null +++ b/Core/src/main/java/com/plotsquared/core/plot/world/StandardPlotWorld.java @@ -0,0 +1,65 @@ +/* + * _____ _ _ _____ _ + * | __ \| | | | / ____| | | + * | |__) | | ___ | |_| (___ __ _ _ _ __ _ _ __ ___ __| | + * | ___/| |/ _ \| __|\___ \ / _` | | | |/ _` | '__/ _ \/ _` | + * | | | | (_) | |_ ____) | (_| | |_| | (_| | | | __/ (_| | + * |_| |_|\___/ \__|_____/ \__, |\__,_|\__,_|_| \___|\__,_| + * | | + * |_| + * PlotSquared plot management system for Minecraft + * Copyright (C) 2020 IntellectualSites + * + * 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.plot.world; + +import com.plotsquared.core.location.Location; +import com.plotsquared.core.plot.PlotArea; +import com.plotsquared.core.plot.PlotWorld; +import com.sk89q.worldedit.regions.CuboidRegion; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Collections; + +/** + * Ordinary plot world with a single plot area + */ +public class StandardPlotWorld extends PlotWorld { + + private final PlotArea area; + + public StandardPlotWorld(@NotNull final String world, @Nullable final PlotArea area) { + super(world); + this.area = area; + } + + @Override @Nullable public PlotArea getArea(@NotNull final Location location) { + return this.area; + } + + @Override @NotNull public Collection getAreas() { + if (this.area == null) { + return Collections.emptyList(); + } + return Collections.singletonList(this.area); + } + + @Override @NotNull public Collection getAreasInRegion(@NotNull final CuboidRegion region) { + return this.getAreas(); + } + +} diff --git a/Core/src/main/java/com/plotsquared/core/util/RegionUtil.java b/Core/src/main/java/com/plotsquared/core/util/RegionUtil.java index fe4d83a96..59b14651a 100644 --- a/Core/src/main/java/com/plotsquared/core/util/RegionUtil.java +++ b/Core/src/main/java/com/plotsquared/core/util/RegionUtil.java @@ -25,9 +25,13 @@ */ package com.plotsquared.core.util; +import com.github.davidmoten.rtree.geometry.Geometries; +import com.github.davidmoten.rtree.geometry.Rectangle; import com.plotsquared.core.plot.Plot; +import com.sk89q.worldedit.math.BlockVector2; import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.regions.CuboidRegion; +import org.jetbrains.annotations.NotNull; public class RegionUtil { public static CuboidRegion createRegion(int pos1x, int pos2x, int pos1z, int pos2z) { @@ -54,6 +58,12 @@ public class RegionUtil { .getY() && y <= max.getY(); } + @NotNull public static Rectangle toRectangle(@NotNull final CuboidRegion region) { + final BlockVector2 min = region.getMinimumPoint().toBlockVector2(); + final BlockVector2 max = region.getMaximumPoint().toBlockVector2(); + return Geometries.rectangle(min.getX(), min.getZ(), max.getX(), max.getZ()); + } + // Because WE (not fawe) lack this for CuboidRegion public static boolean intersects(CuboidRegion region, CuboidRegion other) { BlockVector3 regionMin = region.getMinimumPoint();