43 Commits
Author SHA1 Message Date
redwallhp 79b631698c Incrementing version 2017-12-14 20:30:00 -05:00
redwallhp dffe3fde66 Relative age option for age limiter 2017-12-12 17:20:10 -05:00
redwallhp feacc56bf2 Added default config entries for new 1.11 mobs 2017-01-11 21:55:19 -05:00
redwallhp 682e47f731 Fixed Maven build error 2016-11-03 22:42:06 -04:00
redwallhp 9a480ec6e6 Added subcommand listing to /moblimiter and bumped info text to
/moblimiter help
2016-07-12 05:27:01 -04:00
redwallhp c8fc5ea4ed Added sensible default config, /moblimiter limits is now sortted
alphabetically and now shows an accurate page count instead of "*/2" and
configuration will now properly fall back to defaults when YAML empty
values are specified (e.g. "key: []").
2016-07-12 05:02:08 -04:00
redwallhp d1629ac8a5 Updated the default config to reflect updated dyed sheep handling 2016-06-16 03:54:28 -04:00
redwallhp 6ea3862e62 Rewrites to accommodate dyed sheep properly 2016-06-16 03:48:48 -04:00
redwallhp ac49c287b0 Prevent error if non-limitable mobs are in range of /moblimiter check 2016-06-11 00:30:55 -04:00
redwallhp a603e025b1 Include PLAYER in EntityType blacklist for isLimitableMob() 2016-06-10 05:12:10 -04:00
redwallhp f18306cc57 Adding a master isLimitableMob() method to determine limitability, since
apparently armor stands are LivingEntities, slimes aren't Monsters and
all manner of inconsistent nonsense. TL;DR: the Bukkit API is insane
2016-06-10 02:55:07 -04:00
redwallhp 912e7a1269 Improving LogBlock support 2016-06-09 23:22:28 -04:00
redwallhp c53d5fbea2 Implemented LogBlock logging of mob removals 2016-06-09 23:02:15 -04:00
redwallhp ff28b09fd9 Added DEFAULT line to /moblimiter limits 2016-06-09 03:32:50 -04:00
redwallhp 5c3882cee5 Fixed readme 2016-06-07 22:05:10 -04:00
redwallhp 27cf4d8951 Updated README 2016-06-07 22:04:29 -04:00
redwallhp a5c6270e5c Expanding /moblimiter check command 2016-06-07 21:57:07 -04:00
redwallhp d88ba025c1 Added /moblimiter check to inspect details about a specific mob 2016-06-07 20:51:06 -04:00
redwallhp 41bd5016e8 Add more isDead checks, just to be on the safe side 2016-06-07 04:18:13 -04:00
redwallhp 1ee6292027 Fix for breeding pairs not being left. 2016-06-07 04:03:09 -04:00
redwallhp 3fd7236086 Fixed configuration bug and implemented more commands 2016-06-07 02:20:00 -04:00
redwallhp b634b33a54 Set age culling default value in default config.yml to -1 for safety 2016-06-07 00:35:24 -04:00
redwallhp 61f769bd17 Added reload command 2016-06-07 00:34:26 -04:00
redwallhp 0d96454388 Fixed compilation and runtime issues 2016-06-07 00:19:29 -04:00
redwallhp a5d5cef457 Added /moblimiter command with info 2016-06-06 23:55:00 -04:00
redwallhp 3709d5a072 Made the default config.yml more interesting 2016-06-06 21:35:05 -04:00
redwallhp c822bc3182 Fleshing out README 2016-06-06 21:08:20 -04:00
redwallhp 92bbfc9565 Bringing the breeding/growth acceleration over from MobLimiter 1.x. 2016-06-06 19:56:32 -04:00
redwallhp 1ac1f11b26 Changing Configuration system to accommodate dyed sheep 2016-06-06 18:55:37 -04:00
redwallhp 2673f521dd AgeLimiter will now kill mobs instead of removing them, causing them to
drop their items.
2016-06-06 17:32:53 -04:00
redwallhp c5f410ca4a Changed ConfiguredDefaults fallbacks to -1 for all, for safety reasons.
Don't want a really broken configuration defaulting all mob types to
being wiped out or something.
2016-06-06 16:59:09 -04:00
redwallhp 81ddf25ee9 Run removeMobs() on all loaded chunks on shutdown 2016-06-06 04:32:22 -04:00
redwallhp 85c046cc55 Implemented Chunkunloadlimiter 2016-06-06 04:24:39 -04:00
redwallhp 0fac0edcb4 Added Tameable check to isBreedingPair() 2016-06-06 03:42:49 -04:00
redwallhp 220459f5b7 Working on AgeLimiter 2016-06-06 03:38:42 -04:00
redwallhp a845051047 Beginnings of AgeLimiter, corrected lack of check for "-1" values in
limit configuration
2016-06-06 02:34:16 -04:00
redwallhp 7f220834a1 Removing innacurate README 2016-06-06 01:13:29 -04:00
redwallhp a80d2df85c Implemented SpawnLimiter 2016-06-06 01:08:33 -04:00
redwallhp 192596e755 Added initial structure for the Limiters 2016-06-05 21:46:13 -04:00
redwallhp d5edf101c0 Further configuration additions 2016-06-05 20:38:51 -04:00
redwallhp 8007bb77dd Working on objects to hold configuration data 2016-06-05 20:26:14 -04:00
redwallhp a85e14e14e Initial configuration structure 2016-06-05 19:36:29 -04:00
redwallhp 9331ade285 Beginning work on version 2.0 with a mostly clean slate.
This will be a major rewrite that will function significantly
differently from the original, as culling mobs on chunk unload just
doesn't scale well with modern Minecraft.
2016-06-05 19:18:50 -04:00
18 changed files with 1657 additions and 665 deletions

No files matched your search

+2 -1
View File
@@ -1,6 +1,7 @@
The MIT License
Copyright (c) 2012 Travis Watkins <amaranth@probablycoding.com>
Version 1.x: Copyright (c) 2012 Travis Watkins <amaranth@probablycoding.com>
Version 2.x: Copyright (c) 2016 redwall_hp
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+105 -24
View File
@@ -1,49 +1,130 @@
MobLimiter
===========
Version 2 of MobLimiter, featuring three configurable Limiter engines that control the mob population in different ways.
While the original version of MobLimiter was primarily designed to cull mobs on chunk unload, with limited support to
prevent new spawns in real time, MobLimiter 2 is designed from the ground up to be more proactive about managing the
spawning and removal of entities. The Limiters are:
* **Age:** Mobs can be configured to have a maximum lifespan (in ticks) that results in the entity being killed and
dropping its items after the limit is reached. ("Special" mobs are exempt.) MobLimiter will try to not kill breeding
pairs of farm animals if it can, by not killing farm animals if there would be less than two in the chunk.
* **Spawning:** MobLimiter can be configured to limit the spawning of new mobs in real time, checking the number of the
applicable mob type in a "view distance" (as a chunk radius) as well as in an individual chunk and blocking the addition
of extra mobs beyond the limit.
* **Chunk Unload:** The chunk-unload culling feature of MobLimiter 1.x is still available, if its use is desired.
Configuration
-------------
### Mob caps
### General Settings
The config.yml is made up of lines of `mob: limit`
* `radius`: The "view distance" to check for mobs, as a chunk radius (e.g. 3 would be a 7x7 area)
* `breeding_ticks`: Farm animal breeding cooldown in ticks (-1 to disable)
* `growth_ticks`: Ticks for a farm animal to grow up (-1 to disable)
* `logblock`: Enable LogBlock support. More below.
* `debug`: Print debugging info to console
**Mob** is the bukkit name for the entity. For example *cave spider* is displayed as *cave_spider* in the configuration
To limit a mobs numbers on restart/unload we define it like so:
### Default Limits
zombie: 4
creeper: 10
The `defaults` block defines limits that will globally apply to any mob type that doesn't have an explicit override
defined in the `limits` block. (Undefined values fall back to `-1`, for disabled.) Specific mob limits *inherit* the
default block, with any defined fields overriding the value from `defaults`.
The name of the **entity** followed by a valid **int** after a **:** is placed on a single line
```
defaults:
age: 18000 #15 minutes in ticks
max: 200 #200 in "view distance"
chunk_max: 50 #50 in a single chunk
cull: 4 #cull mobs down to this maximum on chunk unload
```
Any mobs not given numbers in the config will be ignored and no limiting will happen to that specific mob type. However, most mobs have default limits configured in the config.yml file built into the plugin. You can explicitly specify unlimited numbers of a particular mob using a negative number. For example:
* `age`: Enable age limiting and remove the mob after a number of ticks. (e.g. 18000 for 15 minutes)
* `max`: The maximum number of a mob type to be allowed to spawn in a "view distance" defined by `radius`.
* `chunk_max`: The maximum number of a mob type to be allowed to spawn in a single chunk.
* `cull`: If set to a value other than `-1`, the number of mobs to *not* be removed on chunk unload.
ocelot: -1
wolf: -1
*Please note, leaving emtpy lines in the config is a bad idea, don't do this, it won't crash or corrupt the config, it just makes things messy*
### Individual Mob Limits
### Farm animal breeding caps
The `limits` block allows you to specify limits that apply to individual mob types. These inherit the values defined in
`defaults`, overriding the values.
There are two additional options that can be specified in config.yml: `agecapbaby` and `agecapbreed`. These specify how long a farm animal should remain a baby and the adult breeding cooldown, respectively.
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.
These are measured in ticks. A tick is at most 1/20th of a second, possibly longer depending on server load. The following example sets both values to 300 ticks (15+ seconds):
```
limits:
skeleton:
max: 100
chunkMax: 30
age: 12000
cow:
chunkMax: 75
age: 12000
horse:
age: -1
villager:
max: 200
chunkMax: 50
age: -1
```
agecapbaby: 300
agecapbreed: 300
If the number of ticks is 0, baby farm animals will instantly become adults, and adults will have no breeding cooldown. If the number of ticks is less than 0, farm animal breeding is left to vanilla settings.
### Farm Animal Breeding Tweaks
Other breedable entities (ocelots, wolves, villagers) are unaffected by these options.
The `breeding_ticks` and `growth_ticks` fields define how many ticks a farm animal will remain a baby and the breeding
cooldown, respectively. If your server is running at a full 20 ticks per second, a value of 400 for each would make
the respective values approximately 20 seconds.
Running
-------
If the value is set to zero, there will be no delay and the condition will be instantaneous. A value of -1 will disable
tampering with vanilla breeding behavior.
The running of MobLimiter happens on **chunk unload** as well as **plugin disable** events.
This function only affects farm animals, and ignores other breedable entities like ocelots, wolves and villagers.
This means when players navigate away from an area or log out with nobody nearby, the limits will be enforced to those chunks unloaded at that time.
It is also worth noting that limiting will happen on server shutdown **as well as reloads** due to the need for *onDisable* hooks.
### Special Mobs
MobLimiter will not remove any mobs that are deemed to be "special" in some way that may make their removal undesirable.
The criteria include:
* Mobs with custom names, such as from a name tag
* Tamed mobs
* Elder guardians. (Regular guardians can be limited, but Elder ones won't be touched.)
* Any mob that is holding an item, as it may have picked up a player's equipment.
### LogBlock Integration
If LogBlock is running on the server, you can enable LogBlock integration by setting the `logblock` field to true in the
config file. When enabled, mob removals will be tracked as kills in LogBlock when MobLimiter performs a chunk unload
cull or age limit kill.
Age limit kills are logged with a weapon of `watch` and chunk unload culling uses `gold sword`, both using a "player"
name of `MobLimiter`.
### Commands
* `/moblimiter` — Lists all subcommands. Available to all users.
* `/moblimiter help` — Prints a description of what MobLimiter does. Available to all users.
* `/moblimiter reload` — Reload the plugin configuration. Requires `moblimiter.reload`.
* `/moblimiter count` — Count all living entities in your chunk and view radius. Requires `moblimiter.count`.
* `/moblimiter limits` — Print all configured limits. Requires `moblimiter.limits`.
* `/moblimiter check` — Inspect the mob you're looking at, printing its age, limits and statuses. Requires `moblimiter.check`.
All commands can be accessed with the `moblimiter.*` permission node.
Any issues or bugs feel free to create an issue via the [github issues](https://github.com/NerdNu/MobLimiter/issues) tracker.
+121 -47
View File
@@ -1,47 +1,121 @@
sheepwhite: 4
sheeporange: 2
sheepmagenta: 2
sheeplight_blue: 2
sheepyellow: 2
sheeplime: 2
sheeppink: 2
sheepgray: 2
sheepsilver: 2
sheepcyan: 2
sheeppurple: 2
sheepblue: 2
sheepbrown: 2
sheepgreen: 2
sheepred: 2
sheepblack: 2
chicken: 4
cow: 4
mushroom_cow: 4
ocelot: 4
pig: 4
wolf: 4
rabbit: 4
blaze: 4
cave_spider: 4
creeper: 4
enderman: 4
guardian: 4
ghast: 1
pig_zombie: 4
silverfish: 6
skeleton: 4
spider: 4
zombie: 1
agecapbaby: 300
agecapbreed: 300
settings:
debug:
limit_spawn: false
limits: false
chunk_unload: false
age_cap: false
special: false
limit_natural_spawn: false
limit_spawner_spawn: false
spawner_limited: []
natural_limited: []
radius: 3
breeding_ticks: 400
growth_ticks: 400
logblock: false
relative_age: false
debug: false
defaults:
age: -1
max: 150
chunk_max: 50
cull: 4
limits:
bat:
cull: 6
blaze:
age: 12000
cull: 0
cave_spider:
age: 12000
cull: 2
chicken: []
cow: []
creeper:
age: 24000
cull: 8
ender_dragon: []
enderman:
age: 18000
cull: 0
endermite:
cull: 6
evoker: []
ghast:
cull: 1
giant:
cull: -1
guardian:
age: 12000
max: 100
chunk_max: 25
cull: 4
horse:
age: -1
iron_golem: []
llama:
cull: 8
magma_cube:
age: 24000
mushroom_cow: []
ocelot:
cull: 8
pig: []
pig_zombie:
age: 18000
rabbit:
cull: 8
sheep_white:
cull: 2
sheep_orange:
cull: 2
sheep_magenta:
cull: 2
sheep_light_blue:
cull: 2
sheep_yellow:
cull: 2
sheep_lime:
cull: 2
sheep_pink:
cull: 2
sheep_silver:
cull: 2
sheep_cyan:
cull: 2
sheep_purple:
cull: 2
sheep_blue:
cull: 2
sheep_brown:
cull: 2
sheep_green:
cull: 2
sheep_red:
cull: 2
sheep_black:
cull: 2
shulker: []
silverfish:
cull: 6
skeleton:
age: 24000
slime:
age: 24000
snowman:
cull: -1
spider:
age: 12000
cull: 2
squid:
age: 12000
max: 80
chunk_max: 5
cull: 1
vex: []
villager:
chunk_max: 20
age: -1
vindicator: []
witch:
age: 24000
cull: 2
wither:
cull: -1
wolf:
cull: 8
zombie:
age: 24000
+26 -13
View File
@@ -1,20 +1,33 @@
name: MobLimiter
version: ${project.version}
description: ${project.description}
author: Travis Watkins
website: ${project.url}
description: Control the mob population
authors: ["Travis Watkins", "redwall_hp"]
softdepend: ["LogBlock"]
main: nu.nerd.moblimiter.MobLimiter
commands:
moblimiter:
description: Gives information about MobLimiter
permissions:
moblimit.cull:
description: Access to cull command
default: op
moblimiter.check:
description: Check if a mob is special
default: op
moblimiter.*:
description: Access all commands
default: op
children:
moblimiter.reload: true
moblimiter.count: true
moblimiter.limits: true
moblimiter.check: true
moblimiter.reload:
description: Access to reload command
default: op
moblimiter.count:
description: Access to count command
default: op
moblimiter.limits:
description: Access to limits command
default: op
moblimiter.check:
description: Access to check command
default: op
commands:
moblimiter:
description: Gives information about MobLimiter
+38 -74
View File
@@ -1,61 +1,49 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>nu.nerd</groupId>
<artifactId>MobLimiter</artifactId>
<version>1.1.8</version>
<packaging>jar</packaging>
<name>MobLimiter</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<version>2.0.1</version>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.8-R0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>bukkit</artifactId>
<version>1.8-R0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<distributionManagement>
<repositories>
<repository>
<id>repobo-rel</id>
<name>repo.bukkit.org Releases</name>
<url>http://repo.bukkit.org/content/repositories/releases/</url>
<id>spigot-repo</id>
<url>https://hub.spigotmc.org/nexus/content/groups/public/</url>
</repository>
<snapshotRepository>
<id>repobo-snap</id>
<name>repo.bukkit.org Snapshots</name>
<url>http://repo.bukkit.org/content/repositories/snapshots/</url>
</snapshotRepository>
</distributionManagement>
<repositories>
<repository>
<id>spigot-repo</id>
<url>https://hub.spigotmc.org/nexus/content/groups/public/</url>
<id>md_5-releases</id>
<url>http://repo.md-5.net/content/repositories/releases/</url>
</repository>
</repositories>
</repositories>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.9-R0.1-SNAPSHOT</version>
</dependency>
<dependency>
<!-- This is required because LogBlock is a pain -->
<groupId>de.diddiz</groupId>
<artifactId>questioner</artifactId>
<version>${project.version}</version>
<scope>system</scope>
<systemPath>${project.basedir}/vendor/LogBlockQuestioner.jar</systemPath>
</dependency>
<dependency>
<groupId>de.diddiz</groupId>
<artifactId>logblock</artifactId>
<version>1.94</version>
</dependency>
</dependencies>
<pluginRepositories>
<pluginRepository>
<id>bukkit-plugins</id>
<url>http://repo.bukkit.org/content/groups/public</url>
</pluginRepository>
</pluginRepositories>
<description>Limit mobs per chunk</description>
<url>https://github.com/nerdnu/MobLimiter</url>
<scm>
<connection>scm:git:https://github.com/nerdnu/MobLimiter.git</connection>
<developerConnection>scm:git:https://github.com/nerdnu/MobLimiter.git</developerConnection>
<url>https://github.com/nerdnu/MobLimiter</url>
</scm>
<build>
<sourceDirectory>${basedir}/src</sourceDirectory>
<resources>
@@ -66,34 +54,10 @@
<includes>
<include>plugin.yml</include>
<include>config.yml</include>
<include>LICENSE</include>
</includes>
</resource>
</resources>
<defaultGoal>clean install</defaultGoal>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.1</version>
<configuration>
<archive>
<addMavenDescriptor>false</addMavenDescriptor>
</archive>
<finalName>MobLimiter-v${project.version}</finalName>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.0.2</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
@@ -0,0 +1,77 @@
package nu.nerd.moblimiter;
import org.bukkit.entity.Animals;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Tameable;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
/**
* Handle faster breeding
*/
public class BreedingListener implements Listener {
private MobLimiter plugin;
public BreedingListener() {
plugin = MobLimiter.instance;
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
/**
* Apply the breeding changes when a new animal is bred
*/
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
public void onCreatureSpawnEvent(final CreatureSpawnEvent event) {
CreatureSpawnEvent.SpawnReason reason = event.getSpawnReason();
if ((reason == SpawnReason.BREEDING || reason == SpawnReason.EGG || reason == SpawnReason.DISPENSE_EGG)) {
if (isFarmAnimal(event.getEntity())) {
applyBreedingChanges((Animals) event.getEntity());
for (Entity en : event.getEntity().getNearbyEntities(4, 4, 4)) {
if (isFarmAnimal(en)) {
applyBreedingChanges((Animals) en);
}
}
}
}
}
/**
* Controls the rate farm animals grow up and the length of their breeding cooldown.
* growthTicks: How many ticks until the animal becomes an adult.
* breedingTicks: How many ticks until the animal can breed again.
* A value of zero makes the condition instant, and -1 disables tampering with vanilla behavior.
* This functionality was called "agecapbaby" and "agecapbaby" respectively in MobLimiter 1.x.
* @param animal the animal to apply the changes to
*/
private void applyBreedingChanges(Animals animal) {
if (animal.getAgeLock()) return;
int growthTicks = plugin.getConfiguration().getGrowthTicks();
int breedingTicks = plugin.getConfiguration().getBreedingTicks();
if (growthTicks > -1 && !animal.isAdult()) {
animal.setAge(Math.max(animal.getAge(), -growthTicks));
} else if (breedingTicks > -1 && animal.isAdult()) {
animal.setAge(Math.min(animal.getAge(), breedingTicks));
}
}
/**
* Whether the entity in question is a farm animal or not
* @param entity the entity to check
* @return true if this is a farm animal
*/
private boolean isFarmAnimal(Entity entity) {
return (entity instanceof Animals) && !(entity instanceof Tameable);
}
}
+292
View File
@@ -0,0 +1,292 @@
package nu.nerd.moblimiter;
import nu.nerd.moblimiter.configuration.ConfiguredDefaults;
import nu.nerd.moblimiter.configuration.ConfiguredMob;
import org.bukkit.ChatColor;
import org.bukkit.Chunk;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.*;
import java.util.*;
public class CommandHandler implements CommandExecutor {
private MobLimiter plugin;
public CommandHandler() {
plugin = MobLimiter.instance;
plugin.getCommand("moblimiter").setExecutor(this);
}
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
if (args.length == 1 && args[0].equalsIgnoreCase("reload")) {
reloadConfig(sender);
}
else if (args.length > 0 && args[0].equalsIgnoreCase("count")) {
countCommand(sender);
}
else if (args.length > 0 && args[0].equalsIgnoreCase("limits")) {
limitsCommand(sender, args);
}
else if (args.length > 0 && args[0].equalsIgnoreCase("check")) {
checkCommand(sender);
}
else if (args.length > 0 && args[0].equalsIgnoreCase("help")) {
helpText(sender, cmd, args);
}
else {
commandsList(sender);
}
return true;
}
/**
* Command to reload the configuration
*/
private void reloadConfig(CommandSender sender) {
if (!sender.hasPermission("moblimiter.reload")) return;
plugin.getConfiguration().load();
sender.sendMessage(ChatColor.GOLD + "MobLimiter config reloaded");
}
/**
* When a player runs /moblimiter with no arguments, list available commands
* and guide newbies to /moblimiter help
* @param sender
*/
private void commandsList(CommandSender sender) {
StringBuilder msg = new StringBuilder();
msg.append("&6&lMobLimiter: Available Commands\n");
msg.append("&6/moblimiter help&r - What is MobLimiter and how does it work?\n");
if (sender.hasPermission("moblimiter.limits")) {
msg.append("&6/moblimiter limits&r - List the limits for each mob type\n");
}
if (sender.hasPermission("moblimiter.count")) {
msg.append("&6/moblimiter count&r - Count entities in the current chunk and radius\n");
}
if (sender.hasPermission("moblimiter.check")) {
msg.append("&6/moblimiter check&r - Inspect limiting details for the mob you're looking at\n");
}
if (sender.hasPermission("moblimiter.reload")) {
msg.append("&6/moblimiter reload&r - Reload configuration from disk\n");
}
sender.sendMessage(ChatColor.translateAlternateColorCodes('&', msg.toString()));
}
/**
* When a player runs /moblimiter help, print help text
*/
private void helpText(CommandSender sender, Command cmd, String[] args) {
int num = 1;
if (args.length > 1) {
try {
num = Integer.parseInt(args[1]);
} catch (NumberFormatException ex) {
num = 1;
}
}
if (num == 1) {
StringBuilder msg = new StringBuilder();
msg.append(ChatColor.GOLD);
msg.append("This server runs a plugin called " + ChatColor.YELLOW + "MobLimiter"
+ ChatColor.GOLD + " in order to manage the "
+ "amount of mobs on the server, as too many mobs creates lag which affects "
+ "everyone. MobLimiter uses several methods to manage the mob population, depending on how" +
" it is configured:\n");
msg.append(ChatColor.YELLOW);
msg.append("1. Chunk unload culling\n");
msg.append(ChatColor.GOLD);
msg.append("Mobs are culled down to a defined maximum whenever a chunk unloads or the server restarts.\n");
msg.append(ChatColor.YELLOW);
msg.append("2. Real-time spawn limiting\n");
msg.append(ChatColor.GOLD);
msg.append("Mobs are tracked in real-time and new spawns are prevented in an area if there are more than a " +
"certain amount of that mob type there already.\n");
sender.sendMessage(msg.toString());
}
if (num == 2) {
StringBuilder msg = new StringBuilder();
msg.append(ChatColor.YELLOW);
msg.append("3. Age limiting\n");
msg.append(ChatColor.GOLD);
msg.append("Mobs automatically die when they reach a certain age, dropping their items when they do. " +
"e.g. a skeleton may live for 15 minutes before dying.\n");
msg.append("In all cases, \"special\" mobs are always protected and will not be removed by the plugin. This " +
"includes mobs with custom names, tamed animals, and mobs wearing armor.\n");
msg.append("To make up for the culling of passive farm animals, the plugin shortens the growth rate of " +
"baby animals and the waiting time in between breeding events, to approximately ");
msg.append(ChatColor.GRAY);
msg.append(plugin.getConfiguration().getGrowthTicks() / 20);
msg.append(ChatColor.GOLD);
msg.append(" and ");
msg.append(ChatColor.GRAY);
msg.append(plugin.getConfiguration().getBreedingTicks() / 20);
msg.append(ChatColor.GOLD);
msg.append(" seconds respectively.");
sender.sendMessage(msg.toString());
}
sender.sendMessage(String.format("[Page %d/2 - /moblimiter help #]", num));
}
/**
* Command to count entities in the chunk and radius
*/
private void countCommand(CommandSender sender) {
if (!sender.hasPermission("moblimiter.count")) return;
if (!(sender instanceof Player)) {
sender.sendMessage("Console can't do that.");
return;
}
Player player = (Player) sender;
Chunk playerChunk = player.getLocation().getChunk();
int chunkRadius = plugin.getConfiguration().getRadius();
// Count mobs in the chunk
HashMap<String, Integer> chunkCounts = EntityHelper.summarizeMobsInChunk(playerChunk);
// Count mobs in the radius
HashMap<String, Integer> radCounts = EntityHelper.summarizeMobsInRadius(playerChunk, chunkRadius);
// Print results
StringBuilder sb = new StringBuilder(ChatColor.GOLD + "Entities in chunk: ");
for (Map.Entry<String, Integer> entry : chunkCounts.entrySet()) {
sb.append(ChatColor.WHITE);
sb.append(entry.getKey().toLowerCase());
sb.append(": ");
sb.append(ChatColor.GRAY);
sb.append(entry.getValue());
sb.append(" ");
}
sender.sendMessage(sb.toString());
sb = new StringBuilder(ChatColor.GOLD + "Entities in radius: ");
for (Map.Entry<String, Integer> entry : radCounts.entrySet()) {
sb.append(ChatColor.WHITE);
sb.append(entry.getKey().toLowerCase());
sb.append(": ");
sb.append(ChatColor.GRAY);
sb.append(entry.getValue());
sb.append(" ");
}
sender.sendMessage(sb.toString());
}
/**
* Command to display the configured limits
*/
private void limitsCommand(CommandSender sender, String[] args) {
if (!sender.hasPermission("moblimiter.limits")) return;
List<String> lines = new ArrayList<String>();
ConfiguredDefaults d = plugin.getConfiguration().getDefaults();
String values = String.format("Age: %d Max: %d Chunk: %d Cull: %d", d.getAge(), d.getMax(), d.getChunkMax(), d.getCull());
lines.add(String.format("%s%s %s[%s]", ChatColor.GOLD, "DEFAULT", ChatColor.YELLOW, values));
TreeMap<String, ConfiguredMob> limits = new TreeMap<String, ConfiguredMob>(plugin.getConfiguration().getAllLimits());
for (ConfiguredMob l : limits.values()) {
values = String.format("Age: %d Max: %d Chunk: %d Cull: %d", l.getAge(), l.getMax(), l.getChunkMax(), l.getCull());
lines.add(String.format("%s%s %s[%s]", ChatColor.GOLD, l.getKey(), ChatColor.YELLOW, values));
}
int page = 1;
if (args.length > 1) {
try {
page = Integer.parseInt(args[1]);
} catch (NumberFormatException ex) {
page = 1;
}
}
if (lines.size() > 0) {
int offset = (page - 1) * 10;
int pages = ((lines.size()-1) / 10) + 1;
for (int i = offset; i <= offset + 9; i++) {
if (i >= lines.size()) break;
sender.sendMessage(lines.get(i));
}
sender.sendMessage(String.format("%s[Page %d/%d]", ChatColor.GRAY, page, pages));
} else {
sender.sendMessage("There are no limits configured. All mob types will fall back to the default block.");
}
}
/**
* Command to inspect details about the mob the player is looking at
*/
private void checkCommand(CommandSender sender) {
if (!sender.hasPermission("moblimiter.check")) return;
if (!(sender instanceof Player)) {
sender.sendMessage("Console can't do that.");
return;
}
Player player = (Player) sender;
LivingEntity entity = EntityHelper.getMobInLineOfSight(player);
if (entity == null || entity.isDead() || !EntityHelper.isLimitableMob(entity)) {
sender.sendMessage("No mob in sight");
return;
}
ConfiguredMob limits = plugin.getConfiguration().getLimits(entity);
Chunk chunk = entity.getLocation().getChunk();
int chunkRadius = plugin.getConfiguration().getRadius();
HashMap<String, Integer> chunkCounts = EntityHelper.summarizeMobsInChunk(chunk);
HashMap<String, Integer> radCounts = EntityHelper.summarizeMobsInRadius(chunk, chunkRadius);
int nearby = radCounts.get(limits.getKey());
int inChunk = chunkCounts.get(limits.getKey());
boolean isSpecial = EntityHelper.isSpecialMob(entity);
boolean isCullable = !entity.isDead() && (entity instanceof Animals || entity instanceof Monster);
boolean canSpawnChunk = (limits.getChunkMax() > inChunk || limits.getChunkMax() < 0);
boolean canSpawnNearby = (limits.getMax() > nearby || limits.getMax() < 0);
boolean moreCanSpawn = canSpawnChunk && canSpawnNearby;
sender.sendMessage(ChatColor.GOLD + "---");
StringBuilder title = new StringBuilder(ChatColor.GOLD + limits.getKey());
title.append(ChatColor.GRAY);
title.append(String.format(" (%d/%d nearby, %d/%d in chunk)", nearby, limits.getMax(), inChunk, limits.getChunkMax()));
sender.sendMessage(title.toString());
String ageStr;
if (limits.getAge() > -1) {
if (!EntityHelper.isBreedingPair(entity)) {
ageStr = String.format("%d/%d", plugin.getAgeLimiter().adjustedAge(entity), limits.getAge());
} else {
ageStr = "Breeding Pair Exempted";
}
} else {
ageStr = "Not Limited";
}
String chunkCullStr = (limits.getCull() > -1 && isCullable) ? String.format("%d", limits.getCull()) : "Unlimited";
sender.sendMessage(String.format("Age: %s%s", ChatColor.GRAY, ageStr));
sender.sendMessage(String.format("Is Special: %s%b", ChatColor.GRAY, isSpecial));
sender.sendMessage(String.format("Chunk Unload Cap: %s%s", ChatColor.GRAY, chunkCullStr));
sender.sendMessage(String.format("More Can Spawn: %s%b", ChatColor.GRAY, moreCanSpawn));
sender.sendMessage(ChatColor.GOLD + "---");
}
}
+196
View File
@@ -0,0 +1,196 @@
package nu.nerd.moblimiter;
import nu.nerd.moblimiter.configuration.Configuration;
import nu.nerd.moblimiter.configuration.ConfiguredMob;
import org.bukkit.*;
import org.bukkit.block.Block;
import org.bukkit.entity.*;
import org.bukkit.inventory.EntityEquipment;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.Vector;
import java.util.*;
/**
* Mob-related methods that may be shared across classes
*/
public class EntityHelper {
/**
* Check if this is a mob that should ever be limited.
* Merely checking Animals or Monster subclassing is not sufficient due to Bukkit inconsistency.
* e.g. Armor Stands are LivingEntities, apparently.
* So to check whether something is a limitable mob, we check if it's a LivingEntity first and then
* apply a blacklist of edge cases such as Armor Stands.
* @param entity The entity to check
* @return true if the entity is a limitable mob
*/
public static boolean isLimitableMob(Entity entity) {
List<EntityType> blacklist = new ArrayList<EntityType>();
blacklist.add(EntityType.ARMOR_STAND);
blacklist.add(EntityType.PLAYER);
return entity instanceof LivingEntity && !blacklist.contains(entity.getType());
}
/**
* Check if this is a "special mob" that shouldn't be removed in any circumstance
* @param entity entity to check
* @return true if this is a special mob
*/
public static boolean isSpecialMob(LivingEntity entity) {
// Keep mobs with custom names
if (entity.getCustomName() != null) {
return true;
}
// Don't remove tamed mobs
if (entity instanceof Tameable) {
Tameable tameable = (Tameable) entity;
if (tameable.isTamed()) {
return true;
}
}
// Save the sponge!
if (entity.getType() == EntityType.GUARDIAN && ((Guardian)entity).isElder()) {
return true;
}
// Don't remove mobs that are holding something, which they may have picked up
EntityEquipment equipment = entity.getEquipment();
for (ItemStack armor : equipment.getArmorContents()) {
// Unarmored mobs, even animals, spawn with 1xAIR as armor.
if (armor != null && armor.getType() != Material.AIR) {
return true;
}
}
return false;
}
/**
* If this is an Animal and there are two or less in the chunk, don't remove them.
* This protects breeding pairs for farm animals.
* @param entity the entity to check
* @return false if the entity doesn't match the criteria
*/
public static boolean isBreedingPair(Entity entity) {
if (!(entity instanceof Animals)) return false;
if (entity instanceof Tameable) return false;
int count = 0;
ConfiguredMob mob = getConfiguration().getLimits(entity);
for (Entity e : entity.getLocation().getChunk().getEntities()) {
ConfiguredMob m = getConfiguration().getLimits(e);
if (m.getKey().equals(mob.getKey()) && !e.isDead()) {
count++;
}
}
return count < 3;
}
/**
* Return details about an entity for debugging
* @param entity the entity
* @return String representation of the mob name and location
*/
public static String getMobDescription(Entity entity) {
String type = entity.getType().toString();
String world = entity.getLocation().getWorld().getName();
Location loc = entity.getLocation();
return String.format("%s at (%s,%d,%d,%d)", type, world, loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
}
/**
* Return the mob a player is looking at
* @param player The player to check
* @return null or LivingEntity
*/
public static LivingEntity getMobInLineOfSight(Player player) {
List<Entity> entities = player.getNearbyEntities(5, 1, 5);
Iterator<Entity> iterator = entities.iterator();
while (iterator.hasNext()) {
Entity ent = iterator.next();
if (!(ent instanceof LivingEntity)) iterator.remove();
}
Set<Material> nullSet = null;
for (Block block : player.getLineOfSight(nullSet, 6)) {
if (block.getType() != Material.AIR) break; //view is obstructed
for (Entity ent : entities) {
Vector b = block.getLocation().toVector();
Vector head = ent.getLocation().toVector().add(new Vector(0, 1, 0));
Vector foot = ent.getLocation().toVector();
if (head.isInSphere(b, 1.25) || foot.isInSphere(b, 1.25)) {
return (LivingEntity) ent;
}
}
}
return null;
}
/**
* Build a HashMap of entity types and their counts in a chunk
* @param chunk The chunk to check
* @return A HashMap summarizing the entity breakdown of a chunk
*/
public static HashMap<String, Integer> summarizeMobsInChunk(Chunk chunk) {
HashMap<String, Integer> chunkCounts = new HashMap<String, Integer>();
for (Entity e : chunk.getEntities()) {
if (e.isDead() || !isLimitableMob(e)) continue;
ConfiguredMob mob = getConfiguration().getLimits(e);
if (chunkCounts.containsKey(mob.getKey())) {
int count = chunkCounts.get(mob.getKey()) + 1;
chunkCounts.put(mob.getKey(), count);
} else {
chunkCounts.put(mob.getKey(), 1);
}
}
return chunkCounts;
}
/**
* Build a HashMap of entity types and their counts in a chunk radius
* @param start The center of the chunk radius
* @param radius The radius of chunks to check
* @return A HashMap summarizing the entity breakdown within a chunk radius
*/
public static HashMap<String, Integer> summarizeMobsInRadius(Chunk start, int radius) {
HashMap<String, Integer> radCounts = new HashMap<String, Integer>();
World world = start.getWorld();
for (int x = start.getX() - radius; x <= start.getX() + radius; x++) {
for (int z = start.getZ() - radius; z <= start.getZ() + radius; z++) {
Chunk c = world.getChunkAt(x, z);
for (Entity e : c.getEntities()) {
if (e.isDead() || !isLimitableMob(e)) continue;
ConfiguredMob mob = getConfiguration().getLimits(e);
if (radCounts.containsKey(mob.getKey())) {
int count = radCounts.get(mob.getKey()) + 1;
radCounts.put(mob.getKey(), count);
} else {
radCounts.put(mob.getKey(), 1);
}
}
}
}
return radCounts;
}
/**
* Convenience method to get the plugin configuration
*/
public static Configuration getConfiguration() {
return MobLimiter.instance.getConfiguration();
}
}
+29 -506
View File
@@ -1,530 +1,53 @@
package nu.nerd.moblimiter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import org.bukkit.ChatColor;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.*;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
import org.bukkit.event.world.ChunkUnloadEvent;
import org.bukkit.inventory.EntityEquipment;
import org.bukkit.inventory.ItemStack;
import nu.nerd.moblimiter.configuration.Configuration;
import nu.nerd.moblimiter.limiters.AgeLimiter;
import nu.nerd.moblimiter.limiters.ChunkUnloadLimiter;
import nu.nerd.moblimiter.limiters.SpawnLimiter;
import nu.nerd.moblimiter.logblock.LogBlockWrapper;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.util.Vector;
public class MobLimiter extends JavaPlugin implements Listener {
/**
* Map from lower case EntityType.name() to max number of entities of that
* type. Note that EntityType.name() is different from EntityType.getName().
* The former is MUSHROOM_COW while the latter is MushroomCow.
*/
protected Map<String, Integer> limits = new HashMap<String, Integer>();
protected int ageCapBaby = -1;
protected int ageCapBreed = -1;
public class MobLimiter extends JavaPlugin {
/**
* If true, per-chunk mob caps are enforced for non-farm-animal mobs when
* they spawn, in addition to when the chunk is unloaded.
*/
protected boolean limitNaturalSpawn;
/**
* If true, the mob cap applies even when the mob was spawned from a
* mob-spawner.
*/
protected boolean limitSpawnerSpawn;
public static MobLimiter instance;
private Configuration configuration;
private ChunkUnloadLimiter chunkUnloadLimiter;
private AgeLimiter ageLimiter;
private LogBlockWrapper logBlock;
/**
* If true, cancelled spawns are shown in the server log.
*/
protected boolean debugLimitSpawn;
/**
* If true, limits on numbers of each mob type are logged as the are parsed
* from the configuration file.
*/
protected boolean debugLimits;
/**
* If true, mobs removed during chunk unloading are logged.
*/
protected boolean debugChunkUnload;
/**
* If true, do debug traces of the age cap code.
*/
protected boolean debugAgeCap;
/**
* If true, do debug traces of preservation of special mobs on chunk unload.
*/
protected boolean debugSpecial;
/**
* The set of Entity types whose caps are enforced when spawning naturally.
*
* Note that farm animals are explicitly excluded from that cap. Players can
* breed them over the limit.
*/
protected HashMap<EntityType, Integer> naturalLimitedEntities = new HashMap<EntityType, Integer>();
/**
* The set of Entity types whose caps are enforced when spawned by a mobspawner.
*
* Note that farm animals are explicitly excluded from that cap. Players can
* breed them over the limit.
*/
protected HashMap<EntityType, Integer> spawnerLimitedEntities = new HashMap<EntityType, Integer>();
@Override
public void onEnable() {
saveDefaultConfig();
limitNaturalSpawn = getConfig().getBoolean("settings.limit_natural_spawn");
limitSpawnerSpawn = getConfig().getBoolean("settings.limit_spawner_spawn");
debugLimitSpawn = getConfig().getBoolean("settings.debug.limit_spawn");
debugLimits = getConfig().getBoolean("settings.debug.limits");
debugChunkUnload = getConfig().getBoolean("settings.debug.chunk_unload");
debugAgeCap = getConfig().getBoolean("settings.debug.age_cap");
debugSpecial = getConfig().getBoolean("settings.debug.special");
for (Map.Entry<String, Object> entry : this.getConfig().getValues(false).entrySet()) {
if (entry.getKey() != null && entry.getValue() != null && entry.getValue() instanceof Integer) {
String name = entry.getKey().toLowerCase();
int value = (Integer) entry.getValue();
if (name.equals("agecapbaby")) {
ageCapBaby = value;
} else if (name.equals("agecapbreed")) {
ageCapBreed = value;
} else {
// A negative limit allows unlimited numbers of that mob.
if (value >= 0) {
limits.put(name, value);
if (debugLimits) {
getLogger().info(name + " limit: " + value);
}
}
}
}
}
if (getConfig().isConfigurationSection("settings.natural_limited")) {
for (String entityTypeName : getConfig().getConfigurationSection("settings.natural_limited").getKeys(false)) {
EntityType entityType = EntityType.valueOf(entityTypeName.toUpperCase());
if (entityType == null) {
getLogger().severe("Invalid entity type for natural spawn limiting: " + entityTypeName);
} else {
int count = getConfig().getInt(String.format("settings.natural_limited.%s", entityTypeName));
naturalLimitedEntities.put(entityType, count);
}
}
}
if (getConfig().isConfigurationSection("settings.spawner_limited")) {
for (String entityTypeName : getConfig().getConfigurationSection("settings.spawner_limited").getKeys(false)) {
EntityType entityType = EntityType.valueOf(entityTypeName.toUpperCase());
if (entityType == null) {
getLogger().severe("Invalid entity type for natural mobspawner limiting: " + entityTypeName);
} else {
int count = getConfig().getInt(String.format("settings.spawner_limited.%s", entityTypeName));
spawnerLimitedEntities.put(entityType, count);
}
}
}
this.getServer().getPluginManager().registerEvents(this, this);
MobLimiter.instance = this;
configuration = new Configuration();
chunkUnloadLimiter = new ChunkUnloadLimiter();
ageLimiter = new AgeLimiter();
logBlock = new LogBlockWrapper();
new SpawnLimiter();
new BreedingListener();
new CommandHandler();
}
@Override
public void onDisable() {
removeAllMobs();
chunkUnloadLimiter.removeAllMobs();
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onChunkUnload(final ChunkUnloadEvent e) {
removeMobs(e.getChunk());
public Configuration getConfiguration() {
return configuration;
}
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
public void onCreatureSpawnEvent(final CreatureSpawnEvent event) {
SpawnReason reason = event.getSpawnReason();
if ((reason == SpawnReason.BREEDING || reason == SpawnReason.EGG || reason == SpawnReason.DISPENSE_EGG) && isFarmAnimal(event.getEntity())) {
applyAgeCap((Animals) event.getEntity());
for (Entity en : event.getEntity().getNearbyEntities(4, 4, 4)) {
if (isFarmAnimal(en)) {
applyAgeCap((Animals) en);
}
}
}
// Villager breeding has a spawn reason of SpawnReason.DEFAULT
// or SpawnReason.BREEDING depending on CraftBukkit version.
boolean doLimitNaturalSpawn = limitNaturalSpawn &&
(event.getSpawnReason() == SpawnReason.BREEDING ||
event.getSpawnReason() == SpawnReason.NATURAL ||
event.getSpawnReason() == SpawnReason.DEFAULT);
boolean doLimitSpawnerSpawn = limitSpawnerSpawn && event.getSpawnReason() == SpawnReason.SPAWNER;
int cap;
if (doLimitNaturalSpawn && naturalLimitedEntities.containsKey(event.getEntityType())) {
cap = naturalLimitedEntities.get(event.getEntityType());
}
else if (doLimitSpawnerSpawn && spawnerLimitedEntities.containsKey(event.getEntityType())) {
cap = spawnerLimitedEntities.get(event.getEntityType());
}
else {
return;
}
int count = 0;
for (Entity otherEntity : event.getLocation().getChunk().getEntities()) {
if (otherEntity.getType() == event.getEntityType()) {
++count;
if (count >= cap) {
break;
}
}
}
if (count >= cap) {
if (debugLimitSpawn) {
getLogger().info("Cancel spawn of " + getMobDescription(event.getEntity()) +
" (reason = " + event.getSpawnReason().name().toLowerCase() + ", cap = " + cap + ")");
}
// See: https://github.com/NerdNu/NerdBugs/issues/180
// Trying remove() instead since we already use that.
event.getEntity().remove();
}
public AgeLimiter getAgeLimiter() {
return ageLimiter;
}
public boolean isFarmAnimal(Entity en) {
return (en instanceof Animals) && !(en instanceof Tameable);
public LogBlockWrapper getLogBlock() {
return logBlock;
}
public void applyAgeCap(Animals en) {
if (en.getAgeLock() == true) {
if (debugAgeCap) {
getLogger().info("Age locked " + getMobDescription(en) + " with age " + en.getAge() + " was not age capped");
}
return;
}
if (ageCapBaby >= 0 && !en.isAdult()) {
en.setAge(Math.max(en.getAge(), -ageCapBaby));
} else if (ageCapBreed >= 0 && en.isAdult()) {
en.setAge(Math.min(en.getAge(), ageCapBreed));
}
if (debugAgeCap) {
getLogger().info("Age of " + getMobDescription(en) + " capped to " + en.getAge());
}
}
/**
* Return a String describing an entity's type (including color for sheep)
* and location.
*
* @param entity the entity.
* @return the description, including location.
*/
public String getMobDescription(Entity entity) {
String key = getCapKey(entity);
Location loc = entity.getLocation();
return String.format("%s at (%s,%d,%d,%d)", key,
loc.getWorld().getName(), loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
}
/**
* Return the key string used to access the cap on a given mob type.
*
* For sheep, the color is considered to be part of the type. Each sheep
* color has its own distinct cap.
*
* @param entity the entity.
* @return the key used to access the limits map.
*/
public String getCapKey(Entity entity) {
StringBuilder key = new StringBuilder(entity.getType().name());
if (entity instanceof Sheep) {
key.append(((Sheep) entity).getColor().name());
}
return key.toString().toLowerCase();
}
/**
* Return true if the specified Entity has a cap on numbers.
*
* @param entity the entity.
* @return true if the specified Entity has a cap on numbers.
*/
public boolean hasCap(Entity entity) {
return getCap(getCapKey(entity)) >= 0;
}
/**
* Return the per chunk limit on entities with the specified cap key, or -1
* if uncapped.
*
* @param key the mob type as defined by getCapKey().
* @return the per chunk cap for entities with that key, or -1.
*/
public int getCap(String key) {
Integer cap = limits.get(key);
return (cap != null) ? cap : -1;
}
/**
* Return true if the specified LivingEntity is special, meaning it has been
* named, tamed, or is wearing items in its armor slots.
*
* Such "special" mobs are exempted from being removed on chunk unload.
* However, for hostiles, this is not a guarantee as the server can and will
* despawn them without any event from Bukkit.
*
* @return true if the mob is special and should be exempt from limits.
*/
public boolean isSpecialMob(LivingEntity living) {
if (living.getCustomName() != null) {
return true;
}
if (living instanceof Tameable) {
Tameable tameable = (Tameable) living;
if (tameable.isTamed()) {
return true;
}
}
if (living.getType() == EntityType.GUARDIAN && ((Guardian)living).isElder()) {
return true; //save the sponge!
}
EntityEquipment equipment = living.getEquipment();
for (ItemStack armor : equipment.getArmorContents()) {
// Unarmored mobs, even animals, spawn with 1xAIR as armor.
if (armor != null && armor.getType() != Material.AIR) {
return true;
}
}
return false;
}
public void removeMobs(Chunk chunk) {
Map<String, Integer> count = new HashMap<String, Integer>();
for (Entity entity : chunk.getEntities()) {
if (!entity.isDead() && (entity instanceof Animals || entity instanceof Monster)) {
// Exempt "special mobs" from removal.
// Animals and Monsters are both LivingEntitys so this cast
// succeeds.
boolean special = isSpecialMob((LivingEntity) entity);
String key = getCapKey(entity);
if (special) {
if (debugSpecial) {
getLogger().info("Special mob exempted from removal: " + getMobDescription(entity));
}
} else {
Integer oldCount = count.get(key);
int mobCount = (oldCount == null) ? 1 : oldCount + 1;
count.put(key, mobCount);
int cap = getCap(key);
if (cap >= 0 && mobCount > cap) {
entity.remove();
if (debugChunkUnload) {
getLogger().info("Chunk unload removes " + getMobDescription(entity));
}
}
}
}
}
}
private void removeAllMobs() {
for (Chunk c : getServer().getWorlds().get(0).getLoadedChunks()) {
removeMobs(c);
}
}
/**
*
* @param player
* @return
*/
public static Entity getTarget(final Player player) {
assert player != null;
Entity target = null;
double targetDistanceSquared = 0;
final double radiusSquared = 1;
final Vector l = player.getEyeLocation().toVector(), n = player.getLocation().getDirection().normalize();
final double cos45 = Math.cos(Math.PI / 4);
for (final LivingEntity other : player.getWorld().getEntitiesByClass(LivingEntity.class)) {
if (other == player)
continue;
if (target == null || targetDistanceSquared > other.getLocation().distanceSquared(player.getLocation())) {
final Vector t = other.getLocation().add(0, 1, 0).toVector().subtract(l);
if (n.clone().crossProduct(t).lengthSquared() < radiusSquared && t.normalize().dot(n) >= cos45) {
target = other;
targetDistanceSquared = target.getLocation().distanceSquared(player.getLocation());
}
}
}
return target;
}
/**
* Informative /moblimiter command to describe the purpose of MobLimiter to players
* in game and distribute information about the current configuration.
*/
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
if (cmd.getName().equalsIgnoreCase("moblimiter")) {
if (args.length == 1 && args[0].equalsIgnoreCase("cull") && sender.hasPermission("moblimiter.cull")) {
sender.sendMessage("[MobLimiter] Culling mobs in all loaded chunks");
removeAllMobs();
return true;
}
else if (args.length == 1 && args[0].equalsIgnoreCase("check") && sender.hasPermission("moblimiter.check")) {
if (sender instanceof Player) {
LivingEntity le = (LivingEntity) getTarget((Player) sender);
boolean animalOrMonster = (le instanceof Animals) || (le instanceof Monster);
if (le == null) {
sender.sendMessage("[MobLimiter] No mob in sight");
} else {
if (!isSpecialMob(le) && animalOrMonster) {
sender.sendMessage("[MobLimiter] Mob will be culled");
} else {
sender.sendMessage("[MobLimiter] Mob will not be culled");
}
}
return true;
}
}
StringBuilder message = new StringBuilder();
message.append(ChatColor.GOLD);
message.append("This server runs a plugin called " + ChatColor.YELLOW + "MobLimiter"
+ ChatColor.GOLD + " in order to manage the "
+ "amount of mobs on the server, as too many mobs creates lag which affects "
+ "everyone. Mobs are culled down to the following values per chunk on chunk "
+ "unload, (-1 indicates no culling): ");
// if sheep are all configured to the same amount, just say one value
// else put an asterisk and don't report the separate colors anyway,
// since there's so many
if ((getConfig().getInt("sheepwhite") == getConfig().getInt("sheeporange")) &&
(getConfig().getInt("sheeporange") == getConfig().getInt("sheepmagenta")) &&
(getConfig().getInt("sheepmagenta") == getConfig().getInt("sheeplight_blue")) &&
(getConfig().getInt("sheeplight_blue") == getConfig().getInt("sheepyellow")) &&
(getConfig().getInt("sheepyellow") == getConfig().getInt("sheeplime")) &&
(getConfig().getInt("sheeplime") == getConfig().getInt("sheeppink")) &&
(getConfig().getInt("sheeppink") == getConfig().getInt("sheepgray")) &&
(getConfig().getInt("sheepgray") == getConfig().getInt("sheepsilver")) &&
(getConfig().getInt("sheepsilver") == getConfig().getInt("sheepcyan")) &&
(getConfig().getInt("sheepcyan") == getConfig().getInt("sheeppurple")) &&
(getConfig().getInt("sheeppurple") == getConfig().getInt("sheepblue")) &&
(getConfig().getInt("sheepblue") == getConfig().getInt("sheepbrown")) &&
(getConfig().getInt("sheepbrown") == getConfig().getInt("sheepgreen")) &&
(getConfig().getInt("sheepgreen") == getConfig().getInt("sheepred")) &&
(getConfig().getInt("sheepred") == getConfig().getInt("sheepblack")))
{
message.append(ChatColor.YELLOW + "sheep: " + ChatColor.GRAY
+ getConfig().getInt("sheepwhite"));
}
else
{
message.append(ChatColor.YELLOW + "sheep*: " + ChatColor.GRAY
+ getConfig().getInt("sheepwhite"));
}
message.append(ChatColor.YELLOW + " chicken: " + ChatColor.GRAY
+ getConfig().getInt("chicken"));
message.append(ChatColor.YELLOW + " cow: " + ChatColor.GRAY
+ getConfig().getInt("cow"));
message.append(ChatColor.YELLOW + " mushroom_cow: " + ChatColor.GRAY
+ getConfig().getInt("mushroom_cow"));
message.append(ChatColor.YELLOW + " ocelot: " + ChatColor.GRAY
+ getConfig().getInt("ocelot"));
message.append(ChatColor.YELLOW + " pig: " + ChatColor.GRAY
+ getConfig().getInt("pig"));
message.append(ChatColor.YELLOW + " wolf: " + ChatColor.GRAY
+ getConfig().getInt("wolf"));
message.append(ChatColor.YELLOW + " rabbit: " + ChatColor.GRAY
+ getConfig().getInt("rabbit"));
message.append(ChatColor.YELLOW + " blaze: " + ChatColor.GRAY
+ getConfig().getInt("blaze"));
message.append(ChatColor.YELLOW + " cave_spider: " + ChatColor.GRAY
+ getConfig().getInt("cave_spider"));
message.append(ChatColor.YELLOW + " creeper: " + ChatColor.GRAY
+ getConfig().getInt("creeper"));
message.append(ChatColor.YELLOW + " enderman: " + ChatColor.GRAY
+ getConfig().getInt("enderman"));
message.append(ChatColor.YELLOW + " ghast: " + ChatColor.GRAY
+ getConfig().getInt("ghast"));
message.append(ChatColor.YELLOW + " guardian: " + ChatColor.GRAY
+ getConfig().getInt("guardian"));
message.append(ChatColor.YELLOW + " silverfish: " + ChatColor.GRAY
+ getConfig().getInt("silverfish"));
message.append(ChatColor.YELLOW + " skeleton: " + ChatColor.GRAY
+ getConfig().getInt("skeleton"));
message.append(ChatColor.YELLOW + " spider: " + ChatColor.GRAY
+ getConfig().getInt("spider"));
message.append(ChatColor.YELLOW + " squid: " + ChatColor.GRAY
+ getConfig().getInt("squid"));
message.append(ChatColor.YELLOW + " villager: " + ChatColor.GRAY
+ getConfig().getInt("villager"));
message.append(ChatColor.YELLOW + " zombie: " + ChatColor.GRAY
+ getConfig().getInt("zombie"));
message.append(ChatColor.GOLD);
message.append(". To make up for the culling of passive farm animals, the plugin shortens "
+ "the growth rate of baby animals and the waiting time in between breeding "
+ "events, to approximately "
+ ChatColor.GRAY + getConfig().getInt("agecapbaby") / 20
+ ChatColor.GOLD + " and "
+ ChatColor.GRAY + getConfig().getInt("agecapbreed") / 20
+ ChatColor.GOLD + " seconds respectively. ");
// explain spawn-limited, if set
if (getConfig().isList("settings.spawn_limited")) {
message.append("Note that mobs of type " + ChatColor.YELLOW
+ getConfig().getStringList("settings.spawn_limited") + ChatColor.GOLD
+ " are configured to be only new-spawn-limited and not culled, once their numbers "
+ "have reached the integer in the previous list no new mobs of that type will spawn "
+ "in that respective chunk. ");
}
// on nerd.nu we use KitchenSink to plump drops, which is because of
// mob limiting, so mention that here
message.append("Additionally, the drops of passive mobs "
+ "are plumped so that more items drop which allows for fewer animals to produce "
+ "more resources. Tamed pet mobs, mobs custom named with a nametag, and mobs "
+ "wearing armor are not culled. If you have any questions please feel free "
+ "to contact a server administrator.");
sender.sendMessage(message.toString());
}
return true;
}
}
@@ -0,0 +1,148 @@
package nu.nerd.moblimiter.configuration;
import nu.nerd.moblimiter.MobLimiter;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Sheep;
import javax.naming.ConfigurationException;
import java.util.HashMap;
public class Configuration {
private MobLimiter plugin;
private int radius;
private boolean debug;
private int breedingTicks;
private int growthTicks;
private boolean logBlock;
private boolean relativeAge;
private ConfiguredDefaults defaults;
private HashMap<String, ConfiguredMob> limits;
public Configuration() {
plugin = MobLimiter.instance;
plugin.saveDefaultConfig();
load();
}
/**
* Reload the configuration from disk
*/
public void load() {
plugin.reloadConfig();
this.radius = plugin.getConfig().getInt("radius", 3);
this.debug = plugin.getConfig().getBoolean("debug", false);
this.breedingTicks = plugin.getConfig().getInt("breeding_ticks", 300);
this.growthTicks = plugin.getConfig().getInt("growth_ticks", 300);
this.defaults = new ConfiguredDefaults(plugin.getConfig());
this.logBlock = plugin.getConfig().getBoolean("logblock", false);
this.relativeAge = plugin.getConfig().getBoolean("relative_age", false);
this.limits = new HashMap<String, ConfiguredMob>();
ConfigurationSection mobLimits = plugin.getConfig().getConfigurationSection("limits");
if (mobLimits != null) {
for (String key : mobLimits.getKeys(false)) {
try {
ConfigurationSection l = mobLimits.getConfigurationSection(key);
ConfiguredMob mob;
if (l != null) {
mob = new ConfiguredMob(l, defaults);
} else {
mob = new ConfiguredMob(key, defaults); //use default values for YAML "key: []" blocks
}
limits.put(key.toUpperCase(), mob);
} catch (ConfigurationException ex) {
plugin.getLogger().warning(ex.getMessage());
}
}
}
}
/**
* Get the "view distance," in chunks, to check for mobs when a new mob spawns.
*/
public int getRadius() {
return radius;
}
/**
* Is debug mode on?
*/
public boolean debug() {
return debug;
}
/**
* Ticks until a farm animal is ready to breed again.
*/
public int getBreedingTicks() {
return breedingTicks;
}
/**
* Ticks until a farm animal grows up.
*/
public int getGrowthTicks() {
return growthTicks;
}
/**
* Whether support for LogBlock entity removal logging is enabled
* @return true if enabled
*/
public boolean logBlockEnabled() {
return logBlock;
}
/**
* Whether mobs' age should be relative to the last time they target a player
* @return true if enabled
*/
public boolean relativeAgeEnabled() {
return relativeAge;
}
/**
* Get the global default limits
*/
public ConfiguredDefaults getDefaults() {
return defaults;
}
/**
* Get the limits for a specific mob type, gracefully falling back to values from the "default" block
*/
public ConfiguredMob getLimits(Entity entity) {
String key = entity.getType().toString();
if (entity instanceof Sheep) {
key = key + "_" + ((Sheep) entity).getColor().name().toUpperCase();
}
if (limits.containsKey(key)) {
return limits.get(key);
} else {
return new ConfiguredMob(key, defaults);
}
}
public HashMap<String, ConfiguredMob> getAllLimits() {
return limits;
}
}
@@ -0,0 +1,70 @@
package nu.nerd.moblimiter.configuration;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import javax.naming.ConfigurationException;
/**
* Represents global default limits that apply to any mob type, unless a ConfiguredMob overrides.
*/
public class ConfiguredDefaults {
private int age;
private int max;
private int chunkMax;
private int cull;
public ConfiguredDefaults(FileConfiguration config) {
ConfigurationSection section = config.getConfigurationSection("defaults");
if (section != null) {
age = section.getInt("age", -1);
max = section.getInt("max", -1);
chunkMax = section.getInt("chunk_max", -1);
cull = section.getInt("cull", -1);
} else {
age = -1;
max = -1;
chunkMax = -1;
cull = -1;
}
}
/**
* The maximum age, in ticks, that this mob should live for before being removed
*/
public int getAge() {
return age;
}
/**
* The maximum number of this mob type that should be allowed to exist within a
* view distance centered on a spawning mob.
*/
public int getMax() {
return max;
}
/**
* The maximum number of this mob type that should be allowed to exist in a single chunk.
*/
public int getChunkMax() {
return chunkMax;
}
/**
* The number of mobs that should be left after a chunk unload cull
*/
public int getCull() {
return cull;
}
}
@@ -0,0 +1,122 @@
package nu.nerd.moblimiter.configuration;
import nu.nerd.moblimiter.MobLimiter;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.EntityType;
import javax.naming.ConfigurationException;
/**
* Represents configured limits for a specific mob type
*/
public class ConfiguredMob {
private String key;
private EntityType type;
private int age;
private int max;
private int chunkMax;
private int cull;
/**
* Construct a ConfiguredMob from the YAML configuration. Unspecified values will fall back to the values
* defined in the default block.
* @param mob The configuration section
* @param defaults The default values to fall back to
* @throws ConfigurationException
*/
public ConfiguredMob(ConfigurationSection mob, ConfiguredDefaults defaults) throws ConfigurationException{
try {
String name;
if (mob.getName().toUpperCase().startsWith("SHEEP_")) {
name = "SHEEP";
} else {
name = mob.getName();
}
key = mob.getName().toUpperCase();
type = EntityType.valueOf(name.toUpperCase());
age = mob.getInt("age", defaults.getAge());
max = mob.getInt("max", defaults.getMax());
chunkMax = mob.getInt("chunk_max", defaults.getChunkMax());
cull = mob.getInt("cull", defaults.getCull());
} catch (Exception ex) {
throw new ConfigurationException("Invalid configuration for mob type: " + mob.getName());
}
}
/**
* Create a dummy ConfiguredMob with default values, for when one isn't actually configured
* @param mobKey The key this mob type would have in the config
* @param defaults The default values
*/
public ConfiguredMob(String mobKey, ConfiguredDefaults defaults) {
String name;
if (mobKey.toUpperCase().startsWith("SHEEP_")) {
name = "SHEEP";
} else {
name = mobKey.toUpperCase();
}
key = mobKey.toUpperCase();
type = EntityType.valueOf(name.toUpperCase());
age = defaults.getAge();
max = defaults.getMax();
chunkMax = defaults.getChunkMax();
cull = defaults.getCull();
}
/**
* Get the key identifying this mob classification
*/
public String getKey() {
return key;
}
/**
* The Bukkit EntityType these limits apply to
*/
public EntityType getType() {
return type;
}
/**
* The maximum age, in ticks, that this mob should live for before being removed
*/
public int getAge() {
return age;
}
/**
* The maximum number of this mob type that should be allowed to exist within a
* view distance centered on a spawning mob.
*/
public int getMax() {
return max;
}
/**
* The maximum number of this mob type that should be allowed to exist in a single chunk.
*/
public int getChunkMax() {
return chunkMax;
}
/**
* The number of mobs that should be left after a chunk unload cull
*/
public int getCull() {
return cull;
}
}
@@ -0,0 +1,127 @@
package nu.nerd.moblimiter.limiters;
import nu.nerd.moblimiter.MobLimiter;
import nu.nerd.moblimiter.EntityHelper;
import nu.nerd.moblimiter.configuration.ConfiguredMob;
import org.bukkit.Chunk;
import org.bukkit.World;
import org.bukkit.entity.*;
import org.bukkit.event.Listener;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* Sweep loaded chunks every 30 seconds and remove applicable mobs over their type's age limit
*/
public class AgeLimiter extends BukkitRunnable implements Listener {
private MobLimiter plugin;
private int removed;
private Map<UUID, Integer> lastTargeted;
public AgeLimiter() {
plugin = MobLimiter.instance;
lastTargeted = new HashMap<UUID, Integer>();
this.runTaskTimer(plugin, 600L, 600L); //sweep every 30 seconds
}
/**
* Call the sweep on every loaded chunk in every world
*/
public void run() {
removed = 0;
for (World world: plugin.getServer().getWorlds()) {
for (Chunk chunk : world.getLoadedChunks()) {
sweepChunk(chunk);
}
}
if (plugin.getConfiguration().debug()) {
plugin.getLogger().info(String.format("Age limit sweep removed %d entities", removed));
}
}
/**
* Sweep the chunk and remove mobs that are past their shelf life
* @param chunk the chunk to check
*/
private void sweepChunk(Chunk chunk) {
for (Entity entity : chunk.getEntities()) {
// Constrain entities to be removed to limitable mobs, excluding villagers
if (entity.isDead() || !EntityHelper.isLimitableMob(entity) || entity instanceof Villager) continue;
// Exempt special mobs
if (EntityHelper.isSpecialMob((LivingEntity) entity)) {
if (plugin.getConfiguration().debug()) {
plugin.getLogger().info("Special mob exempted from removal: " + EntityHelper.getMobDescription(entity));
}
continue;
}
// Leave two of any farm animals
if (EntityHelper.isBreedingPair(entity)) continue;
// If relative ages are on, and the mob is targeting a player, don't remove it
if (isTargetingPlayer(entity)) continue;
// Remove mobs
ConfiguredMob limits = plugin.getConfiguration().getLimits(entity);
if (!entity.isDead() && adjustedAge(entity) > limits.getAge() && limits.getAge() > -1) {
((LivingEntity) entity).damage(1000); // Kill the entity and drop its items
removed++;
lastTargeted.remove(entity.getUniqueId());
plugin.getLogBlock().logEntityRemoval(entity, 347);
if (plugin.getConfiguration().debug()) {
plugin.getLogger().info("Removed mob (age limit): " + EntityHelper.getMobDescription(entity));
}
}
}
}
/**
* Don't let the sweep kill entities currently targeting a player.
* Also, keep resetting the relative age, since EntityTargetEvent
* only fires when the target changes.
* @param entity the entity to check
* @return true if the Entity is a Creature type and is targeting a player.
*/
private boolean isTargetingPlayer(Entity entity) {
if (!plugin.getConfiguration().relativeAgeEnabled()) return false;
if (entity instanceof Creature) {
Creature creature = (Creature) entity;
if (creature.getTarget() != null && creature.getTarget().getType().equals(EntityType.PLAYER)) {
lastTargeted.put(entity.getUniqueId(), entity.getTicksLived());
return true;
}
}
return false;
}
/**
* Returns the adjusted age of a mob.
* If relative age is on, and the mob recently tageted a player, the delta ticks will be returned.
* Otherwise, the regular getTicksLived() will be used.
* @param entity the entity to check
* @return the age in ticks
*/
public int adjustedAge(Entity entity) {
if (lastTargeted.containsKey(entity.getUniqueId())) {
return entity.getTicksLived() - lastTargeted.get(entity.getUniqueId());
} else {
return entity.getTicksLived();
}
}
}
@@ -0,0 +1,92 @@
package nu.nerd.moblimiter.limiters;
import nu.nerd.moblimiter.EntityHelper;
import nu.nerd.moblimiter.MobLimiter;
import nu.nerd.moblimiter.configuration.ConfiguredMob;
import org.bukkit.Chunk;
import org.bukkit.World;
import org.bukkit.entity.*;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.world.ChunkUnloadEvent;
import java.util.HashMap;
import java.util.Map;
/**
* Cull applicable mobs on chunk unload.
* This is based on behavior from the original MobLimiter 1.x.
*/
public class ChunkUnloadLimiter implements Listener {
private MobLimiter plugin;
public ChunkUnloadLimiter() {
plugin = MobLimiter.instance;
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onChunkUnload(final ChunkUnloadEvent event) {
removeMobs(event.getChunk());
}
/**
* Remove excess mobs in a chunk, if chunk unload culling is enabled for the mob type.
* This is the default behavior from MobLimiter 1.x.
* @param chunk The chunk to cull mobs in
*/
private void removeMobs(Chunk chunk) {
Map<String, Integer> count = new HashMap<String, Integer>();
for (Entity entity : chunk.getEntities()) {
// Constrain entities to be removed to limitable mobs, excluding villagers
if (entity.isDead() || !EntityHelper.isLimitableMob(entity) || entity instanceof Villager) continue;
// Exempt special mobs
if (EntityHelper.isSpecialMob((LivingEntity) entity)) {
if (plugin.getConfiguration().debug()) {
plugin.getLogger().info("Special mob exempted from removal: " + EntityHelper.getMobDescription(entity));
}
continue;
}
ConfiguredMob limits = plugin.getConfiguration().getLimits(entity);
String key = limits.getKey();
int cap = limits.getCull();
Integer oldCount = count.get(key);
int mobCount = (oldCount == null) ? 1 : oldCount + 1;
count.put(key, mobCount);
if (cap > -1 && mobCount > cap) {
plugin.getLogBlock().logEntityRemoval(entity, 283);
entity.remove();
if (plugin.getConfiguration().debug()) {
plugin.getLogger().info("Chunk unload removed: " + EntityHelper.getMobDescription(entity));
}
}
}
}
/**
* Run removeMobs() on all loaded chunks
*/
public void removeAllMobs() {
for (World world : plugin.getServer().getWorlds()) {
for (Chunk chunk : world.getLoadedChunks()) {
removeMobs(chunk);
}
}
}
}
@@ -0,0 +1,122 @@
package nu.nerd.moblimiter.limiters;
import nu.nerd.moblimiter.EntityHelper;
import nu.nerd.moblimiter.MobLimiter;
import nu.nerd.moblimiter.configuration.ConfiguredMob;
import org.bukkit.Chunk;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
import java.util.ArrayList;
import java.util.List;
/**
* Limit newly spawning mobs if there are too many
*/
public class SpawnLimiter implements Listener {
private MobLimiter plugin;
private List<SpawnReason> reasons;
public SpawnLimiter() {
plugin = MobLimiter.instance;
plugin.getServer().getPluginManager().registerEvents(this, plugin);
reasons = new ArrayList<SpawnReason>();
reasons.add(SpawnReason.BREEDING);
reasons.add(SpawnReason.DEFAULT);
reasons.add(SpawnReason.NATURAL);
reasons.add(SpawnReason.SPAWNER);
}
/**
* Handle mob limting on creature spawn
*/
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
public void onCreatureSpawnEvent(final CreatureSpawnEvent event) {
SpawnReason reason = event.getSpawnReason();
ConfiguredMob limits = plugin.getConfiguration().getLimits(event.getEntity());
if (!reasons.contains(reason)) return;
// Cancel spawn of mobs over the radius limit
if (countEntitiesInSpawnRadius(event.getEntity()) >= limits.getMax() && limits.getMax() > -1) {
log(event.getEntity(), reason, "radius", limits.getMax());
event.getEntity().remove();
}
// Cancel spawn of mobs over the chunk limit
if (countEntitiesInChunk(event.getEntity()) >= limits.getChunkMax() && limits.getChunkMax() > -1) {
log(event.getEntity(), reason, "chunk", limits.getChunkMax());
event.getEntity().remove();
}
}
/**
* Count entities of the same type within a "view distance" in chunks from the original entity
* @param entity the entity to check
* @return number of matching entities
*/
private int countEntitiesInSpawnRadius(Entity entity) {
int count = 0;
int radius = plugin.getConfiguration().getRadius();
ConfiguredMob mob = plugin.getConfiguration().getLimits(entity);
World world = entity.getWorld();
Chunk start = entity.getLocation().getChunk();
for (int x = start.getX() - radius; x <= start.getX() + radius; x++) {
for (int z = start.getZ() - radius; z <= start.getZ() + radius; z++) {
Chunk c = world.getChunkAt(x, z);
for (Entity e : c.getEntities()) {
ConfiguredMob m = plugin.getConfiguration().getLimits(e);
if (m.getKey().equals(mob.getKey()) && !e.isDead()) {
count++;
}
}
}
}
return count;
}
/**
* Count entities of the same type in an individual chunk
* @param entity The entity to check
* @return number of matching entities
*/
private int countEntitiesInChunk(Entity entity) {
int count = 0;
ConfiguredMob mob = plugin.getConfiguration().getLimits(entity);
for (Entity e : entity.getLocation().getChunk().getEntities()) {
ConfiguredMob m = plugin.getConfiguration().getLimits(e);
if (m.getKey().equals(mob.getKey()) && !e.isDead()) {
count++;
}
}
return count;
}
/**
* Log entity removal for diagnostic purposes if debug mode is on
*/
private void log(Entity entity, SpawnReason reason, String capType, int cap) {
if (!plugin.getConfiguration().debug()) return;
String mob = EntityHelper.getMobDescription(entity);
String details = String.format("[reason: %s, cap type: %s, cap: %d]", reason.toString(), capType, cap);
String msg = String.format("Cancelled spawn of %s %s", mob, details);
plugin.getLogger().info(msg);
}
}
@@ -0,0 +1,45 @@
package nu.nerd.moblimiter.logblock;
import de.diddiz.LogBlock.Actor;
import de.diddiz.LogBlock.Consumer;
import de.diddiz.LogBlock.LogBlock;
import nu.nerd.moblimiter.MobLimiter;
import org.bukkit.Bukkit;
import org.bukkit.entity.Entity;
import org.bukkit.plugin.Plugin;
/**
* Handle communication with the LogBlock API.
* This will be used by LogBlockWrapper to safely avoid creating a hard dependency.
*/
public class LogBlockBackend {
private MobLimiter plugin;
private Consumer lbConsumer = null;
public LogBlockBackend() {
plugin = MobLimiter.instance;
Plugin plugin = Bukkit.getServer().getPluginManager().getPlugin("LogBlock");
if (plugin != null) lbConsumer = ((LogBlock) plugin).getConsumer();
}
/**
* Log an entity removal to LogBlock with the reason specified
* @param entity The entity being removed
* @param name The "name" LogBlock will store. e.g. MobLimiterCull
* @param itemId The item to ascribe the kill to. Zero for fist.
*/
public void logEntityRemoval(Entity entity, String name, int itemId) {
if (lbConsumer != null) {
Actor killer = new Actor(name);
Actor victim = Actor.actorFromEntity(entity);
lbConsumer.queueKill(entity.getLocation(), killer, victim, itemId);
}
}
}
@@ -0,0 +1,45 @@
package nu.nerd.moblimiter.logblock;
import nu.nerd.moblimiter.MobLimiter;
import org.bukkit.Bukkit;
import org.bukkit.entity.Entity;
import org.bukkit.plugin.Plugin;
/**
* Wraps LogBlock API calls so it can be a soft dependency
*/
public class LogBlockWrapper {
private MobLimiter plugin;
private LogBlockBackend logBlock = null;
public LogBlockWrapper() {
plugin = MobLimiter.instance;
if (plugin.getConfiguration().logBlockEnabled() && logBlockPresent()) {
logBlock = new LogBlockBackend();
}
}
/**
* Log an entity removal to LogBlock with the reason specified
* @param entity The entity being removed
* @param itemId The "weapon" used. Zero for first
*/
public void logEntityRemoval(Entity entity, int itemId) {
if (logBlock != null) {
logBlock.logEntityRemoval(entity, "MobLimiter", itemId);
}
}
private boolean logBlockPresent() {
Plugin plugin = Bukkit.getServer().getPluginManager().getPlugin("LogBlock");
return (plugin != null);
}
}
BIN
View File
Binary file not shown.