From b7b6c04f5f9d928c848f9f3958eecd74ddf5a576 Mon Sep 17 00:00:00 2001 From: tastybento Date: Wed, 6 Feb 2019 23:20:45 -0800 Subject: [PATCH] Performs block limiting. Store blocks placed in the database. Entity limits are not done yet. --- pom.xml | 12 +- .../java/bentobox/addon/limits/Limits.java | 59 ++-- .../java/bentobox/addon/limits/Settings.java | 14 + .../limits/listeners/BlockLimitsListener.java | 278 ++++++++++++++++++ .../listeners/EntityLimitsListener.java | 12 +- .../addon/limits/objects/EntityLimitsDO.java | 2 + .../limits/objects/IslandBlockCount.java | 87 ++++++ src/main/resources/addon.yml | 4 +- src/main/resources/config.yml | 40 +-- src/main/resources/locales/en-US.yml | 28 +- 10 files changed, 437 insertions(+), 99 deletions(-) create mode 100644 src/main/java/bentobox/addon/limits/listeners/BlockLimitsListener.java create mode 100644 src/main/java/bentobox/addon/limits/objects/IslandBlockCount.java diff --git a/pom.xml b/pom.xml index 97eb5e6..6560279 100644 --- a/pom.xml +++ b/pom.xml @@ -1,10 +1,10 @@ 4.0.0 - bentobox.add - addon-limits - 0.0.1-SNAPSHOT + world.bentobox + limits + 0.0.2-SNAPSHOT addon-limits - An add-on for BentoBox that limits entities on islands. + An add-on for BentoBox that limits blocks and entities on islands. https://github.com/BentoBoxWorld/addon-level 2018 @@ -41,7 +41,7 @@ org.spigotmc spigot-api - 1.13.1-R0.1-SNAPSHOT + 1.13.2-R0.1-SNAPSHOT provided @@ -65,7 +65,7 @@ world.bentobox bentobox - 0.10.0-SNAPSHOT + 1.3.0-SNAPSHOT provided diff --git a/src/main/java/bentobox/addon/limits/Limits.java b/src/main/java/bentobox/addon/limits/Limits.java index 27f22f1..b26c8c5 100644 --- a/src/main/java/bentobox/addon/limits/Limits.java +++ b/src/main/java/bentobox/addon/limits/Limits.java @@ -1,20 +1,36 @@ package bentobox.addon.limits; +import java.util.List; +import java.util.stream.Collectors; + +import org.bukkit.World; + +import bentobox.addon.limits.listeners.BlockLimitsListener; +import bentobox.addon.limits.listeners.EntityLimitsListener; import world.bentobox.bentobox.api.addons.Addon; -import world.bentobox.bentobox.api.commands.CompositeCommand; +import world.bentobox.bentobox.api.addons.GameModeAddon; /** - * Addon to BSkyBlock that enables island level scoring and top ten functionality + * Addon to BentoBox that monitors and enforces limits * @author tastybento * */ public class Limits extends Addon { - Settings settings; + private Settings settings; + private EntityLimitsListener listener; + private List worlds; + private BlockLimitsListener blockLimitListener; @Override public void onDisable(){ + if (listener != null) { + worlds.forEach(listener::disable); + } + if (blockLimitListener != null) { + blockLimitListener.save(); + } } @Override @@ -23,33 +39,18 @@ public class Limits extends Addon { saveDefaultConfig(); // Load settings settings = new Settings(this); - // Register commands - // AcidIsland hook in - this.getPlugin().getAddonsManager().getAddonByName("AcidIsland").ifPresent(a -> { - CompositeCommand acidIslandCmd = getPlugin().getCommandsManager().getCommand(getConfig().getString("acidisland.user-command","ai")); - if (acidIslandCmd != null) { - CompositeCommand acidCmd = getPlugin().getCommandsManager().getCommand(getConfig().getString("acidisland.admin-command","acid")); - } - }); - // BSkyBlock hook in - this.getPlugin().getAddonsManager().getAddonByName("BSkyBlock").ifPresent(a -> { - CompositeCommand bsbIslandCmd = getPlugin().getCommandsManager().getCommand(getConfig().getString("bskyblock.user-command","island")); - if (bsbIslandCmd != null) { - CompositeCommand bsbAdminCmd = getPlugin().getCommandsManager().getCommand(getConfig().getString("bskyblock.admin-command","bsbadmin")); - } - }); - - // Register new island listener - //registerListener(new NewIslandListener(this)); - //registerListener(new JoinLeaveListener(this)); + // Register worlds from GameModes + worlds = getPlugin().getAddonsManager().getGameModeAddons().stream() + .filter(gm -> settings.getGameModes().contains(gm.getDescription().getName())) + .map(GameModeAddon::getOverWorld) + .collect(Collectors.toList()); + worlds.forEach(w -> log("Limits will apply to " + w.getName())); + // Register listener + //listener = new EntityLimitsListener(this); + //registerListener(listener); + blockLimitListener = new BlockLimitsListener(this); + registerListener(blockLimitListener); // Done - - } - - /** - * Save the levels to the database - */ - private void save(){ } /** diff --git a/src/main/java/bentobox/addon/limits/Settings.java b/src/main/java/bentobox/addon/limits/Settings.java index 5acaff6..ba074d7 100644 --- a/src/main/java/bentobox/addon/limits/Settings.java +++ b/src/main/java/bentobox/addon/limits/Settings.java @@ -1,7 +1,9 @@ package bentobox.addon.limits; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.bukkit.configuration.ConfigurationSection; @@ -10,8 +12,13 @@ import org.bukkit.entity.EntityType; public class Settings { private Map limits = new HashMap<>(); + private List gameModes = new ArrayList<>(); public Settings(Limits addon) { + + // GameModes + gameModes = addon.getConfig().getStringList("game-modes"); + ConfigurationSection el = addon.getConfig().getConfigurationSection("entitylimits"); if (el != null) { for (String key : el.getKeys(false)) { @@ -34,4 +41,11 @@ public class Settings { return limits; } + /** + * @return the gameModes + */ + public List getGameModes() { + return gameModes; + } + } diff --git a/src/main/java/bentobox/addon/limits/listeners/BlockLimitsListener.java b/src/main/java/bentobox/addon/limits/listeners/BlockLimitsListener.java new file mode 100644 index 0000000..a9497e8 --- /dev/null +++ b/src/main/java/bentobox/addon/limits/listeners/BlockLimitsListener.java @@ -0,0 +1,278 @@ +/** + * + */ +package bentobox.addon.limits.listeners; + +import java.util.HashMap; +import java.util.Map; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.event.Cancellable; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockBurnEvent; +import org.bukkit.event.block.BlockExplodeEvent; +import org.bukkit.event.block.BlockFadeEvent; +import org.bukkit.event.block.BlockFormEvent; +import org.bukkit.event.block.BlockGrowEvent; +import org.bukkit.event.block.BlockMultiPlaceEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.block.BlockSpreadEvent; +import org.bukkit.event.block.EntityBlockFormEvent; +import org.bukkit.event.block.LeavesDecayEvent; +import org.bukkit.event.entity.EntityChangeBlockEvent; +import org.bukkit.event.entity.EntityExplodeEvent; + +import bentobox.addon.limits.Limits; +import bentobox.addon.limits.objects.IslandBlockCount; +import world.bentobox.bentobox.api.events.island.IslandEvent.IslandDeletedEvent; +import world.bentobox.bentobox.api.localization.TextVariables; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.database.Database; +import world.bentobox.bentobox.util.Util; + +/** + * @author tastybento + * + */ +public class BlockLimitsListener implements Listener { + + /** + * Save every 10 blocks of change + */ + private static final Integer CHANGE_LIMIT = 9; + private Limits addon; + private Map countMap = new HashMap<>(); + private Map saveMap = new HashMap<>(); + private Database handler; + private Map> limitMap = new HashMap<>(); + private Map defaultLimitMap = new HashMap<>(); + + public BlockLimitsListener(Limits addon) { + this.addon = addon; + handler = new Database<>(addon, IslandBlockCount.class); + handler.loadObjects().forEach(ibc -> countMap.put(ibc.getUniqueId(), ibc)); + loadAllLimits(); + } + + /** + * Loads the default and world-specific limits + */ + private void loadAllLimits() { + // Load the default limits + addon.log("Loading default limits"); + if (addon.getConfig().isConfigurationSection("blocklimits")) { + ConfigurationSection limitConfig = addon.getConfig().getConfigurationSection("blocklimits"); + defaultLimitMap = loadLimits(limitConfig); + } + // Load specific worlds + + if (addon.getConfig().isConfigurationSection("worlds")) { + ConfigurationSection worlds = addon.getConfig().getConfigurationSection("worlds"); + for (String worldName : worlds.getKeys(false)) { + World world = Bukkit.getWorld(worldName); + if (world != null && addon.getPlugin().getIWM().inWorld(world)) { + addon.log("Loading limits for " + world.getName()); + limitMap.putIfAbsent(world, new HashMap<>()); + ConfigurationSection matsConfig = worlds.getConfigurationSection(worldName); + limitMap.put(world, loadLimits(matsConfig)); + } + } + } + + } + + /** + * Loads limit map from configuration section + * @param cs - configuration section + * @return limit map + */ + private Map loadLimits(ConfigurationSection cs) { + Map mats = new HashMap<>(); + for (String material : cs.getKeys(false)) { + Material mat = Material.getMaterial(material); + if (mat != null && mat.isBlock()) { + mats.put(mat, cs.getInt(material)); + addon.log("Limit " + mat + " to " + cs.getInt(material)); + } else { + addon.logError("Material " + material + " is not a valid block. Skipping..."); + } + } + return mats; + } + + /** + * Save the count database completely + */ + public void save() { + countMap.values().forEach(handler::saveObject); + } + + // Player-related events + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlock(BlockPlaceEvent e) { + notify(e, User.getInstance(e.getPlayer()), process(e.getBlock(), true), e.getBlock().getType()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlock(BlockBreakEvent e) { + notify(e, User.getInstance(e.getPlayer()), process(e.getBlock(), false), e.getBlock().getType()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlock(BlockMultiPlaceEvent e) { + notify(e, User.getInstance(e.getPlayer()), process(e.getBlock(), true), e.getBlock().getType()); + } + + private void notify(Cancellable e, User user, int limit, Material m) { + if (limit > -1) { + user.sendMessage("limits.hit-limit", + "[material]", Util.prettifyText(m.toString()), + TextVariables.NUMBER, String.valueOf(limit)); + e.setCancelled(true); + } + } + + // Non-player events + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlock(BlockBurnEvent e) { + process(e.getBlock(), false); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlock(BlockExplodeEvent e) { + e.blockList().forEach(b -> process(b, false)); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlock(BlockFadeEvent e) { + process(e.getBlock(), false); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlock(BlockFormEvent e) { + process(e.getBlock(), true); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlock(BlockGrowEvent e) { + process(e.getBlock(), true); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlock(BlockSpreadEvent e) { + process(e.getBlock(), true); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlock(EntityBlockFormEvent e) { + process(e.getBlock(), true); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlock(LeavesDecayEvent e) { + process(e.getBlock(), false); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlock(EntityExplodeEvent e) { + e.blockList().forEach(b -> process(b, false)); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlock(EntityChangeBlockEvent e) { + process(e.getBlock(), false); + } + + private int process(Block b, boolean add) { + return process(b, add, b.getType()); + } + + /** + * Check if a block can be + * @param b - block + * @param add - true to add a block, false to remove + * @param changeTo - material this block will become + * @return limit amount if over limit, or -1 if no limitation + */ + private int process(Block b, boolean add, Material changeTo) { + // Check if on island + return addon.getIslands().getIslandAt(b.getLocation()).map(i -> { + String id = i.getUniqueId(); + countMap.putIfAbsent(id, new IslandBlockCount(id)); + saveMap.putIfAbsent(id, 0); + if (add) { + // Check limit + int limit = checkLimit(b.getWorld(), b.getType(), id); + if (limit > -1) { + return limit; + } + countMap.get(id).add(b.getType()); + saveMap.merge(id, 1, Integer::sum); + } else { + if (countMap.containsKey(id)) { + // Check for changes + if (!changeTo.equals(b.getType()) && changeTo.isBlock()) { + // Check limit + int limit = checkLimit(b.getWorld(), changeTo, id); + if (limit > -1) { + return limit; + } + countMap.get(id).add(changeTo); + } + countMap.get(id).remove(b.getType()); + saveMap.merge(id, 1, Integer::sum); + } + } + if (saveMap.get(id) > CHANGE_LIMIT) { + handler.saveObject(countMap.get(id)); + saveMap.remove(id); + } + return -1; + }).orElse(-1); + } + + /** + * Check if this material is at its limit for world on this island + * @param w - world + * @param m - material + * @param id - island id + * @return limit amount if at limit + */ + private int checkLimit(World w, Material m, String id) { + // Check specific world first + if (limitMap.containsKey(w) && limitMap.get(w).containsKey(m)) { + // Material is overridden in world + if (countMap.get(id).isAtLimit(m, limitMap.get(w).get(m))) { + return limitMap.get(w).get(m); + } else { + // No limit + return -1; + } + } + // Check default limit map + if (defaultLimitMap.containsKey(m) && countMap.get(id).isAtLimit(m, defaultLimitMap.get(m))) { + return defaultLimitMap.get(m); + } + // No limit + return -1; + } + + /** + * Removes island from the database + * @param e - island delete event + */ + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onIslandDelete(IslandDeletedEvent e) { + countMap.remove(e.getIsland().getUniqueId()); + saveMap.remove(e.getIsland().getUniqueId()); + handler.deleteID(e.getIsland().getUniqueId()); + } + +} diff --git a/src/main/java/bentobox/addon/limits/listeners/EntityLimitsListener.java b/src/main/java/bentobox/addon/limits/listeners/EntityLimitsListener.java index de79589..02bc16c 100644 --- a/src/main/java/bentobox/addon/limits/listeners/EntityLimitsListener.java +++ b/src/main/java/bentobox/addon/limits/listeners/EntityLimitsListener.java @@ -60,9 +60,9 @@ public class EntityLimitsListener implements Listener { entity.setMetadata("spawnLoc", new FixedMetadataValue(addon.getPlugin(), eld.getSpawnLoc().get(entity.getUniqueId()))); } } + // Delete chunk + handler.deleteObject(eld); } - // Delete chunk - handler.deleteObject(eld); } } @@ -81,7 +81,7 @@ public class EntityLimitsListener implements Listener { Map spawnLoc = new HashMap<>(); Arrays.stream(e.getChunk().getEntities()).filter(x -> x.hasMetadata("spawnLoc")).forEach(entity -> { // Get the meta data - entity.getMetadata("spawnLoc").stream().filter(y -> y.getOwningPlugin().equals(addon)).forEach(v -> { + entity.getMetadata("spawnLoc").stream().filter(y -> y.getOwningPlugin().equals(addon.getPlugin())).forEach(v -> { spawnLoc.put(entity.getUniqueId(), v.asString()); }); }); @@ -124,10 +124,7 @@ public class EntityLimitsListener implements Listener { for (Entity entity : e.getVehicle().getLocation().getWorld().getNearbyEntities(e.getVehicle().getLocation(), 5, 5, 5)) { if (entity instanceof Player) { Player player = (Player)entity; - Boolean bypass = false; - if (player.isOp() || player.hasPermission(addon.getPlugin().getIWM().getPermissionPrefix(e.getVehicle().getWorld()) + "mod.bypass")) { - bypass = true; - } + boolean bypass = (player.isOp() || player.hasPermission(addon.getPlugin().getIWM().getPermissionPrefix(e.getVehicle().getWorld()) + "mod.bypass")); // Check island addon.getIslands().getProtectedIslandAt(e.getVehicle().getLocation()).ifPresent(island -> { // Ignore spawn @@ -177,7 +174,6 @@ public class EntityLimitsListener implements Listener { if (entity instanceof Player) { Player player = (Player)entity; if (player.isOp() || player.hasPermission(addon.getPlugin().getIWM().getPermissionPrefix(e.getEntity().getWorld()) + "mod.bypass")) { - //plugin.getLogger().info("DEBUG: bypass"); bypass = true; break; } diff --git a/src/main/java/bentobox/addon/limits/objects/EntityLimitsDO.java b/src/main/java/bentobox/addon/limits/objects/EntityLimitsDO.java index 030998d..07f3650 100644 --- a/src/main/java/bentobox/addon/limits/objects/EntityLimitsDO.java +++ b/src/main/java/bentobox/addon/limits/objects/EntityLimitsDO.java @@ -22,6 +22,8 @@ public class EntityLimitsDO implements DataObject { @Expose private Map spawnLoc = new HashMap<>(); + public EntityLimitsDO() {} + public EntityLimitsDO(String uniqueId) { this.uniqueId = uniqueId; } diff --git a/src/main/java/bentobox/addon/limits/objects/IslandBlockCount.java b/src/main/java/bentobox/addon/limits/objects/IslandBlockCount.java new file mode 100644 index 0000000..7786137 --- /dev/null +++ b/src/main/java/bentobox/addon/limits/objects/IslandBlockCount.java @@ -0,0 +1,87 @@ +/** + * + */ +package bentobox.addon.limits.objects; + +import java.util.HashMap; +import java.util.Map; + +import org.bukkit.Material; + +import com.google.gson.annotations.Expose; + +import world.bentobox.bentobox.database.objects.DataObject; + +/** + * @author tastybento + * + */ +public class IslandBlockCount implements DataObject { + + @Expose + private String uniqueId = ""; + + @Expose + private Map blockCount = new HashMap<>(); + + public IslandBlockCount(String uniqueId2) { + this.uniqueId = uniqueId2; + } + + /* (non-Javadoc) + * @see world.bentobox.bentobox.database.objects.DataObject#getUniqueId() + */ + @Override + public String getUniqueId() { + return uniqueId; + } + + /* (non-Javadoc) + * @see world.bentobox.bentobox.database.objects.DataObject#setUniqueId(java.lang.String) + */ + @Override + public void setUniqueId(String uniqueId) { + this.uniqueId = uniqueId; + } + + /** + * @return the blockCount + */ + public Map getBlockCount() { + return blockCount; + } + + /** + * @param blockCount the blockCount to set + */ + public void setBlockCount(Map blockCount) { + this.blockCount = blockCount; + } + + /** + * Add a material to the count + * @param material - material + */ + public void add(Material material) { + blockCount.merge(material, 1, Integer::sum); + } + + /** + * Remove a material from the count + * @param material - material + */ + public void remove(Material material) { + blockCount.put(material, blockCount.getOrDefault(material, 0) - 1); + blockCount.values().removeIf(v -> v <= 0); + } + + /** + * Check if this material is at or over a limit + * @param material - block material + * @param limit - limit to check + * @return true if count is >= limit + */ + public boolean isAtLimit(Material material, int limit) { + return blockCount.getOrDefault(material, 0) >= limit; + } +} diff --git a/src/main/resources/addon.yml b/src/main/resources/addon.yml index 2fb60af..b948674 100755 --- a/src/main/resources/addon.yml +++ b/src/main/resources/addon.yml @@ -1,8 +1,8 @@ -name: BentoBox-Limits +name: Limits main: bentobox.addon.limits.Limits version: ${version} authors: tastybento -softdepend: AcidIsland, BSkyBlock +softdepend: AcidIsland, BSkyBlock, CaveBlock, SkyGrid diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 597cee1..7fa00c8 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,9 +1,16 @@ -# General entity limiting -# Use this section to limit how many entities can be added to an island. +# General block limiting +# Use this section to limit how many blocks can be added to an island. # 0 means the item will be blocked from placement completely. -# Uncomment to set the limit. The numbers are just suggested values. -# The limit is per-world, so a hopper limit of 30 means up to 30 in the overworld and -# up to 30 in the nether. +# These limits apply to every world +blocklimits: + HOPPER: 10 +# This section is for world-specific limits and overrides the general limit +# Specify each world you want to limit individually (including nether and end worlds) +worlds: + AcidIsland_world: + HOPPER: 11 + +# NOT IMPLEMENTED YET entitylimits: # Mobs, animals and other living entities #BAT: 10 @@ -42,26 +49,3 @@ entitylimits: #ZOMBIE: 10 #ZOMBIE_HORSE: 10 #ZOMBIE_VILLAGER: 10 - # These are the ONLY blocks that can be limited (because they are entities). - #BANNER: 20 - #ITEM_FRAME: 30 - #FURNACE: 10 - #CHEST: 50 - #TRAPPED_CHEST: 50 - #ENDER_CHEST: 1 - #JUKEBOX: 5 - #DISPENSER: 5 - #DROPPER: 5 - #SIGN: 10 - #MOB_SPAWNER: 10 - #NOTE_BLOCK: 5 - #ENCHANTMENT_TABLE: 5 - #BEACON: 12 - #SKULL: 50 - #DAYLIGHT_DETECTOR: 10 - HOPPER: 30 - #REDSTONE_COMPARATOR: 30 - #FLOWER_POT: 20 - #PAINTING: 5 - #ARMOR_STAND: 5 - #BREWING_STAND: 20 \ No newline at end of file diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index 3810b4e..46a1013 100755 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -3,30 +3,6 @@ # the one at http://yaml-online-parser.appspot.com # ########################################################################################### -warps: - deactivate: "&cOld warp sign deactivated!" - success: "&ASuccess!" - sign-removed: "&CWarp sign removed!" - title: "Warp Signs" - player-warped: "&2[name] warped to your warp sign!" - previous: "&6Previous page" - next: "&6Next page" - warpToPlayersSign: "&6Warping to [player]'s sign" - # The [text] is replaced with the welcome line text from config.yml - warpTip: "&6Place a warp sign with [text] on the top" - error: - does-not-exist: "&cOh snap! That warp no longer exists!" - no-remove: "&CYou cannot remove that sign!" - not-enough-level: "&CYour island level is not high enough!" - no-permission: "&CYou do not have permission to do that!" - not-on-island: "&CYou must be on your island to do that!" - duplicate: "&CDuplicate sign placed" - no-warps-yet: "&CThere are no warps available yet" - your-level-is: "&cYou island level is only [level] and must be higher than [required]" - help: - description: "open the warps panel" -warp: - help: - parameters: "" - description: "warp to the player's warp sign" +limits: + hit-limit: "&c[material] limited to [number]!" \ No newline at end of file