├── .gitignore ├── plugin.yml ├── LICENSE ├── src └── nu │ └── nerd │ └── moblimiter │ ├── MobLimiter.java │ ├── configuration │ ├── ConfiguredDefaults.java │ ├── ConfiguredMob.java │ └── Configuration.java │ ├── limiters │ ├── EntityUnloadLimiter.java │ ├── SpawnLimiter.java │ └── AgeLimiter.java │ ├── listeners │ ├── ClickEvents.java │ └── EntityEvents.java │ ├── EntityHelper.java │ └── CommandHandler.java ├── pom.xml ├── config.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | /classes 3 | /bin 4 | *~ 5 | .DS_Store 6 | .idea 7 | *.iml 8 | -------------------------------------------------------------------------------- /plugin.yml: -------------------------------------------------------------------------------- 1 | name: MobLimiter 2 | version: ${project.version} 3 | description: Control the mob population 4 | authors: ["Travis Watkins", "redwall_hp"] 5 | softdepend: ["LogBlock"] 6 | api-version: '1.20' 7 | 8 | main: nu.nerd.moblimiter.MobLimiter 9 | 10 | permissions: 11 | moblimiter.*: 12 | description: Access all commands 13 | default: op 14 | children: 15 | moblimiter.reload: true 16 | moblimiter.count: true 17 | moblimiter.limits: true 18 | moblimiter.check: true 19 | moblimiter.spawners.bypass: true 20 | moblimiter.reload: 21 | description: Access to reload command 22 | default: op 23 | moblimiter.count: 24 | description: Access to count command 25 | default: op 26 | moblimiter.limits: 27 | description: Access to limits command 28 | default: op 29 | moblimiter.check: 30 | description: Access to check command 31 | default: op 32 | moblimiter.spawners.bypass: 33 | description: Able to replace the mob in a spawner 34 | default: op 35 | 36 | commands: 37 | moblimiter: 38 | description: Gives information about MobLimiter 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Version 1.x: Copyright (c) 2012 Travis Watkins 4 | Version 2.x: Copyright (c) 2016 redwall_hp 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/nu/nerd/moblimiter/MobLimiter.java: -------------------------------------------------------------------------------- 1 | package nu.nerd.moblimiter; 2 | 3 | import nu.nerd.moblimiter.configuration.Configuration; 4 | import nu.nerd.moblimiter.limiters.AgeLimiter; 5 | import nu.nerd.moblimiter.limiters.EntityUnloadLimiter; 6 | import nu.nerd.moblimiter.limiters.SpawnLimiter; 7 | import nu.nerd.moblimiter.listeners.ClickEvents; 8 | import nu.nerd.moblimiter.listeners.EntityEvents; 9 | import org.bukkit.Location; 10 | import org.bukkit.plugin.java.JavaPlugin; 11 | 12 | 13 | 14 | public class MobLimiter extends JavaPlugin { 15 | 16 | 17 | public static MobLimiter instance; 18 | private Configuration configuration; 19 | private EntityUnloadLimiter chunkUnloadLimiter; 20 | private AgeLimiter ageLimiter; 21 | 22 | 23 | public void onEnable() { 24 | MobLimiter.instance = this; 25 | configuration = new Configuration(); 26 | chunkUnloadLimiter = new EntityUnloadLimiter(); 27 | ageLimiter = new AgeLimiter(); 28 | new SpawnLimiter(); 29 | new ClickEvents(); 30 | new EntityEvents(); 31 | new CommandHandler(); 32 | System.out.println(configuration.getSpawnEggs()); 33 | } 34 | 35 | 36 | public void onDisable() { 37 | chunkUnloadLimiter.removeAllMobs(); 38 | } 39 | 40 | 41 | public Configuration getConfiguration() { 42 | return configuration; 43 | } 44 | 45 | 46 | public AgeLimiter getAgeLimiter() { 47 | return ageLimiter; 48 | } 49 | 50 | public static String locationToString(Location location) { 51 | return String.format("X: %.2f Y: %.2f Z: %.2f World: %s", 52 | location.getX(), 53 | location.getY(), 54 | location.getZ(), 55 | location.getWorld().getName()); 56 | } 57 | 58 | 59 | } 60 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | nu.nerd 9 | MobLimiter 10 | 2.3.1 11 | 12 | 13 | 14 | 15 | papermc 16 | https://repo.papermc.io/repository/maven-public/ 17 | 18 | 19 | md_5-releases 20 | http://repo.md-5.net/content/repositories/releases/ 21 | 22 | 23 | 24 | 25 | 26 | 27 | io.papermc.paper 28 | paper-api 29 | 1.20.4-R0.1-SNAPSHOT 30 | provided 31 | 32 | 33 | 34 | 35 | 36 | ${basedir}/src 37 | 38 | 39 | . 40 | true 41 | . 42 | 43 | plugin.yml 44 | config.yml 45 | 46 | 47 | 48 | 49 | 50 | 51 | org.apache.maven.plugins 52 | maven-compiler-plugin 53 | 3.7.0 54 | 55 | 1.8 56 | 1.8 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/nu/nerd/moblimiter/configuration/ConfiguredDefaults.java: -------------------------------------------------------------------------------- 1 | package nu.nerd.moblimiter.configuration; 2 | 3 | import org.bukkit.configuration.ConfigurationSection; 4 | import org.bukkit.configuration.file.FileConfiguration; 5 | 6 | import javax.naming.ConfigurationException; 7 | 8 | 9 | /** 10 | * Represents global default limits that apply to any mob type, unless a ConfiguredMob overrides. 11 | */ 12 | public class ConfiguredDefaults { 13 | 14 | 15 | private int age; 16 | private int max; 17 | private int chunkMax; 18 | private int cull; 19 | 20 | 21 | public ConfiguredDefaults(FileConfiguration config) { 22 | ConfigurationSection section = config.getConfigurationSection("defaults"); 23 | if (section != null) { 24 | age = section.getInt("age", -1); 25 | max = section.getInt("max", -1); 26 | chunkMax = section.getInt("chunk_max", -1); 27 | cull = section.getInt("cull", -1); 28 | } else { 29 | age = -1; 30 | max = -1; 31 | chunkMax = -1; 32 | cull = -1; 33 | } 34 | } 35 | 36 | 37 | /** 38 | * The maximum age, in ticks, that this mob should live for before being removed 39 | */ 40 | public int getAge() { 41 | return age; 42 | } 43 | 44 | 45 | /** 46 | * The maximum number of this mob type that should be allowed to exist within a 47 | * view distance centered on a spawning mob. 48 | */ 49 | public int getMax() { 50 | return max; 51 | } 52 | 53 | 54 | /** 55 | * The maximum number of this mob type that should be allowed to exist in a single chunk. 56 | */ 57 | public int getChunkMax() { 58 | return chunkMax; 59 | } 60 | 61 | 62 | /** 63 | * The number of mobs that should be left after a chunk unload cull 64 | */ 65 | public int getCull() { 66 | return cull; 67 | } 68 | 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/nu/nerd/moblimiter/limiters/EntityUnloadLimiter.java: -------------------------------------------------------------------------------- 1 | package nu.nerd.moblimiter.limiters; 2 | 3 | import nu.nerd.moblimiter.EntityHelper; 4 | import nu.nerd.moblimiter.MobLimiter; 5 | import nu.nerd.moblimiter.configuration.ConfiguredMob; 6 | import org.bukkit.Chunk; 7 | import org.bukkit.World; 8 | import org.bukkit.entity.*; 9 | import org.bukkit.event.EventHandler; 10 | import org.bukkit.event.Listener; 11 | import org.bukkit.event.world.EntitiesUnloadEvent; 12 | 13 | import java.util.Arrays; 14 | import java.util.HashMap; 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | 19 | /** 20 | * Cull applicable mobs on chunk unload. 21 | * This is based on behavior from the original MobLimiter 1.x. 22 | */ 23 | public class EntityUnloadLimiter implements Listener { 24 | 25 | 26 | private MobLimiter plugin; 27 | 28 | private Chunk chunk; 29 | 30 | 31 | public EntityUnloadLimiter() { 32 | plugin = MobLimiter.instance; 33 | plugin.getServer().getPluginManager().registerEvents(this, plugin); 34 | } 35 | 36 | @EventHandler 37 | public void onEntitiesUnloaded(EntitiesUnloadEvent event) { 38 | removeMobs(event.getEntities()); 39 | } 40 | 41 | 42 | /** 43 | * Remove excess mobs in a chunk, if chunk unload culling is enabled for the mob type. 44 | * This is the default behavior from MobLimiter 1.x. 45 | * @param entities The list of entities being unloaded 46 | */ 47 | private void removeMobs(List entities) { 48 | Map count = new HashMap(); 49 | for (Entity entity : entities) { 50 | 51 | // Constrain entities to be removed to limitable mobs, excluding villagers 52 | if (entity.isDead() || !EntityHelper.isLimitableMob(entity) || entity instanceof Villager) continue; 53 | 54 | // Exempt special mobs 55 | if (EntityHelper.isSpecialMob((LivingEntity) entity)) { 56 | if (plugin.getConfiguration().debug()) { 57 | plugin.getLogger().info("Special mob exempted from removal: " + EntityHelper.getMobDescription(entity)); 58 | } 59 | continue; 60 | } 61 | 62 | ConfiguredMob limits = plugin.getConfiguration().getLimits(entity); 63 | String key = limits.getKey(); 64 | int cap = limits.getCull(); 65 | 66 | Integer oldCount = count.get(key); 67 | int mobCount = (oldCount == null) ? 1 : oldCount + 1; 68 | count.put(key, mobCount); 69 | 70 | if (cap > -1 && mobCount > cap) { 71 | entity.remove(); 72 | if (plugin.getConfiguration().debug()) { 73 | plugin.getLogger().info("Chunk unload removed: " + EntityHelper.getMobDescription(entity)); 74 | } 75 | } 76 | 77 | } 78 | } 79 | 80 | 81 | /** 82 | * Run removeMobs() on all loaded chunks 83 | */ 84 | public void removeAllMobs() { 85 | for (World world : plugin.getServer().getWorlds()) { 86 | for (Chunk chunk : world.getLoadedChunks()) { 87 | removeMobs(Arrays.asList(chunk.getEntities())); 88 | } 89 | } 90 | } 91 | 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/nu/nerd/moblimiter/configuration/ConfiguredMob.java: -------------------------------------------------------------------------------- 1 | package nu.nerd.moblimiter.configuration; 2 | 3 | 4 | import nu.nerd.moblimiter.MobLimiter; 5 | import org.bukkit.configuration.ConfigurationSection; 6 | import org.bukkit.entity.EntityType; 7 | 8 | import javax.naming.ConfigurationException; 9 | 10 | 11 | /** 12 | * Represents configured limits for a specific mob type 13 | */ 14 | public class ConfiguredMob { 15 | 16 | 17 | private String key; 18 | private EntityType type; 19 | private int age; 20 | private int max; 21 | private int chunkMax; 22 | private int cull; 23 | 24 | 25 | /** 26 | * Construct a ConfiguredMob from the YAML configuration. Unspecified values will fall back to the values 27 | * defined in the default block. 28 | * @param mob The configuration section 29 | * @param defaults The default values to fall back to 30 | * @throws ConfigurationException 31 | */ 32 | public ConfiguredMob(ConfigurationSection mob, ConfiguredDefaults defaults) throws ConfigurationException{ 33 | try { 34 | String name; 35 | if (mob.getName().toUpperCase().startsWith("SHEEP_")) { 36 | name = "SHEEP"; 37 | } else { 38 | name = mob.getName(); 39 | } 40 | key = mob.getName().toUpperCase(); 41 | type = EntityType.valueOf(name.toUpperCase()); 42 | age = mob.getInt("age", defaults.getAge()); 43 | max = mob.getInt("max", defaults.getMax()); 44 | chunkMax = mob.getInt("chunk_max", defaults.getChunkMax()); 45 | cull = mob.getInt("cull", defaults.getCull()); 46 | } catch (Exception ex) { 47 | throw new ConfigurationException("Invalid configuration for mob type: " + mob.getName()); 48 | } 49 | } 50 | 51 | 52 | /** 53 | * Create a dummy ConfiguredMob with default values, for when one isn't actually configured 54 | * @param mobKey The key this mob type would have in the config 55 | * @param defaults The default values 56 | */ 57 | public ConfiguredMob(String mobKey, ConfiguredDefaults defaults) { 58 | String name; 59 | if (mobKey.toUpperCase().startsWith("SHEEP_")) { 60 | name = "SHEEP"; 61 | } else { 62 | name = mobKey.toUpperCase(); 63 | } 64 | key = mobKey.toUpperCase(); 65 | type = EntityType.valueOf(name.toUpperCase()); 66 | age = defaults.getAge(); 67 | max = defaults.getMax(); 68 | chunkMax = defaults.getChunkMax(); 69 | cull = defaults.getCull(); 70 | } 71 | 72 | 73 | /** 74 | * Get the key identifying this mob classification 75 | */ 76 | public String getKey() { 77 | return key; 78 | } 79 | 80 | 81 | /** 82 | * The Bukkit EntityType these limits apply to 83 | */ 84 | public EntityType getType() { 85 | return type; 86 | } 87 | 88 | 89 | /** 90 | * The maximum age, in ticks, that this mob should live for before being removed 91 | */ 92 | public int getAge() { 93 | return age; 94 | } 95 | 96 | 97 | /** 98 | * The maximum number of this mob type that should be allowed to exist within a 99 | * view distance centered on a spawning mob. 100 | */ 101 | public int getMax() { 102 | return max; 103 | } 104 | 105 | 106 | /** 107 | * The maximum number of this mob type that should be allowed to exist in a single chunk. 108 | */ 109 | public int getChunkMax() { 110 | return chunkMax; 111 | } 112 | 113 | 114 | /** 115 | * The number of mobs that should be left after a chunk unload cull 116 | */ 117 | public int getCull() { 118 | return cull; 119 | } 120 | 121 | 122 | } 123 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | radius: 3 2 | breeding_ticks: 400 3 | growth_ticks: 400 4 | logblock: false 5 | relative_age: false 6 | debug: false 7 | 8 | 9 | defaults: 10 | age: -1 11 | max: 150 12 | chunk_max: 50 13 | cull: 4 14 | 15 | 16 | limits: 17 | bat: 18 | cull: 6 19 | blaze: 20 | age: 12000 21 | cull: 0 22 | cave_spider: 23 | age: 12000 24 | cull: 2 25 | chicken: [] 26 | cod: [] 27 | cow: [] 28 | creeper: 29 | age: 24000 30 | cull: 8 31 | dolphin: [] 32 | ender_dragon: [] 33 | enderman: 34 | age: 18000 35 | cull: 0 36 | endermite: 37 | cull: 6 38 | evoker: [] 39 | ghast: 40 | cull: 1 41 | giant: 42 | cull: -1 43 | guardian: 44 | age: 12000 45 | max: 100 46 | chunk_max: 25 47 | cull: 4 48 | horse: 49 | age: -1 50 | illusioner: [] 51 | iron_golem: [] 52 | llama: 53 | cull: 8 54 | magma_cube: 55 | age: 24000 56 | mushroom_cow: [] 57 | ocelot: 58 | cull: 8 59 | parrot: 60 | cull: 8 61 | phantom: [] 62 | pig: [] 63 | pig_zombie: 64 | age: 18000 65 | pufferfish: [] 66 | rabbit: 67 | cull: 8 68 | salmon: [] 69 | sheep_white: 70 | cull: 2 71 | sheep_orange: 72 | cull: 2 73 | sheep_magenta: 74 | cull: 2 75 | sheep_light_blue: 76 | cull: 2 77 | sheep_yellow: 78 | cull: 2 79 | sheep_lime: 80 | cull: 2 81 | sheep_pink: 82 | cull: 2 83 | sheep_silver: 84 | cull: 2 85 | sheep_cyan: 86 | cull: 2 87 | sheep_purple: 88 | cull: 2 89 | sheep_blue: 90 | cull: 2 91 | sheep_brown: 92 | cull: 2 93 | sheep_green: 94 | cull: 2 95 | sheep_red: 96 | cull: 2 97 | sheep_black: 98 | cull: 2 99 | shulker: [] 100 | silverfish: 101 | cull: 6 102 | skeleton: 103 | age: 24000 104 | slime: 105 | age: 24000 106 | snowman: 107 | cull: -1 108 | spider: 109 | age: 12000 110 | cull: 2 111 | squid: 112 | age: 12000 113 | max: 80 114 | chunk_max: 5 115 | cull: 1 116 | tropical_fish: [] 117 | turtle: [] 118 | vex: [] 119 | villager: 120 | chunk_max: 20 121 | age: -1 122 | vindicator: [] 123 | witch: 124 | age: 24000 125 | cull: 2 126 | wither: 127 | cull: -1 128 | wolf: 129 | cull: 8 130 | zombie: 131 | age: 24000 132 | 133 | spawn_eggs: 134 | - 'ALLAY_SPAWN_EGG' 135 | - 'ARMADILLO_SPAWN_EGG' 136 | - 'AXOLOTL_SPAWN_EGG' 137 | - 'BAT_SPAWN_EGG' 138 | - 'BEE_SPAWN_EGG' 139 | - 'BLAZE_SPAWN_EGG' 140 | - 'BOGGED_SPAWN_EGG' 141 | - 'BREEZE_SPAWN_EGG' 142 | - 'CAT_SPAWN_EGG' 143 | - 'CAMEL_SPAWN_EGG' 144 | - 'CAVE_SPIDER_SPAWN_EGG' 145 | - 'COD_SPAWN_EGG' 146 | - 'COW_SPAWN_EGG' 147 | - 'CREEPER_SPAWN_EGG' 148 | - 'DOLPHIN_SPAWN_EGG' 149 | - 'DONKEY_SPAWN_EGG' 150 | - 'DROWNED_SPAWN_EGG' 151 | - 'ELDER_GUARDIAN_SPAWN_EGG' 152 | - 'ENDER_DRAGON_SPAWN_EGG' 153 | - 'ENDERMAN_SPAWN_EGG' 154 | - 'ENDERMITE_SPAWN_EGG' 155 | - 'EVOKER_SPAWN_EGG' 156 | - 'FOX_SPAWN_EGG' 157 | - 'FROG_SPAWN_EGG' 158 | - 'GHAST_SPAWN_EGG' 159 | - 'GLOW_SQUID_SPAWN_EGG' 160 | - 'GOAT_SPAWN_EGG' 161 | - 'GUARDIAN_SPAWN_EGG' 162 | - 'HOGLIN_SPAWN_EGG' 163 | - 'HORSE_SPAWN_EGG' 164 | - 'HUSK_SPAWN_EGG' 165 | - 'IRON_GOLEM_SPAWN_EGG' 166 | - 'LLAMA_SPAWN_EGG' 167 | - 'MAGMA_CUBE_SPAWN_EGG' 168 | - 'MOOSHROOM_SPAWN_EGG' 169 | - 'MULE_SPAWN_EGG' 170 | - 'OCELOT_SPAWN_EGG' 171 | - 'PANDA_SPAWN_EGG' 172 | - 'PARROT_SPAWN_EGG' 173 | - 'PHANTOM_SPAWN_EGG' 174 | - 'PIG_SPAWN_EGG' 175 | - 'PIGLIN_SPAWN_EGG' 176 | - 'PIGLIN_BRUTE_SPAWN_EGG' 177 | - 'PILLAGER_SPAWN_EGG' 178 | - 'POLAR_BEAR_SPAWN_EGG' 179 | - 'PUFFERFISH_SPAWN_EGG' 180 | - 'RABBIT_SPAWN_EGG' 181 | - 'RAVAGER_SPAWN_EGG' 182 | - 'SALMON_SPAWN_EGG' 183 | - 'SHEEP_SPAWN_EGG' 184 | - 'SHULKER_SPAWN_EGG' 185 | - 'SILVERFISH_SPAWN_EGG' 186 | - 'SKELETON_SPAWN_EGG' 187 | - 'SKELETON_HORSE_SPAWN_EGG' 188 | - 'SLIME_SPAWN_EGG' 189 | - 'SNIFFER_SPAWN_EGG' 190 | - 'SNOW_GOLEM_SPAWN_EGG' 191 | - 'SPIDER_SPAWN_EGG' 192 | - 'SQUID_SPAWN_EGG' 193 | - 'STRAY_SPAWN_EGG' 194 | - 'STRIDER_SPAWN_EGG' 195 | - 'TADPOLE_SPAWN_EGG' 196 | - 'TRADER_LLAMA_SPAWN_EGG' 197 | - 'TROPICAL_FISH_SPAWN_EGG' 198 | - 'TURTLE_SPAWN_EGG' 199 | - 'VEX_SPAWN_EGG' 200 | - 'VILLAGER_SPAWN_EGG' 201 | - 'VINDICATOR_SPAWN_EGG' 202 | - 'WANDERING_TRADER_SPAWN_EGG' 203 | - 'WARDEN_SPAWN_EGG' 204 | - 'WITCH_SPAWN_EGG' 205 | - 'WITHER_SPAWN_EGG' 206 | - 'WITHER_SKELETON_SPAWN_EGG' 207 | - 'WOLF_SPAWN_EGG' 208 | - 'ZOGLIN_SPAWN_EGG' 209 | - 'ZOMBIE_SPAWN_EGG' 210 | - 'ZOMBIE_HORSE_SPAWN_EGG' 211 | - 'ZOMBIE_VILLAGER_SPAWN_EGG' 212 | - 'ZOMBIFIED_PIGLIN_SPAWN_EGG' 213 | -------------------------------------------------------------------------------- /src/nu/nerd/moblimiter/limiters/SpawnLimiter.java: -------------------------------------------------------------------------------- 1 | package nu.nerd.moblimiter.limiters; 2 | 3 | import nu.nerd.moblimiter.EntityHelper; 4 | import nu.nerd.moblimiter.MobLimiter; 5 | import nu.nerd.moblimiter.configuration.ConfiguredMob; 6 | import org.bukkit.Chunk; 7 | import org.bukkit.World; 8 | import org.bukkit.entity.Entity; 9 | import org.bukkit.event.EventHandler; 10 | import org.bukkit.event.EventPriority; 11 | import org.bukkit.event.Listener; 12 | import org.bukkit.event.entity.CreatureSpawnEvent; 13 | import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason; 14 | 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | 19 | /** 20 | * Limit newly spawning mobs if there are too many 21 | */ 22 | public class SpawnLimiter implements Listener { 23 | 24 | 25 | private MobLimiter plugin; 26 | private List reasons; 27 | 28 | 29 | public SpawnLimiter() { 30 | plugin = MobLimiter.instance; 31 | plugin.getServer().getPluginManager().registerEvents(this, plugin); 32 | reasons = new ArrayList<>(); 33 | reasons.add(SpawnReason.BREEDING); 34 | reasons.add(SpawnReason.DEFAULT); 35 | reasons.add(SpawnReason.NATURAL); 36 | reasons.add(SpawnReason.SPAWNER); 37 | } 38 | 39 | 40 | /** 41 | * Handle mob limting on creature spawn 42 | */ 43 | @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) 44 | public void onCreatureSpawnEvent(final CreatureSpawnEvent event) { 45 | 46 | SpawnReason reason = event.getSpawnReason(); 47 | ConfiguredMob limits = plugin.getConfiguration().getLimits(event.getEntity()); 48 | 49 | if (!reasons.contains(reason)) return; 50 | 51 | // Cancel spawn of mobs over the radius limit 52 | if (countEntitiesInSpawnRadius(event.getEntity()) >= limits.getMax() && limits.getMax() > -1) { 53 | log(event.getEntity(), reason, "radius", limits.getMax()); 54 | event.getEntity().remove(); 55 | } 56 | 57 | // Cancel spawn of mobs over the chunk limit 58 | if (countEntitiesInChunk(event.getEntity()) >= limits.getChunkMax() && limits.getChunkMax() > -1) { 59 | log(event.getEntity(), reason, "chunk", limits.getChunkMax()); 60 | event.getEntity().remove(); 61 | } 62 | 63 | } 64 | 65 | 66 | /** 67 | * Count entities of the same type within a "view distance" in chunks from the original entity 68 | * @param entity the entity to check 69 | * @return number of matching entities 70 | */ 71 | private int countEntitiesInSpawnRadius(Entity entity) { 72 | int count = 0; 73 | int radius = plugin.getConfiguration().getRadius(); 74 | ConfiguredMob mob = plugin.getConfiguration().getLimits(entity); 75 | World world = entity.getWorld(); 76 | Chunk start = entity.getLocation().getChunk(); 77 | for (int x = start.getX() - radius; x <= start.getX() + radius; x++) { 78 | for (int z = start.getZ() - radius; z <= start.getZ() + radius; z++) { 79 | Chunk c = world.getChunkAt(x, z); 80 | for (Entity e : c.getEntities()) { 81 | ConfiguredMob m = plugin.getConfiguration().getLimits(e); 82 | if (m.getKey().equals(mob.getKey()) && !e.isDead()) { 83 | count++; 84 | } 85 | } 86 | } 87 | } 88 | return count; 89 | } 90 | 91 | 92 | /** 93 | * Count entities of the same type in an individual chunk 94 | * @param entity The entity to check 95 | * @return number of matching entities 96 | */ 97 | private int countEntitiesInChunk(Entity entity) { 98 | int count = 0; 99 | ConfiguredMob mob = plugin.getConfiguration().getLimits(entity); 100 | for (Entity e : entity.getLocation().getChunk().getEntities()) { 101 | ConfiguredMob m = plugin.getConfiguration().getLimits(e); 102 | if (m.getKey().equals(mob.getKey()) && !e.isDead()) { 103 | count++; 104 | } 105 | } 106 | return count; 107 | } 108 | 109 | 110 | /** 111 | * Log entity removal for diagnostic purposes if debug mode is on 112 | */ 113 | private void log(Entity entity, SpawnReason reason, String capType, int cap) { 114 | if (!plugin.getConfiguration().debug()) return; 115 | String mob = EntityHelper.getMobDescription(entity); 116 | String details = String.format("[reason: %s, cap type: %s, cap: %d]", reason.toString(), capType, cap); 117 | String msg = String.format("Cancelled spawn of %s %s", mob, details); 118 | plugin.getLogger().info(msg); 119 | } 120 | 121 | 122 | } 123 | -------------------------------------------------------------------------------- /src/nu/nerd/moblimiter/limiters/AgeLimiter.java: -------------------------------------------------------------------------------- 1 | package nu.nerd.moblimiter.limiters; 2 | 3 | import nu.nerd.moblimiter.MobLimiter; 4 | import nu.nerd.moblimiter.EntityHelper; 5 | import nu.nerd.moblimiter.configuration.ConfiguredMob; 6 | import org.bukkit.Chunk; 7 | import org.bukkit.World; 8 | import org.bukkit.entity.*; 9 | import org.bukkit.event.Listener; 10 | import org.bukkit.scheduler.BukkitRunnable; 11 | 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | import java.util.UUID; 15 | 16 | 17 | /** 18 | * Sweep loaded chunks every 30 seconds and remove applicable mobs over their type's age limit 19 | */ 20 | public class AgeLimiter extends BukkitRunnable implements Listener { 21 | 22 | 23 | private MobLimiter plugin; 24 | private int removed; 25 | private Map lastTargeted; 26 | 27 | 28 | public AgeLimiter() { 29 | plugin = MobLimiter.instance; 30 | lastTargeted = new HashMap(); 31 | this.runTaskTimer(plugin, 600L, 600L); //sweep every 30 seconds 32 | } 33 | 34 | 35 | /** 36 | * Call the sweep on every loaded chunk in every world 37 | */ 38 | public void run() { 39 | removed = 0; 40 | for (World world: plugin.getServer().getWorlds()) { 41 | for (Chunk chunk : world.getLoadedChunks()) { 42 | sweepChunk(chunk); 43 | } 44 | } 45 | if (plugin.getConfiguration().debug()) { 46 | plugin.getLogger().info(String.format("Age limit sweep removed %d entities", removed)); 47 | } 48 | } 49 | 50 | 51 | /** 52 | * Sweep the chunk and remove mobs that are past their shelf life 53 | * @param chunk the chunk to check 54 | */ 55 | private void sweepChunk(Chunk chunk) { 56 | for (Entity entity : chunk.getEntities()) { 57 | 58 | // Constrain entities to be removed to limitable mobs, excluding villagers 59 | if (entity.isDead() || !EntityHelper.isLimitableMob(entity) || entity instanceof Villager) continue; 60 | 61 | // Exempt special mobs 62 | if (EntityHelper.isSpecialMob((LivingEntity) entity)) { 63 | if (plugin.getConfiguration().debug()) { 64 | plugin.getLogger().info("Special mob exempted from removal: " + EntityHelper.getMobDescription(entity)); 65 | } 66 | continue; 67 | } 68 | 69 | // Leave two of any farm animals 70 | if (EntityHelper.isBreedingPair(entity)) continue; 71 | 72 | // If relative ages are on, and the mob is targeting a player, don't remove it 73 | if (isTargetingPlayer(entity)) continue; 74 | 75 | // Remove mobs 76 | ConfiguredMob limits = plugin.getConfiguration().getLimits(entity); 77 | if (!entity.isDead() && adjustedAge(entity) > limits.getAge() && limits.getAge() > -1) { 78 | ((LivingEntity) entity).damage(1000); // Kill the entity and drop its items 79 | removed++; 80 | lastTargeted.remove(entity.getUniqueId()); 81 | if (plugin.getConfiguration().debug()) { 82 | plugin.getLogger().info("Removed mob (age limit): " + EntityHelper.getMobDescription(entity)); 83 | } 84 | } 85 | 86 | } 87 | } 88 | 89 | 90 | /** 91 | * Don't let the sweep kill entities currently targeting a player. 92 | * Also, keep resetting the relative age, since EntityTargetEvent 93 | * only fires when the target changes. 94 | * @param entity the entity to check 95 | * @return true if the Entity is a Creature type and is targeting a player. 96 | */ 97 | private boolean isTargetingPlayer(Entity entity) { 98 | if (!plugin.getConfiguration().relativeAgeEnabled()) return false; 99 | if (entity instanceof Creature) { 100 | Creature creature = (Creature) entity; 101 | if (creature.getTarget() != null && creature.getTarget().getType().equals(EntityType.PLAYER)) { 102 | lastTargeted.put(entity.getUniqueId(), entity.getTicksLived()); 103 | return true; 104 | } 105 | } 106 | return false; 107 | } 108 | 109 | 110 | /** 111 | * Returns the adjusted age of a mob. 112 | * If relative age is on, and the mob recently tageted a player, the delta ticks will be returned. 113 | * Otherwise, the regular getTicksLived() will be used. 114 | * @param entity the entity to check 115 | * @return the age in ticks 116 | */ 117 | public int adjustedAge(Entity entity) { 118 | if (lastTargeted.containsKey(entity.getUniqueId())) { 119 | return entity.getTicksLived() - lastTargeted.get(entity.getUniqueId()); 120 | } else { 121 | return entity.getTicksLived(); 122 | } 123 | } 124 | 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/nu/nerd/moblimiter/listeners/ClickEvents.java: -------------------------------------------------------------------------------- 1 | package nu.nerd.moblimiter.listeners; 2 | 3 | import io.papermc.paper.event.player.AsyncChatEvent; 4 | import net.kyori.adventure.text.Component; 5 | import net.kyori.adventure.text.format.TextColor; 6 | import nu.nerd.moblimiter.MobLimiter; 7 | import org.bukkit.Material; 8 | import org.bukkit.entity.*; 9 | import org.bukkit.event.EventHandler; 10 | import org.bukkit.event.EventPriority; 11 | import org.bukkit.event.Listener; 12 | import org.bukkit.event.block.Action; 13 | import org.bukkit.event.player.PlayerInteractAtEntityEvent; 14 | import org.bukkit.event.player.PlayerInteractEvent; 15 | import org.bukkit.inventory.ItemStack; 16 | 17 | import java.util.List; 18 | 19 | public class ClickEvents implements Listener { 20 | 21 | MobLimiter plugin; 22 | private List spawnEggs; 23 | 24 | public ClickEvents() { 25 | plugin = MobLimiter.instance; 26 | spawnEggs = MobLimiter.instance.getConfiguration().getSpawnEggs(); 27 | plugin.getServer().getPluginManager().registerEvents(this, plugin); 28 | } 29 | 30 | // -------------------------------------------------------------------------------------------- 31 | 32 | /** 33 | * Formerly part of LimitSpawnEggs 34 | * Stops players from changing the mob of a spawner with spawn eggs 35 | */ 36 | @EventHandler(ignoreCancelled = true) 37 | public void onPlayerInteract(PlayerInteractEvent event) { 38 | Player player = event.getPlayer(); 39 | if(player.hasPermission("moblimiter.spawners.bypass")) { 40 | return; 41 | } 42 | if(event.getAction() == Action.RIGHT_CLICK_BLOCK && 43 | event.getClickedBlock().getType() == Material.SPAWNER && 44 | event.getItem() != null && 45 | spawnEggs.contains(event.getItem().getType())) { 46 | event.setCancelled(true); 47 | player.sendMessage(Component.text("You don't have permission to edit spawners.") 48 | .color(TextColor.fromHexString("#FF5555"))); 49 | } 50 | } 51 | 52 | // -------------------------------------------------------------------------------------------- 53 | 54 | /** 55 | * Formerly part of KeepBabyMobs 56 | * Handles the locking of baby mobs. Originally handled by KeepBabyMobs. 57 | */ 58 | @EventHandler 59 | public void onPlayerInteractAtEntityEvent(PlayerInteractAtEntityEvent event) { 60 | Entity entity = event.getRightClicked(); 61 | Player player = event.getPlayer(); 62 | ItemStack hand = player.getEquipment().getItemInMainHand(); 63 | 64 | if(hand.getType().equals(Material.NAME_TAG)) { 65 | lockMobAge(entity, player, hand); 66 | } 67 | 68 | } 69 | 70 | // -------------------------------------------------------------------------------------------- 71 | 72 | /** 73 | * Locks the mob's age and makes it a baby forever 74 | * @param entity The entity being age locked 75 | * @param player The player initiating the age lock 76 | * @param hand The item being used to lock the entity's age 77 | */ 78 | public void lockMobAge(Entity entity, Player player, ItemStack hand) { 79 | // If the held item is a nametag 80 | if(hand.getItemMeta().hasDisplayName()) { 81 | // Tadpoles are checked separately due to a different handling of their growth. 82 | if(entity.getType().equals(EntityType.TADPOLE)) { 83 | ((Tadpole) entity).setAgeLock(true); 84 | logLock(player, entity); 85 | 86 | // Check other mobs that can age and breed after. 87 | } else if(entity instanceof Breedable) { 88 | Breedable breedable = (Breedable) entity; 89 | 90 | if(!breedable.isAdult()) { 91 | breedable.setAgeLock(true); 92 | 93 | if(!(breedable instanceof Horse)) { 94 | breedable.setAge(Integer.MIN_VALUE); 95 | 96 | } 97 | logLock(player, breedable); 98 | } 99 | 100 | } 101 | } 102 | } 103 | 104 | // -------------------------------------------------------------------------------------------- 105 | 106 | /** 107 | * Logs any locked mobs to console. 108 | * @param player The player locking the mob 109 | * @param entity The entity being locked 110 | */ 111 | public void logLock(Player player, Entity entity) { 112 | player.sendMessage(Component.text("That mob has now been age locked. How adorable!") 113 | .color(TextColor.fromHexString("#FFAA00"))); 114 | MobLimiter.instance.getComponentLogger().info(Component.text(String.format("%s age locked %s named %s at %s", 115 | player.getName(), entity.getType(), entity.customName(), 116 | MobLimiter.locationToString(entity.getLocation())))); 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/nu/nerd/moblimiter/configuration/Configuration.java: -------------------------------------------------------------------------------- 1 | package nu.nerd.moblimiter.configuration; 2 | 3 | 4 | import nu.nerd.moblimiter.MobLimiter; 5 | import org.bukkit.Material; 6 | import org.bukkit.configuration.ConfigurationSection; 7 | import org.bukkit.entity.Entity; 8 | import org.bukkit.entity.Sheep; 9 | 10 | import javax.naming.ConfigurationException; 11 | import java.util.ArrayList; 12 | import java.util.HashMap; 13 | import java.util.List; 14 | 15 | public class Configuration { 16 | 17 | 18 | private MobLimiter plugin; 19 | private int radius; 20 | private boolean debug; 21 | private int breedingTicks; 22 | private int growthTicks; 23 | private boolean logBlock; 24 | private boolean relativeAge; 25 | private ConfiguredDefaults defaults; 26 | private HashMap limits; 27 | private List spawnEggs = new ArrayList<>(); 28 | 29 | 30 | public Configuration() { 31 | plugin = MobLimiter.instance; 32 | plugin.saveDefaultConfig(); 33 | load(); 34 | } 35 | 36 | 37 | /** 38 | * Reload the configuration from disk 39 | */ 40 | public void load() { 41 | 42 | plugin.reloadConfig(); 43 | this.radius = plugin.getConfig().getInt("radius", 3); 44 | this.debug = plugin.getConfig().getBoolean("debug", false); 45 | this.breedingTicks = plugin.getConfig().getInt("breeding_ticks", 300); 46 | this.growthTicks = plugin.getConfig().getInt("growth_ticks", 300); 47 | this.defaults = new ConfiguredDefaults(plugin.getConfig()); 48 | this.logBlock = plugin.getConfig().getBoolean("logblock", false); 49 | this.relativeAge = plugin.getConfig().getBoolean("relative_age", false); 50 | 51 | spawnEggs = new ArrayList<>(); 52 | 53 | this.limits = new HashMap(); 54 | ConfigurationSection mobLimits = plugin.getConfig().getConfigurationSection("limits"); 55 | if (mobLimits != null) { 56 | for (String key : mobLimits.getKeys(false)) { 57 | try { 58 | ConfigurationSection l = mobLimits.getConfigurationSection(key); 59 | ConfiguredMob mob; 60 | if (l != null) { 61 | mob = new ConfiguredMob(l, defaults); 62 | } else { 63 | mob = new ConfiguredMob(key, defaults); //use default values for YAML "key: []" blocks 64 | } 65 | limits.put(key.toUpperCase(), mob); 66 | } catch (ConfigurationException ex) { 67 | plugin.getLogger().warning(ex.getMessage()); 68 | } 69 | } 70 | } 71 | 72 | for(String egg : plugin.getConfig().getStringList("spawn_eggs")) { 73 | spawnEggs.add(Material.getMaterial(egg)); 74 | } 75 | System.out.println(spawnEggs); 76 | 77 | } 78 | 79 | 80 | /** 81 | * Get the "view distance," in chunks, to check for mobs when a new mob spawns. 82 | */ 83 | public int getRadius() { 84 | return radius; 85 | } 86 | 87 | 88 | /** 89 | * Is debug mode on? 90 | */ 91 | public boolean debug() { 92 | return debug; 93 | } 94 | 95 | 96 | /** 97 | * Ticks until a farm animal is ready to breed again. 98 | */ 99 | public int getBreedingTicks() { 100 | return breedingTicks; 101 | } 102 | 103 | 104 | /** 105 | * Ticks until a farm animal grows up. 106 | */ 107 | public int getGrowthTicks() { 108 | return growthTicks; 109 | } 110 | 111 | 112 | /** 113 | * Whether support for LogBlock entity removal logging is enabled 114 | * @return true if enabled 115 | */ 116 | public boolean logBlockEnabled() { 117 | return logBlock; 118 | } 119 | 120 | 121 | /** 122 | * Whether mobs' age should be relative to the last time they target a player 123 | * @return true if enabled 124 | */ 125 | public boolean relativeAgeEnabled() { 126 | return relativeAge; 127 | } 128 | 129 | 130 | /** 131 | * Get the global default limits 132 | */ 133 | public ConfiguredDefaults getDefaults() { 134 | return defaults; 135 | } 136 | 137 | /** 138 | * Gets a list of spawn eggs as defined in the config 139 | * @return A list of spawn eggs as Strings 140 | */ 141 | public List getSpawnEggs(){return spawnEggs;} 142 | 143 | 144 | /** 145 | * Get the limits for a specific mob type, gracefully falling back to values from the "default" block 146 | */ 147 | public ConfiguredMob getLimits(Entity entity) { 148 | String key = entity.getType().toString(); 149 | if (entity instanceof Sheep) { 150 | key = key + "_" + ((Sheep) entity).getColor().name().toUpperCase(); 151 | } 152 | if (limits.containsKey(key)) { 153 | return limits.get(key); 154 | } else { 155 | return new ConfiguredMob(key, defaults); 156 | } 157 | } 158 | 159 | 160 | public HashMap getAllLimits() { 161 | return limits; 162 | } 163 | 164 | 165 | } 166 | -------------------------------------------------------------------------------- /src/nu/nerd/moblimiter/listeners/EntityEvents.java: -------------------------------------------------------------------------------- 1 | package nu.nerd.moblimiter.listeners; 2 | 3 | import net.kyori.adventure.text.Component; 4 | import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; 5 | import nu.nerd.moblimiter.MobLimiter; 6 | import org.bukkit.entity.*; 7 | import org.bukkit.event.EventHandler; 8 | import org.bukkit.event.EventPriority; 9 | import org.bukkit.event.Listener; 10 | import org.bukkit.event.entity.CreatureSpawnEvent; 11 | import org.bukkit.event.entity.EntityDeathEvent; 12 | 13 | public class EntityEvents implements Listener { 14 | 15 | MobLimiter plugin; 16 | 17 | public EntityEvents() { 18 | plugin = MobLimiter.instance; 19 | plugin.getServer().getPluginManager().registerEvents(this, plugin); 20 | } 21 | 22 | // -------------------------------------------------------------------------------------------- 23 | 24 | /** 25 | * Formerly part of KeepBabyMobs. 26 | * Logs the death of an entity. 27 | */ 28 | @EventHandler(priority = EventPriority.NORMAL) 29 | public void onEntityDeath(EntityDeathEvent event) { 30 | Entity entity = event.getEntity(); 31 | Player player = event.getEntity().getKiller(); 32 | 33 | if(player == null || !(entity instanceof Breedable)) { 34 | if(entity instanceof Tadpole) { 35 | logDeath(player, entity, ""); 36 | } 37 | return; 38 | } 39 | 40 | Breedable breedable = (Breedable) entity; 41 | 42 | if(breedable.getAgeLock()) { 43 | String extrainfo = ""; 44 | if(entity instanceof Cat) { 45 | extrainfo = "type: " + ((Cat) entity).getCatType().name(); 46 | } 47 | if(entity instanceof Tameable) { 48 | Tameable tameable = ((Tameable) entity); 49 | if(tameable.isTamed()) { 50 | extrainfo = "owner: " + ((Tameable) entity).getOwner().getName(); 51 | } 52 | } 53 | 54 | logDeath(player, breedable, extrainfo); 55 | 56 | } 57 | 58 | } 59 | 60 | // -------------------------------------------------------------------------------------------- 61 | 62 | /** 63 | * Outputs the death details of an age locked baby animal 64 | * @param player The player who killed the animal 65 | * @param entity The mob killed 66 | * @param extrainfo Extra information like coat colour and owner 67 | */ 68 | public void logDeath(Player player, Entity entity, String extrainfo) { 69 | MobLimiter.instance.getComponentLogger().info(Component.text(String.format("%s killed %s named %s at %s %s", 70 | player.getName(), 71 | entity.getType(), 72 | PlainTextComponentSerializer.plainText().serialize(entity.customName()), 73 | MobLimiter.locationToString(entity.getLocation()), 74 | extrainfo))); 75 | } 76 | 77 | // -------------------------------------------------------------------------------------------- 78 | 79 | /** 80 | * Apply the breeding changes when a new animal is bred 81 | */ 82 | @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) 83 | public void onCreatureSpawnEvent(final CreatureSpawnEvent event) { 84 | CreatureSpawnEvent.SpawnReason reason = event.getSpawnReason(); 85 | if ((reason == CreatureSpawnEvent.SpawnReason.BREEDING || reason == CreatureSpawnEvent.SpawnReason.EGG || reason == CreatureSpawnEvent.SpawnReason.DISPENSE_EGG)) { 86 | if (isFarmAnimal(event.getEntity())) { 87 | applyBreedingChanges((Animals) event.getEntity()); 88 | for (Entity en : event.getEntity().getNearbyEntities(4, 4, 4)) { 89 | if (isFarmAnimal(en)) { 90 | applyBreedingChanges((Animals) en); 91 | } 92 | } 93 | } 94 | } 95 | } 96 | 97 | // -------------------------------------------------------------------------------------------- 98 | 99 | /** 100 | * Controls the rate farm animals grow up and the length of their breeding cooldown. 101 | * growthTicks: How many ticks until the animal becomes an adult. 102 | * breedingTicks: How many ticks until the animal can breed again. 103 | * A value of zero makes the condition instant, and -1 disables tampering with vanilla behavior. 104 | * This functionality was called "agecapbaby" and "agecapbaby" respectively in MobLimiter 1.x. 105 | * @param animal the animal to apply the changes to 106 | */ 107 | private void applyBreedingChanges(Animals animal) { 108 | if (animal.getAgeLock()) return; 109 | int growthTicks = plugin.getConfiguration().getGrowthTicks(); 110 | int breedingTicks = plugin.getConfiguration().getBreedingTicks(); 111 | if (growthTicks > -1 && !animal.isAdult()) { 112 | animal.setAge(Math.max(animal.getAge(), -growthTicks)); 113 | } else if (breedingTicks > -1 && animal.isAdult()) { 114 | animal.setAge(Math.min(animal.getAge(), breedingTicks)); 115 | } 116 | } 117 | 118 | // -------------------------------------------------------------------------------------------- 119 | 120 | /** 121 | * Whether the entity in question is a farm animal or not 122 | * @param entity the entity to check 123 | * @return true if this is a farm animal 124 | */ 125 | private boolean isFarmAnimal(Entity entity) { 126 | return (entity instanceof Animals) && !(entity instanceof Tameable); 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MobLimiter 2 | =========== 3 | 4 | Version 2 of MobLimiter, featuring three configurable Limiter engines that control the mob population in different ways. 5 | 6 | While the original version of MobLimiter was primarily designed to cull mobs on chunk unload, with limited support to 7 | prevent new spawns in real time, MobLimiter 2 is designed from the ground up to be more proactive about managing the 8 | spawning and removal of entities. The Limiters are: 9 | 10 | * **Age:** Mobs can be configured to have a maximum lifespan (in ticks) that results in the entity being killed and 11 | dropping its items after the limit is reached. ("Special" mobs are exempt.) MobLimiter will try to not kill breeding 12 | pairs of farm animals if it can, by not killing farm animals if there would be less than two in the chunk. 13 | 14 | * **Spawning:** MobLimiter can be configured to limit the spawning of new mobs in real time, checking the number of the 15 | applicable mob type in a "view distance" (as a chunk radius) as well as in an individual chunk and blocking the addition 16 | of extra mobs beyond the limit. 17 | 18 | * **Entity Unload:** Similar to version 2.0, the new entity-unload system checks for entities unloading within chunks and culls them down to numbers specified in the config, if desired. 19 | 20 | MobLimiter also offers spawner modification protection to prevent unwanted changes to the mobs a spawner produces, as well as mob age locking for when you want to keep your baby a baby forever. 21 | 22 | Configuration 23 | ------------- 24 | 25 | ### General Settings 26 | 27 | * `radius`: The "view distance" to check for mobs, as a chunk radius (e.g. 3 would be a 7x7 area) 28 | * `breeding_ticks`: Farm animal breeding cooldown in ticks (-1 to disable) 29 | * `growth_ticks`: Ticks for a farm animal to grow up (-1 to disable) 30 | * `logblock`: Enable LogBlock support. More below. 31 | * `debug`: Print debugging info to console 32 | 33 | 34 | ### Default Limits 35 | 36 | The `defaults` block defines limits that will globally apply to any mob type that doesn't have an explicit override 37 | defined in the `limits` block. (Undefined values fall back to `-1`, for disabled.) Specific mob limits *inherit* the 38 | default block, with any defined fields overriding the value from `defaults`. 39 | 40 | ``` 41 | defaults: 42 | age: 18000 #15 minutes in ticks 43 | max: 200 #200 in "view distance" 44 | chunk_max: 50 #50 in a single chunk 45 | cull: 4 #cull mobs down to this maximum on chunk unload 46 | ``` 47 | 48 | * `age`: Enable age limiting and remove the mob after a number of ticks. (e.g. 18000 for 15 minutes) 49 | * `max`: The maximum number of a mob type to be allowed to spawn in a "view distance" defined by `radius`. 50 | * `chunk_max`: The maximum number of a mob type to be allowed to spawn in a single chunk. 51 | * `cull`: If set to a value other than `-1`, the number of mobs to *not* be removed on chunk unload. 52 | 53 | 54 | ### Individual Mob Limits 55 | 56 | The `limits` block allows you to specify limits that apply to individual mob types. These inherit the values defined in 57 | `defaults`, overriding the values. 58 | 59 | Mob types are named using their Bukkit EntityType string, with the exception of sheep, which are addressed in the form of `sheep_white` or `sheep_red` so they can be handled individually for farming purposes. 60 | 61 | ``` 62 | limits: 63 | skeleton: 64 | max: 100 65 | chunkMax: 30 66 | age: 12000 67 | cow: 68 | chunkMax: 75 69 | age: 12000 70 | horse: 71 | age: -1 72 | villager: 73 | max: 200 74 | chunkMax: 50 75 | age: -1 76 | ``` 77 | 78 | 79 | ### Farm Animal Breeding Tweaks 80 | 81 | The `breeding_ticks` and `growth_ticks` fields define how many ticks a farm animal will remain a baby and the breeding 82 | cooldown, respectively. If your server is running at a full 20 ticks per second, a value of 400 for each would make 83 | the respective values approximately 20 seconds. 84 | 85 | If the value is set to zero, there will be no delay and the condition will be instantaneous. A value of -1 will disable 86 | tampering with vanilla breeding behavior. 87 | 88 | This function only affects farm animals, and ignores other breedable entities like ocelots, wolves and villagers. 89 | 90 | ### Prevent Spawner Modification 91 | 92 | MobLimiter blocks the modification of spawners through the use of spawn eggs. You can allow a player to modify them by 93 | granting the permission `moblimiter.spawners.bypass`. You can also customize which spawn eggs are blocked in the config 94 | under the `spawn_eggs` section. 95 | 96 | ### Mob Age Locking 97 | 98 | You're able to lock the age of passive baby mobs by naming them with a nametag. These babies will never grow up, even 99 | if fed with food. If one of these mobs is killed, a log will be made with the person who killed it, the type of mob, 100 | location, and any features specific to that mob (coat colour, owner, etc.). 101 | 102 | ### Special Mobs 103 | 104 | MobLimiter will not remove any mobs that are deemed to be "special" in some way that may make their removal undesirable. 105 | 106 | The criteria include: 107 | 108 | * Mobs with custom names, such as from a name tag 109 | 110 | * Tamed mobs 111 | 112 | * Elder guardians. (Regular guardians can be limited, but Elder ones won't be touched.) 113 | 114 | * Any mob that is holding an item, as it may have picked up a player's equipment. 115 | 116 | 117 | ### LogBlock Integration 118 | 119 | If LogBlock is running on the server, you can enable LogBlock integration by setting the `logblock` field to true in the 120 | config file. When enabled, mob removals will be tracked as kills in LogBlock when MobLimiter performs a chunk unload 121 | cull or age limit kill. 122 | 123 | Age limit kills are logged with a weapon of `watch` and chunk unload culling uses `gold sword`, both using a "player" 124 | name of `MobLimiter`. 125 | 126 | 127 | ### Commands 128 | 129 | * `/moblimiter` — Lists all subcommands. Available to all users. 130 | 131 | * `/moblimiter help` — Prints a description of what MobLimiter does. Available to all users. 132 | 133 | * `/moblimiter reload` — Reload the plugin configuration. Requires `moblimiter.reload`. 134 | 135 | * `/moblimiter count` — Count all living entities in your chunk and view radius. Requires `moblimiter.count`. 136 | 137 | * `/moblimiter limits` — Print all configured limits. Requires `moblimiter.limits`. 138 | 139 | * `/moblimiter check` — Inspect the mob you're looking at, printing its age, limits and statuses. Requires `moblimiter.check`. 140 | 141 | All commands can be accessed with the `moblimiter.*` permission node. 142 | 143 | -------------------------------------------------------------------------------- /src/nu/nerd/moblimiter/EntityHelper.java: -------------------------------------------------------------------------------- 1 | package nu.nerd.moblimiter; 2 | 3 | 4 | import nu.nerd.moblimiter.configuration.Configuration; 5 | import nu.nerd.moblimiter.configuration.ConfiguredMob; 6 | import org.bukkit.*; 7 | import org.bukkit.block.Block; 8 | import org.bukkit.entity.*; 9 | import org.bukkit.inventory.EntityEquipment; 10 | import org.bukkit.inventory.ItemStack; 11 | import org.bukkit.util.Vector; 12 | 13 | import java.util.*; 14 | 15 | /** 16 | * Mob-related methods that may be shared across classes 17 | */ 18 | public class EntityHelper { 19 | 20 | private static final HashSet BLACKLIST = new HashSet<>(Arrays.asList( 21 | EntityType.ARMOR_STAND, EntityType.PLAYER 22 | )); 23 | 24 | /** 25 | * Check if this is a mob that should ever be limited. 26 | * Merely checking Animals or Monster subclassing is not sufficient due to Bukkit inconsistency. 27 | * e.g. Armor Stands are LivingEntities, apparently. 28 | * So to check whether something is a limitable mob, we check if it's a LivingEntity first and then 29 | * apply a blacklist of edge cases such as Armor Stands. 30 | * @param entity The entity to check 31 | * @return true if the entity is a limitable mob 32 | */ 33 | public static boolean isLimitableMob(Entity entity) { 34 | return entity instanceof LivingEntity && !BLACKLIST.contains(entity.getType()); 35 | } 36 | 37 | 38 | /** 39 | * Check if this is a "special mob" that shouldn't be removed in any circumstance 40 | * @param entity entity to check 41 | * @return true if this is a special mob 42 | */ 43 | public static boolean isSpecialMob(LivingEntity entity) { 44 | 45 | // Keep mobs with custom names 46 | if (entity.customName() != null) { 47 | return true; 48 | } 49 | 50 | // Don't remove tamed mobs 51 | if (entity instanceof Tameable) { 52 | Tameable tameable = (Tameable) entity; 53 | if (tameable.isTamed()) { 54 | return true; 55 | } 56 | } 57 | 58 | // Save the sponge! 59 | if (entity.getType() == EntityType.ELDER_GUARDIAN) { 60 | return true; 61 | } 62 | 63 | // Don't remove mobs that are holding something, which they may have picked up 64 | EntityEquipment equipment = entity.getEquipment(); 65 | for (ItemStack armor : equipment.getArmorContents()) { 66 | // Unarmored mobs, even animals, spawn with 1xAIR as armor. 67 | if (armor != null && armor.getType() != Material.AIR) { 68 | return true; 69 | } 70 | } 71 | 72 | // Don't cull allays who are holding items 73 | if(entity.getType() == EntityType.ALLAY) { 74 | if(equipment.getItemInMainHand() != null && equipment.getItemInMainHand().getType() != Material.AIR) { 75 | return true; 76 | } 77 | } 78 | 79 | return false; 80 | 81 | } 82 | 83 | 84 | /** 85 | * If this is an Animal and there are two or less in the chunk, don't remove them. 86 | * This protects breeding pairs for farm animals. 87 | * @param entity the entity to check 88 | * @return false if the entity doesn't match the criteria 89 | */ 90 | public static boolean isBreedingPair(Entity entity) { 91 | if (!(entity instanceof Animals)) return false; 92 | if (entity instanceof Tameable) return false; 93 | int count = 0; 94 | ConfiguredMob mob = getConfiguration().getLimits(entity); 95 | for (Entity e : entity.getLocation().getChunk().getEntities()) { 96 | ConfiguredMob m = getConfiguration().getLimits(e); 97 | if (m.getKey().equals(mob.getKey()) && !e.isDead()) { 98 | count++; 99 | } 100 | } 101 | return count < 3; 102 | } 103 | 104 | 105 | /** 106 | * Return details about an entity for debugging 107 | * @param entity the entity 108 | * @return String representation of the mob name and location 109 | */ 110 | public static String getMobDescription(Entity entity) { 111 | String type = entity.getType().toString(); 112 | String world = entity.getLocation().getWorld().getName(); 113 | Location loc = entity.getLocation(); 114 | return String.format("%s at (%s,%d,%d,%d)", type, world, loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()); 115 | } 116 | 117 | 118 | /** 119 | * Return the mob a player is looking at 120 | * @param player The player to check 121 | * @return null or LivingEntity 122 | */ 123 | public static LivingEntity getMobInLineOfSight(Player player) { 124 | List entities = player.getNearbyEntities(5, 1, 5); 125 | Iterator iterator = entities.iterator(); 126 | while (iterator.hasNext()) { 127 | Entity ent = iterator.next(); 128 | if (!(ent instanceof LivingEntity)) iterator.remove(); 129 | } 130 | Set nullSet = null; 131 | for (Block block : player.getLineOfSight(nullSet, 6)) { 132 | if (block.getType() != Material.AIR) break; //view is obstructed 133 | for (Entity ent : entities) { 134 | Vector b = block.getLocation().toVector(); 135 | Vector head = ent.getLocation().toVector().add(new Vector(0, 1, 0)); 136 | Vector foot = ent.getLocation().toVector(); 137 | if (head.isInSphere(b, 1.25) || foot.isInSphere(b, 1.25)) { 138 | return (LivingEntity) ent; 139 | } 140 | } 141 | } 142 | return null; 143 | } 144 | 145 | 146 | /** 147 | * Build a HashMap of entity types and their counts in a chunk 148 | * @param chunk The chunk to check 149 | * @return A HashMap summarizing the entity breakdown of a chunk 150 | */ 151 | public static HashMap summarizeMobsInChunk(Chunk chunk) { 152 | HashMap chunkCounts = new HashMap(); 153 | for (Entity e : chunk.getEntities()) { 154 | if (e.isDead() || !isLimitableMob(e)) continue; 155 | ConfiguredMob mob = getConfiguration().getLimits(e); 156 | if (chunkCounts.containsKey(mob.getKey())) { 157 | int count = chunkCounts.get(mob.getKey()) + 1; 158 | chunkCounts.put(mob.getKey(), count); 159 | } else { 160 | chunkCounts.put(mob.getKey(), 1); 161 | } 162 | } 163 | return chunkCounts; 164 | } 165 | 166 | 167 | /** 168 | * Build a HashMap of entity types and their counts in a chunk radius 169 | * @param start The center of the chunk radius 170 | * @param radius The radius of chunks to check 171 | * @return A HashMap summarizing the entity breakdown within a chunk radius 172 | */ 173 | public static HashMap summarizeMobsInRadius(Chunk start, int radius) { 174 | HashMap radCounts = new HashMap(); 175 | World world = start.getWorld(); 176 | for (int x = start.getX() - radius; x <= start.getX() + radius; x++) { 177 | for (int z = start.getZ() - radius; z <= start.getZ() + radius; z++) { 178 | Chunk c = world.getChunkAt(x, z); 179 | for (Entity e : c.getEntities()) { 180 | if (e.isDead() || !isLimitableMob(e)) continue; 181 | ConfiguredMob mob = getConfiguration().getLimits(e); 182 | if (radCounts.containsKey(mob.getKey())) { 183 | int count = radCounts.get(mob.getKey()) + 1; 184 | radCounts.put(mob.getKey(), count); 185 | } else { 186 | radCounts.put(mob.getKey(), 1); 187 | } 188 | } 189 | } 190 | } 191 | return radCounts; 192 | } 193 | 194 | 195 | /** 196 | * Convenience method to get the plugin configuration 197 | */ 198 | public static Configuration getConfiguration() { 199 | return MobLimiter.instance.getConfiguration(); 200 | } 201 | 202 | 203 | } 204 | -------------------------------------------------------------------------------- /src/nu/nerd/moblimiter/CommandHandler.java: -------------------------------------------------------------------------------- 1 | package nu.nerd.moblimiter; 2 | 3 | 4 | import net.kyori.adventure.text.Component; 5 | import net.kyori.adventure.text.TextComponent; 6 | import net.kyori.adventure.text.format.NamedTextColor; 7 | import net.kyori.adventure.text.format.TextColor; 8 | import net.kyori.adventure.text.format.TextDecoration; 9 | import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; 10 | import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; 11 | import net.md_5.bungee.api.ChatColor; 12 | import nu.nerd.moblimiter.configuration.ConfiguredDefaults; 13 | import nu.nerd.moblimiter.configuration.ConfiguredMob; 14 | import org.bukkit.Chunk; 15 | import org.bukkit.command.Command; 16 | import org.bukkit.command.CommandExecutor; 17 | import org.bukkit.command.CommandSender; 18 | import org.bukkit.entity.*; 19 | 20 | import java.util.*; 21 | 22 | public class CommandHandler implements CommandExecutor { 23 | 24 | 25 | private MobLimiter plugin; 26 | 27 | 28 | public CommandHandler() { 29 | plugin = MobLimiter.instance; 30 | plugin.getCommand("moblimiter").setExecutor(this); 31 | } 32 | 33 | // -------------------------------------------------------------------------------------------- 34 | 35 | public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) { 36 | if (args.length == 1 && args[0].equalsIgnoreCase("reload")) { 37 | reloadConfig(sender); 38 | } else if (args.length > 0 && args[0].equalsIgnoreCase("count")) { 39 | countCommand(sender); 40 | } else if (args.length > 0 && args[0].equalsIgnoreCase("limits")) { 41 | limitsCommand(sender, args); 42 | } else if (args.length > 0 && args[0].equalsIgnoreCase("check")) { 43 | checkCommand(sender); 44 | } else if (args.length > 0 && args[0].equalsIgnoreCase("help")) { 45 | helpText(sender, cmd, args); 46 | } else { 47 | commandsList(sender); 48 | } 49 | return true; 50 | } 51 | 52 | // -------------------------------------------------------------------------------------------- 53 | 54 | /** 55 | * Command to reload the configuration 56 | */ 57 | private void reloadConfig(CommandSender sender) { 58 | if (!sender.hasPermission("moblimiter.reload")) { 59 | sender.sendMessage(Component.text("You don't have permission to do this.") 60 | .color(TextColor.fromHexString("#FF5555"))); 61 | return; 62 | } 63 | plugin.getConfiguration().load(); 64 | sender.sendMessage(Component.text("MobLimiter config reloaded!") 65 | .color(TextColor.fromHexString("#FFAA00"))); 66 | } 67 | 68 | // -------------------------------------------------------------------------------------------- 69 | 70 | /** 71 | * When a player runs /moblimiter with no arguments, list available commands 72 | * and guide newbies to /moblimiter help 73 | * 74 | * @param sender 75 | */ 76 | private void commandsList(CommandSender sender) { 77 | 78 | Component helpMessage = LegacyComponentSerializer.legacyAmpersand() 79 | .deserialize("&6&lMobLimiter: Available Commands&r"); 80 | helpMessage = helpMessage.appendNewline().append(LegacyComponentSerializer.legacyAmpersand() 81 | .deserialize("&6/moblimiter help&r - What is MobLimiter and how does it work?")); 82 | if (sender.hasPermission("moblimiter.limits")) { 83 | helpMessage = helpMessage.appendNewline().append(LegacyComponentSerializer.legacyAmpersand() 84 | .deserialize("&6/moblimiter limits&r - List the limits for each mob type")); 85 | } 86 | if (sender.hasPermission("moblimiter.count")) { 87 | helpMessage = helpMessage.appendNewline().append(LegacyComponentSerializer.legacyAmpersand() 88 | .deserialize("&6/moblimiter count&r - Count entities in the current chunk and radius")); 89 | } 90 | if (sender.hasPermission("moblimiter.check")) { 91 | helpMessage = helpMessage.appendNewline().append(LegacyComponentSerializer.legacyAmpersand() 92 | .deserialize("&6/moblimiter check&r - Inspect limiting details for the mob you're looking at")); 93 | } 94 | if (sender.hasPermission("moblimiter.reload")) { 95 | helpMessage = helpMessage.appendNewline().append(LegacyComponentSerializer.legacyAmpersand() 96 | .deserialize("&6/moblimiter reload&r - Reload configuration from disk")); 97 | } 98 | sender.sendMessage(helpMessage); 99 | } 100 | 101 | // -------------------------------------------------------------------------------------------- 102 | 103 | /** 104 | * When a player runs /moblimiter help, print help text 105 | */ 106 | private void helpText(CommandSender sender, Command cmd, String[] args) { 107 | 108 | int num = 1; 109 | if (args.length > 1) { 110 | try { 111 | num = Integer.parseInt(args[1]); 112 | } catch (NumberFormatException ex) { 113 | num = 1; 114 | } 115 | } 116 | 117 | if (num == 1) { 118 | Component helpMessage = LegacyComponentSerializer.legacyAmpersand() 119 | .deserialize("&6This server runs a plugin called &eMobLimiter" + 120 | " &6in order to manage the amount of mobs on the server, as too many mobs creates lag" + 121 | " which affects everyone. MobLimiter uses several methods to manage the mob population," + 122 | " depending on how it is configured:").appendNewline(); 123 | 124 | helpMessage = helpMessage.append(LegacyComponentSerializer.legacyAmpersand() 125 | .deserialize("&e1. Entity unload culling")) 126 | .appendNewline().append(LegacyComponentSerializer.legacyAmpersand() 127 | .deserialize("&6Mobs are culled down to a defined maximum" + 128 | " whenever a cluster of entities unloads or the server restarts.")) 129 | .appendNewline(); 130 | 131 | helpMessage = helpMessage.append(LegacyComponentSerializer.legacyAmpersand() 132 | .deserialize("&e2. Real-time spawn limiting")) 133 | .appendNewline().append(LegacyComponentSerializer.legacyAmpersand() 134 | .deserialize("&6Mobs are tracked in real-time and new spawns" + 135 | " are prevented in an area if there are more than a certain amount" + 136 | " of that mob type there already.")); 137 | 138 | sender.sendMessage(helpMessage); 139 | } 140 | if (num == 2) { 141 | 142 | sender.sendMessage(LegacyComponentSerializer.legacyAmpersand() 143 | .deserialize("&e3. Age limiting").appendNewline() 144 | .append(LegacyComponentSerializer.legacyAmpersand() 145 | .deserialize("&6Mobs automatically die when they reach a certain age," + 146 | " dropping their items when they do. " + 147 | "e.g. a skeleton may live for 15 minutes before dying.")) 148 | .appendNewline().append(LegacyComponentSerializer.legacyAmpersand() 149 | .deserialize("&6In all cases, \"special\" mobs are always protected" + 150 | " and will not be removed by the plugin. This includes mobs with" + 151 | " custom names, tamed animals, and mobs wearing armor.")) 152 | .appendNewline().append(LegacyComponentSerializer.legacyAmpersand() 153 | .deserialize("To make up for the culling of passive farm animals," + 154 | " the plugin shortens the growth rate of baby animals and the" + 155 | " waiting time in between breeding events, to approximately &7" + 156 | (plugin.getConfiguration().getGrowthTicks() / 20) + " &6and &7" + 157 | (plugin.getConfiguration().getBreedingTicks() / 20) + " &6seconds " + 158 | "respectively."))); 159 | } 160 | sender.sendMessage(Component.text(String.format("[Page %d/2 - /moblimiter help #]", num))); 161 | } 162 | 163 | // -------------------------------------------------------------------------------------------- 164 | 165 | /** 166 | * Command to count entities in the chunk and radius 167 | */ 168 | private void countCommand(CommandSender sender) { 169 | 170 | if (!sender.hasPermission("moblimiter.count")) return; 171 | if (!(sender instanceof Player)) { 172 | sender.sendMessage("Console can't do that."); 173 | return; 174 | } 175 | Player player = (Player) sender; 176 | Chunk playerChunk = player.getLocation().getChunk(); 177 | int chunkRadius = plugin.getConfiguration().getRadius(); 178 | 179 | // Count mobs in the chunk 180 | HashMap chunkCounts = EntityHelper.summarizeMobsInChunk(playerChunk); 181 | 182 | // Count mobs in the radius 183 | HashMap radCounts = EntityHelper.summarizeMobsInRadius(playerChunk, chunkRadius); 184 | 185 | // Print results 186 | Component countMessage = LegacyComponentSerializer.legacyAmpersand().deserialize("&6Entities in chunk: &r"); 187 | 188 | sender.sendMessage(countMessager(chunkCounts, "&6Entities in chunk: &r")); 189 | sender.sendMessage(countMessager(radCounts, "&6Entities in radius: &r")); 190 | 191 | } 192 | 193 | // -------------------------------------------------------------------------------------------- 194 | 195 | /** 196 | * Outputs the number of mobs nearby 197 | * @param map A hashmap of mob names and counts 198 | * @param header 199 | * @return 200 | */ 201 | private Component countMessager(HashMap map, String header) { 202 | Component countMessage = LegacyComponentSerializer.legacyAmpersand().deserialize(header); 203 | for (Map.Entry entry : map.entrySet()) { 204 | countMessage = countMessage.append(Component.text( 205 | "&f" + 206 | entry.getKey().toLowerCase() + 207 | ": &7" + 208 | entry.getValue() + 209 | " ")); 210 | } 211 | return countMessage; 212 | } 213 | 214 | // -------------------------------------------------------------------------------------------- 215 | 216 | /** 217 | * Command to display the configured limits 218 | */ 219 | private void limitsCommand(CommandSender sender, String[] args) { 220 | if (!sender.hasPermission("moblimiter.limits")) return; 221 | List lines = new ArrayList<>(); 222 | ConfiguredDefaults d = plugin.getConfiguration().getDefaults(); 223 | String values = String.format("Age: %d Max: %d Chunk: %d Cull: %d", d.getAge(), 224 | d.getMax(), d.getChunkMax(), d.getCull()); 225 | lines.add(String.format("%s%s %s[%s]", "&9", "DEFAULT", "&e", values)); 226 | TreeMap limits = new TreeMap<>(plugin.getConfiguration().getAllLimits()); 227 | for (ConfiguredMob l : limits.values()) { 228 | values = String.format("Age: %d Max: %d Chunk: %d Cull: %d", 229 | l.getAge(), l.getMax(), l.getChunkMax(), l.getCull()); 230 | lines.add(String.format("%s%s %s[%s]", "&6", l.getKey(), "&e", values)); 231 | } 232 | int page = 1; 233 | if (args.length > 1) { 234 | try { 235 | page = Integer.parseInt(args[1]); 236 | } catch (NumberFormatException ex) { 237 | page = 1; 238 | } 239 | } 240 | if (lines.size() > 0) { 241 | int offset = (page - 1) * 10; 242 | int pages = ((lines.size() - 1) / 10) + 1; 243 | for (int i = offset; i <= offset + 9; i++) { 244 | if (i >= lines.size()) break; 245 | sender.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(lines.get(i))); 246 | } 247 | sender.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(String.format("%s[Page %d/%d]", "&7", page, pages))); 248 | } else { 249 | sender.sendMessage(Component.text("There are no limits configured. All mob types will fall back" + 250 | " to the default block.").color(TextColor.fromHexString("#FF5555"))); 251 | } 252 | } 253 | 254 | // -------------------------------------------------------------------------------------------- 255 | 256 | /** 257 | * Command to inspect details about the mob the player is looking at 258 | */ 259 | private void checkCommand(CommandSender sender) { 260 | 261 | if (!sender.hasPermission("moblimiter.check")) return; 262 | if (!(sender instanceof Player)) { 263 | sender.sendMessage("Console can't do that."); 264 | return; 265 | } 266 | 267 | Player player = (Player) sender; 268 | LivingEntity entity = EntityHelper.getMobInLineOfSight(player); 269 | if (entity == null || entity.isDead() || !EntityHelper.isLimitableMob(entity)) { 270 | sender.sendMessage("No mob in sight"); 271 | return; 272 | } 273 | 274 | ConfiguredMob limits = plugin.getConfiguration().getLimits(entity); 275 | Chunk chunk = entity.getLocation().getChunk(); 276 | int chunkRadius = plugin.getConfiguration().getRadius(); 277 | HashMap chunkCounts = EntityHelper.summarizeMobsInChunk(chunk); 278 | HashMap radCounts = EntityHelper.summarizeMobsInRadius(chunk, chunkRadius); 279 | int nearby = radCounts.get(limits.getKey()); 280 | int inChunk = chunkCounts.get(limits.getKey()); 281 | boolean isSpecial = EntityHelper.isSpecialMob(entity); 282 | boolean isCullable = !entity.isDead() && (entity instanceof Animals || entity instanceof Monster); 283 | boolean canSpawnChunk = (limits.getChunkMax() > inChunk || limits.getChunkMax() < 0); 284 | boolean canSpawnNearby = (limits.getMax() > nearby || limits.getMax() < 0); 285 | boolean moreCanSpawn = canSpawnChunk && canSpawnNearby; 286 | 287 | sender.sendMessage(Component.text("---").color(TextColor.fromHexString("#FFAA00"))); 288 | sender.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize( 289 | String.format("&7 (%d/%d nearby, %d/%d in chunk)", nearby, limits.getMax(), inChunk, 290 | limits.getChunkMax()))); 291 | 292 | String ageStr; 293 | if (limits.getAge() > -1) { 294 | if (!EntityHelper.isBreedingPair(entity)) { 295 | ageStr = String.format("%d/%d", plugin.getAgeLimiter().adjustedAge(entity), limits.getAge()); 296 | } else { 297 | ageStr = "Breeding Pair Exempted"; 298 | } 299 | } else { 300 | ageStr = "Not Limited"; 301 | } 302 | 303 | String chunkCullStr = (limits.getCull() > -1 && isCullable) ? String.format("%d", limits.getCull()) : "Unlimited"; 304 | 305 | sender.sendMessage(LegacyComponentSerializer.legacyAmpersand() 306 | .deserialize(String.format("Age: %s%s", "&7", ageStr))); 307 | sender.sendMessage(LegacyComponentSerializer.legacyAmpersand() 308 | .deserialize(String.format("Is Special: %s%b", "&7", isSpecial))); 309 | sender.sendMessage(LegacyComponentSerializer.legacyAmpersand() 310 | .deserialize(String.format("Chunk Unload Cap: %s%s", "&7", chunkCullStr))); 311 | sender.sendMessage(LegacyComponentSerializer.legacyAmpersand() 312 | .deserialize(String.format("More Can Spawn: %s%b", "&7", moreCanSpawn))); 313 | 314 | sender.sendMessage(Component.text("---").color(TextColor.fromHexString("#FFAA00"))); 315 | 316 | } 317 | 318 | 319 | } 320 | --------------------------------------------------------------------------------