Uses multi-threaded island scanning.

Optimized for Paper. Requires BentoBox 1.9.0 or later.

Makes admin calc command alias recount.

https://github.com/BentoBoxWorld/Limits/issues/52
This commit is contained in:
tastybento 2019-11-16 16:23:37 -08:00
parent d97fa9b9ae
commit af59047f57
10 changed files with 67 additions and 105 deletions

View File

@ -46,13 +46,13 @@
<powermock.version>2.0.2</powermock.version> <powermock.version>2.0.2</powermock.version>
<!-- More visible way how to change dependency versions --> <!-- More visible way how to change dependency versions -->
<spigot.version>1.13.2-R0.1-SNAPSHOT</spigot.version> <spigot.version>1.13.2-R0.1-SNAPSHOT</spigot.version>
<bentobox.version>1.6.0</bentobox.version> <bentobox.version>1.9.0-SNAPSHOT</bentobox.version>
<!-- Revision variable removes warning about dynamic version --> <!-- Revision variable removes warning about dynamic version -->
<revision>${build.version}-SNAPSHOT</revision> <revision>${build.version}-SNAPSHOT</revision>
<!-- Do not change unless you want different name for local builds. --> <!-- Do not change unless you want different name for local builds. -->
<build.number>-LOCAL</build.number> <build.number>-LOCAL</build.number>
<!-- This allows to change between versions. --> <!-- This allows to change between versions. -->
<build.version>1.7.0</build.version> <build.version>1.9.0</build.version>
</properties> </properties>
<!-- Profiles will allow to automatically change build version. --> <!-- Profiles will allow to automatically change build version. -->

View File

@ -24,7 +24,7 @@ public class CalcCommand extends CompositeCommand {
* @param addon - addon * @param addon - addon
*/ */
public CalcCommand(Limits addon, CompositeCommand parent) { public CalcCommand(Limits addon, CompositeCommand parent) {
super(parent, "calc"); super(parent, "calc", "recount");
this.addon = addon; this.addon = addon;
} }
@ -60,7 +60,7 @@ public class CalcCommand extends CompositeCommand {
} }
} }
public void calcLimits(UUID targetPlayer, User sender) { private void calcLimits(UUID targetPlayer, User sender) {
if (addon.getIslands().getIsland(getWorld(), targetPlayer) != null) { if (addon.getIslands().getIsland(getWorld(), targetPlayer) != null) {
new LimitsCalc(getWorld(), getPlugin(), targetPlayer, addon, sender); new LimitsCalc(getWorld(), getPlugin(), targetPlayer, addon, sender);
} else { } else {

View File

@ -2,6 +2,7 @@ package bentobox.addon.limits.commands;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@ -58,8 +59,8 @@ public class LimitPanel {
.put(Material.BEETROOTS, Material.BEETROOT) .put(Material.BEETROOTS, Material.BEETROOT)
.put(Material.REDSTONE_WIRE, Material.REDSTONE); .put(Material.REDSTONE_WIRE, Material.REDSTONE);
// Block to Material icons // Block to Material icons
Optional.ofNullable(Material.getMaterial("SWEET_BERRY_BUSH")).ifPresent(material -> builder.put(material, Material.getMaterial("SWEET_BERRIES"))); Optional.ofNullable(Material.getMaterial("SWEET_BERRY_BUSH")).ifPresent(material -> builder.put(material, Objects.requireNonNull(Material.getMaterial("SWEET_BERRIES"))));
Optional.ofNullable(Material.getMaterial("BAMBOO_SAPLING")).ifPresent(material -> builder.put(material, Material.getMaterial("BAMBOO"))); Optional.ofNullable(Material.getMaterial("BAMBOO_SAPLING")).ifPresent(material -> builder.put(material, Objects.requireNonNull(Material.getMaterial("BAMBOO"))));
B2M = builder.build(); B2M = builder.build();
} }
@ -105,7 +106,7 @@ public class LimitPanel {
addon.getSettings().getLimits().forEach((k,v) -> { addon.getSettings().getLimits().forEach((k,v) -> {
PanelItemBuilder pib = new PanelItemBuilder(); PanelItemBuilder pib = new PanelItemBuilder();
pib.name(Util.prettifyText(k.toString())); pib.name(Util.prettifyText(k.toString()));
Material m = Material.BARRIER; Material m;
try { try {
if (E2M.containsKey(k)) { if (E2M.containsKey(k)) {
m = E2M.get(k); m = E2M.get(k);

View File

@ -1,16 +1,13 @@
package bentobox.addon.limits.commands; package bentobox.addon.limits.commands;
import java.util.EnumMap; import java.util.*;
import java.util.HashSet; import java.util.concurrent.atomic.AtomicInteger;
import java.util.Iterator; import java.util.stream.Collectors;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.ChunkSnapshot; import org.bukkit.ChunkSnapshot;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.scheduler.BukkitTask;
import bentobox.addon.limits.Limits; import bentobox.addon.limits.Limits;
import bentobox.addon.limits.listeners.BlockLimitsListener; import bentobox.addon.limits.listeners.BlockLimitsListener;
@ -19,71 +16,47 @@ import world.bentobox.bentobox.BentoBox;
import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.api.user.User;
import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.database.objects.Island;
import world.bentobox.bentobox.util.Pair; import world.bentobox.bentobox.util.Pair;
import world.bentobox.bentobox.util.Util;
/** /**
* *
* @author YellowZaki * @author YellowZaki, tastybento
*/ */
public class LimitsCalc { public class LimitsCalc {
private boolean checking; private final Limits addon;
private Limits addon; private final World world;
private World world; private final Island island;
private Island island; private final BlockLimitsListener bll;
private BlockLimitsListener bll;
private IslandBlockCount ibc; private IslandBlockCount ibc;
private Map<Material, Integer> blockCount; private final Map<Material, AtomicInteger> blockCount;
private BukkitTask task; private final User sender;
private User sender; private final Set<Pair<Integer, Integer>> chunksToScan;
private int count;
LimitsCalc(World world, BentoBox instance, UUID targetPlayer, Limits addon, User sender) { LimitsCalc(World world, BentoBox instance, UUID targetPlayer, Limits addon, User sender) {
this.checking = true;
this.addon = addon; this.addon = addon;
this.world = world;
this.island = instance.getIslands().getIsland(world, targetPlayer); this.island = instance.getIslands().getIsland(world, targetPlayer);
this.bll = addon.getBlockLimitListener(); this.bll = addon.getBlockLimitListener();
this.ibc = bll.getIsland(island.getUniqueId()); this.ibc = bll.getIsland(island.getUniqueId());
blockCount = new EnumMap<>(Material.class); blockCount = new EnumMap<>(Material.class);
this.sender = sender; this.sender = sender;
Set<Pair<Integer, Integer>> chunksToScan = getChunksToScan(island); this.world = world;
this.task = addon.getServer().getScheduler().runTaskTimer(addon.getPlugin(), () -> {
Set<ChunkSnapshot> chunkSnapshot = new HashSet<>();
if (checking) {
Iterator<Pair<Integer, Integer>> it = chunksToScan.iterator();
if (!it.hasNext()) {
// Nothing left
tidyUp();
return;
}
// Add chunk snapshots to the list
while (it.hasNext() && chunkSnapshot.size() < 200) {
Pair<Integer, Integer> pair = it.next();
if (!world.isChunkLoaded(pair.x, pair.z)) {
world.loadChunk(pair.x, pair.z);
chunkSnapshot.add(world.getChunkAt(pair.x, pair.z).getChunkSnapshot());
world.unloadChunk(pair.x, pair.z);
} else {
chunkSnapshot.add(world.getChunkAt(pair.x, pair.z).getChunkSnapshot());
}
it.remove();
}
// Move to next step
checking = false;
checkChunksAsync(chunkSnapshot);
}
}, 0L, 1);
}
private void checkChunksAsync(final Set<ChunkSnapshot> chunkSnapshot) { // Get chunks to scan
// Run async task to scan chunks chunksToScan = getChunksToScan(island);
addon.getServer().getScheduler().runTaskAsynchronously(addon.getPlugin(), () -> { count = 0;
for (ChunkSnapshot chunk : chunkSnapshot) { chunksToScan.forEach(c -> Util.getChunkAtAsync(world, c.x, c.z).thenAccept(ch -> {
scanChunk(chunk); ChunkSnapshot snapShot = ch.getChunkSnapshot();
} Bukkit.getScheduler().runTaskAsynchronously(addon.getPlugin(), () -> {
// Nothing happened, change state this.scanChunk(snapShot);
checking = true; count++;
}); if (count == chunksToScan.size()) {
this.tidyUp();
}
});
}));
} }
@ -98,7 +71,7 @@ public class LimitsCalc {
if (chunk.getZ() * 16 + z < island.getMinProtectedZ() || chunk.getZ() * 16 + z >= island.getMinProtectedZ() + island.getProtectionRange() * 2) { if (chunk.getZ() * 16 + z < island.getMinProtectedZ() || chunk.getZ() * 16 + z >= island.getMinProtectedZ() + island.getProtectionRange() * 2) {
continue; continue;
} }
for (int y = 0; y < island.getCenter().getWorld().getMaxHeight(); y++) { for (int y = 0; y < Objects.requireNonNull(island.getCenter().getWorld()).getMaxHeight(); y++) {
Material blockData = chunk.getBlockType(x, y, z); Material blockData = chunk.getBlockType(x, y, z);
// Air is free // Air is free
if (!blockData.equals(Material.AIR)) { if (!blockData.equals(Material.AIR)) {
@ -114,9 +87,9 @@ public class LimitsCalc {
// md is limited // md is limited
if (bll.getMaterialLimits(world, island.getUniqueId()).containsKey(md)) { if (bll.getMaterialLimits(world, island.getUniqueId()).containsKey(md)) {
if (!blockCount.containsKey(md)) { if (!blockCount.containsKey(md)) {
blockCount.put(md, 1); blockCount.put(md, new AtomicInteger(1));
} else { } else {
blockCount.put(md, blockCount.get(md) + 1); blockCount.get(md).getAndIncrement();
} }
} }
} }
@ -133,14 +106,15 @@ public class LimitsCalc {
} }
private void tidyUp() { private void tidyUp() {
// Cancel
task.cancel();
if (ibc == null) { if (ibc == null) {
ibc = new IslandBlockCount(); ibc = new IslandBlockCount();
} }
ibc.setBlockCount(blockCount); ibc.setBlockCount(blockCount.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> entry.getValue().get())));
bll.setIsland(island.getUniqueId(), ibc); bll.setIsland(island.getUniqueId(), ibc);
sender.sendMessage("admin.limits.calc.finished"); Bukkit.getScheduler().runTask(addon.getPlugin(), () -> sender.sendMessage("admin.limits.calc.finished"));
} }
} }

View File

@ -1,13 +1,6 @@
package bentobox.addon.limits.listeners; package bentobox.addon.limits.listeners;
import java.util.ArrayList; import java.util.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Material; import org.bukkit.Material;
@ -98,19 +91,19 @@ public class BlockLimitsListener implements Listener {
addon.log("Loading default limits"); addon.log("Loading default limits");
if (addon.getConfig().isConfigurationSection("blocklimits")) { if (addon.getConfig().isConfigurationSection("blocklimits")) {
ConfigurationSection limitConfig = addon.getConfig().getConfigurationSection("blocklimits"); ConfigurationSection limitConfig = addon.getConfig().getConfigurationSection("blocklimits");
defaultLimitMap = loadLimits(limitConfig); defaultLimitMap = loadLimits(Objects.requireNonNull(limitConfig));
} }
// Load specific worlds // Load specific worlds
if (addon.getConfig().isConfigurationSection("worlds")) { if (addon.getConfig().isConfigurationSection("worlds")) {
ConfigurationSection worlds = addon.getConfig().getConfigurationSection("worlds"); ConfigurationSection worlds = addon.getConfig().getConfigurationSection("worlds");
for (String worldName : worlds.getKeys(false)) { for (String worldName : Objects.requireNonNull(worlds).getKeys(false)) {
World world = Bukkit.getWorld(worldName); World world = Bukkit.getWorld(worldName);
if (world != null && addon.inGameModeWorld(world)) { if (world != null && addon.inGameModeWorld(world)) {
addon.log("Loading limits for " + world.getName()); addon.log("Loading limits for " + world.getName());
worldLimitMap.putIfAbsent(world, new HashMap<>()); worldLimitMap.putIfAbsent(world, new HashMap<>());
ConfigurationSection matsConfig = worlds.getConfigurationSection(worldName); ConfigurationSection matsConfig = worlds.getConfigurationSection(worldName);
worldLimitMap.put(world, loadLimits(matsConfig)); worldLimitMap.put(world, loadLimits(Objects.requireNonNull(matsConfig)));
} }
} }
} }
@ -156,7 +149,7 @@ public class BlockLimitsListener implements Listener {
handleBreak(e, e.getPlayer(), e.getBlock()); handleBreak(e, e.getPlayer(), e.getBlock());
} }
void handleBreak(Cancellable e, Player player, Block b) { private void handleBreak(Cancellable e, Player player, Block b) {
Material mat = b.getType(); Material mat = b.getType();
// Special handling for crops that can break in different ways // Special handling for crops that can break in different ways
if (mat.equals(Material.WHEAT_SEEDS)) { if (mat.equals(Material.WHEAT_SEEDS)) {

View File

@ -1,6 +1,9 @@
package bentobox.addon.limits.listeners; package bentobox.addon.limits.listeners;
import java.util.Objects;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Entity; import org.bukkit.entity.Entity;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
@ -41,7 +44,7 @@ public class EntityLimitListener implements Listener {
} }
if (addon.getSettings().getLimits().containsKey(e.getVehicle().getType())) { if (addon.getSettings().getLimits().containsKey(e.getVehicle().getType())) {
// If someone in that area has the bypass permission, allow the spawning // If someone in that area has the bypass permission, allow the spawning
for (Entity entity : e.getVehicle().getLocation().getWorld().getNearbyEntities(e.getVehicle().getLocation(), 5, 5, 5)) { for (Entity entity : Objects.requireNonNull(e.getVehicle().getLocation().getWorld()).getNearbyEntities(e.getVehicle().getLocation(), 5, 5, 5)) {
if (entity instanceof Player) { if (entity instanceof Player) {
Player player = (Player)entity; Player player = (Player)entity;
boolean bypass = (player.isOp() || player.hasPermission(addon.getPlugin().getIWM().getPermissionPrefix(e.getVehicle().getWorld()) + MOD_BYPASS)); boolean bypass = (player.isOp() || player.hasPermission(addon.getPlugin().getIWM().getPermissionPrefix(e.getVehicle().getWorld()) + MOD_BYPASS));
@ -103,7 +106,7 @@ public class EntityLimitListener implements Listener {
private boolean checkByPass(Location l) { private boolean checkByPass(Location l) {
// If someone in that area has the bypass permission, allow the spawning // If someone in that area has the bypass permission, allow the spawning
for (Entity entity : l.getWorld().getNearbyEntities(l, 5, 5, 5)) { for (Entity entity : Objects.requireNonNull(l.getWorld()).getNearbyEntities(l, 5, 5, 5)) {
if (entity instanceof Player) { if (entity instanceof Player) {
Player player = (Player)entity; Player player = (Player)entity;
if (player.isOp() || player.hasPermission(addon.getPlugin().getIWM().getPermissionPrefix(l.getWorld()) + MOD_BYPASS)) { if (player.isOp() || player.hasPermission(addon.getPlugin().getIWM().getPermissionPrefix(l.getWorld()) + MOD_BYPASS)) {
@ -121,8 +124,9 @@ public class EntityLimitListener implements Listener {
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onBlock(HangingPlaceEvent e) { public void onBlock(HangingPlaceEvent e) {
Player player = e.getPlayer(); Player player = e.getPlayer();
if (player == null) return;
addon.getIslands().getIslandAt(e.getEntity().getLocation()).ifPresent(island -> { addon.getIslands().getIslandAt(e.getEntity().getLocation()).ifPresent(island -> {
boolean bypass = player.isOp() || player.hasPermission(addon.getPlugin().getIWM().getPermissionPrefix(e.getEntity().getWorld()) + MOD_BYPASS); boolean bypass = Objects.requireNonNull(player).isOp() || player.hasPermission(addon.getPlugin().getIWM().getPermissionPrefix(e.getEntity().getWorld()) + MOD_BYPASS);
// Check if entity can be hung // Check if entity can be hung
if (!bypass && !island.isSpawn() && atLimit(island, e.getEntity())) { if (!bypass && !island.isSpawn() && atLimit(island, e.getEntity())) {
// Not allowed // Not allowed
@ -143,7 +147,9 @@ public class EntityLimitListener implements Listener {
e.setCancelled(true); e.setCancelled(true);
// If the reason is anything but because of a spawner then tell players within range // If the reason is anything but because of a spawner then tell players within range
if (!e.getSpawnReason().equals(SpawnReason.SPAWNER) && !e.getSpawnReason().equals(SpawnReason.NATURAL) && !e.getSpawnReason().equals(SpawnReason.INFECTION) && !e.getSpawnReason().equals(SpawnReason.NETHER_PORTAL) && !e.getSpawnReason().equals(SpawnReason.REINFORCEMENTS) && !e.getSpawnReason().equals(SpawnReason.SLIME_SPLIT)) { if (!e.getSpawnReason().equals(SpawnReason.SPAWNER) && !e.getSpawnReason().equals(SpawnReason.NATURAL) && !e.getSpawnReason().equals(SpawnReason.INFECTION) && !e.getSpawnReason().equals(SpawnReason.NETHER_PORTAL) && !e.getSpawnReason().equals(SpawnReason.REINFORCEMENTS) && !e.getSpawnReason().equals(SpawnReason.SLIME_SPLIT)) {
for (Entity ent : e.getLocation().getWorld().getNearbyEntities(e.getLocation(), 5, 5, 5)) { World w = e.getLocation().getWorld();
if (w == null) return;
for (Entity ent : w.getNearbyEntities(e.getLocation(), 5, 5, 5)) {
if (ent instanceof Player) { if (ent instanceof Player) {
User.getInstance(ent).sendMessage("entity-limits.hit-limit", "[entity]", User.getInstance(ent).sendMessage("entity-limits.hit-limit", "[entity]",
Util.prettifyText(e.getEntityType().toString()), Util.prettifyText(e.getEntityType().toString()),

View File

@ -1,6 +1,7 @@
package bentobox.addon.limits.listeners; package bentobox.addon.limits.listeners;
import java.util.Locale; import java.util.Locale;
import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import org.apache.commons.lang.math.NumberUtils; import org.apache.commons.lang.math.NumberUtils;
@ -141,8 +142,8 @@ public class JoinListener implements Listener {
// Set perm-based limits // Set perm-based limits
String prefix = addon.getGameModePermPrefix(world); String prefix = addon.getGameModePermPrefix(world);
String name = addon.getGameModeName(world); String name = addon.getGameModeName(world);
if (!prefix.isEmpty() && !name.isEmpty()) { if (!prefix.isEmpty() && !name.isEmpty() && owner.getPlayer() != null) {
checkPerms(owner.getPlayer(), prefix + "island.limit.", island.getUniqueId(), name); checkPerms(Objects.requireNonNull(owner.getPlayer()), prefix + "island.limit.", island.getUniqueId(), name);
} }
} }
} }

View File

@ -27,7 +27,7 @@ worlds:
HOPPER: 11 HOPPER: 11
# Default entity limits within a player's island space (protected area and to island limit). # Default entity limits within a player's island space (protected area and to island limit).
# A limit of 5 will allow up to 5 entites in over world, 5 in nether and 5 in the end. # A limit of 5 will allow up to 5 entities in over world, 5 in nether and 5 in the end.
# Affects all types of creature spawning. Also includes entities like MINECARTS. # Affects all types of creature spawning. Also includes entities like MINECARTS.
# Note: Only the first 49 limited blocks and entities are shown in the limits GUI. # Note: Only the first 49 limited blocks and entities are shown in the limits GUI.
entitylimits: entitylimits:

View File

@ -19,7 +19,7 @@ admin:
calc: calc:
parameters: "<player>" parameters: "<player>"
description: "recalculate the island limits for player" description: "recalculate the island limits for player"
finished: "&aIsland recalc finished sucessfully!" finished: "&aIsland recalc finished successfully!"
island: island:
limits: limits:

View File

@ -1,6 +1,3 @@
/**
*
*/
package bentobox.addon.limits.listeners; package bentobox.addon.limits.listeners;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
@ -10,6 +7,7 @@ import static org.mockito.Mockito.when;
import static org.mockito.Mockito.any; import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.eq; import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.times;
import java.util.Collections; import java.util.Collections;
@ -24,7 +22,6 @@ import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.permissions.PermissionAttachmentInfo; import org.bukkit.permissions.PermissionAttachmentInfo;
import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -70,11 +67,8 @@ public class JoinListenerTest {
@Mock @Mock
private Island island; private Island island;
/**
* @throws java.lang.Exception
*/
@Before @Before
public void setUp() throws Exception { public void setUp() {
jl = new JoinListener(addon); jl = new JoinListener(addon);
// Setup addon // Setup addon
when(addon.getGameModes()).thenReturn(Collections.singletonList(bskyblock)); when(addon.getGameModes()).thenReturn(Collections.singletonList(bskyblock));
@ -111,13 +105,6 @@ public class JoinListenerTest {
when(island.getOwner()).thenReturn(UUID.randomUUID()); when(island.getOwner()).thenReturn(UUID.randomUUID());
} }
/**
* @throws java.lang.Exception
*/
@After
public void tearDown() throws Exception {
}
/** /**
* Test method for {@link bentobox.addon.limits.listeners.JoinListener#onNewIsland(world.bentobox.bentobox.api.events.island.IslandEvent)}. * Test method for {@link bentobox.addon.limits.listeners.JoinListener#onNewIsland(world.bentobox.bentobox.api.events.island.IslandEvent)}.
*/ */
@ -157,7 +144,7 @@ public class JoinListenerTest {
IslandEvent e = new IslandEvent(island, null, false, null, IslandEvent.Reason.CREATED); IslandEvent e = new IslandEvent(island, null, false, null, IslandEvent.Reason.CREATED);
jl.onNewIsland(e); jl.onNewIsland(e);
verify(island).getWorld(); verify(island).getWorld();
verify(owner).getPlayer(); verify(owner, times(2)).getPlayer();
} }
/** /**