66import io .papermc .paper .threadedregions .scheduler .ScheduledTask ;
77import me .xginko .aef .modules .AEFModule ;
88import me .xginko .aef .utils .ChunkUtil ;
9+ import me .xginko .aef .utils .EntityUtil ;
910import me .xginko .aef .utils .LocationUtil ;
1011import org .bukkit .Chunk ;
11- import org .bukkit .Material ;
1212import org .bukkit .World ;
1313import org .bukkit .entity .Entity ;
1414import org .bukkit .entity .EntityType ;
1717import org .bukkit .event .HandlerList ;
1818import org .bukkit .event .Listener ;
1919import org .bukkit .event .entity .EntitySpawnEvent ;
20- import org .bukkit .event .world .ChunkUnloadEvent ;
20+ import org .bukkit .event .world .EntitiesLoadEvent ;
21+ import org .bukkit .event .world .EntitiesUnloadEvent ;
2122
2223import java .util .EnumMap ;
24+ import java .util .List ;
2325import java .util .Map ;
26+ import java .util .Objects ;
2427import java .util .TreeMap ;
25- import java .util .concurrent .atomic .AtomicInteger ;
2628import java .util .function .Consumer ;
2729
2830public class CustomEntityLimit extends AEFModule implements Consumer <ScheduledTask >, Listener {
2931
3032 private final Map <EntityType , Integer > entityLimits = new EnumMap <>(EntityType .class );
3133 private final long checkPeriod , minChunkAge ;
32- private final boolean logIsEnabled , enableChunkAgeSkip , forceLoadEntities ;
34+ private final boolean enableChunkAgeSkip , forceLoadEntities ;
3335
3436 private ScheduledTask scheduledTask ;
3537
3638 public CustomEntityLimit () {
3739 super ("chunk-limits.entity-limits.custom-limit" , false , """
3840 Limit specific entity types per chunk.\s
3941 Read over the defaults at least once before enabling.""" );
40- this .logIsEnabled = config .getBoolean (configPath + ".log-removals" , true );
4142 this .checkPeriod = config .getInt (configPath + ".check-period-in-ticks" , 1200 , """
4243 Check all loaded chunks every x ticks.""" );
4344 this .enableChunkAgeSkip = config .getBoolean (configPath + ".chunk-age-skip.enable" , true );
@@ -137,14 +138,15 @@ public CustomEntityLimit() {
137138 https://jd.papermc.io/paper/1.20.6/org/bukkit/entity/EntityType.html\s
138139 Make sure your minecraft version is matching as well.""" );
139140 for (String configuredEntity : section .getKeys (false )) {
141+ String configuredLimit = section .getString (configuredEntity );
140142 try {
141- Integer maxAmountPerChunk = Integer . parseInt ( section . getString ( configuredEntity ));
142- EntityType limitedEntity = EntityType .valueOf (configuredEntity );
143- entityLimits . put ( limitedEntity , maxAmountPerChunk );
144- } catch (NumberFormatException e ) {
143+ entityLimits . put (
144+ EntityType .valueOf (configuredEntity ),
145+ Integer . parseInt ( Objects . requireNonNull ( configuredLimit )) );
146+ } catch (NumberFormatException | NullPointerException e ) {
145147 notRecognized (Integer .class , configuredEntity );
146148 } catch (IllegalArgumentException e ) {
147- notRecognized (Material .class , configuredEntity );
149+ notRecognized (EntityType .class , configuredEntity );
148150 }
149151 }
150152 }
@@ -162,94 +164,118 @@ public void disable() {
162164 if (scheduledTask != null ) scheduledTask .cancel ();
163165 }
164166
165- @ EventHandler (priority = EventPriority .HIGHEST , ignoreCancelled = true )
166- private void onChunkUnload (ChunkUnloadEvent event ) {
167- if (ChunkUtil .isRetrievalUnsafe (event .getChunk ())) return ;
167+ private void enforceEntityLimit (Map <EntityType , List <Entity >> entitiesByTypes ) {
168+ for (Map .Entry <EntityType , List <Entity >> entry : entitiesByTypes .entrySet ()) {
169+ if (!entityLimits .containsKey (entry .getKey ())) {
170+ continue ;
171+ }
168172
169- for (Map .Entry <EntityType , Integer > limit : entityLimits .entrySet ()) {
170- final int maxAllowedPerChunk = limit .getValue ();
173+ int entityLimit = entityLimits .get (entry .getKey ());
171174
172- AtomicInteger count = new AtomicInteger ();
175+ if (entry .getValue ().size () <= entityLimit ) {
176+ continue ;
177+ }
173178
174- for (Entity entity : event .getChunk ().getEntities ()) {
175- entity .getScheduler ().execute (plugin , () -> {
176- if (entity .getType () != limit .getKey ()) return ;
177- if (count .incrementAndGet () <= maxAllowedPerChunk ) return ;
179+ List <Entity > entitiesToRemove = entry .getValue ().subList (entityLimit , entry .getValue ().size () - 1 );
178180
181+ for (int i = 1 ; i < entitiesToRemove .size (); i ++) {
182+ Entity entity = entitiesToRemove .get (i );
183+ int index = i ;
184+ entity .getScheduler ().execute (plugin , () -> {
179185 entity .remove ();
180- if (logIsEnabled ) info ("Removed entity " + entity .getType () + " at " +
181- LocationUtil .toString (entity .getLocation ()) + " because reached limit of " + maxAllowedPerChunk );
186+ logger ().info ("Removed {} at {} ({}/{} limit={})" ,
187+ entity .getType (),
188+ LocationUtil .toString (entity .getLocation ()),
189+ index ,
190+ entitiesToRemove .size (),
191+ entityLimit );
182192 }, null , 1L );
183193 }
184- }
185- }
186194
187- // Abort spawning process when chunk limit is reached
188- @ EventHandler (priority = EventPriority .HIGHEST , ignoreCancelled = true )
189- private void onEntitySpawn (PreCreatureSpawnEvent event ) {
190- if (!entityLimits .containsKey (event .getType ())) return ;
195+ entitiesToRemove .clear ();
196+ entry .getValue ().clear ();
197+ }
191198
192- final int maxAllowedPerChunk = entityLimits . get ( event . getType () );
193- int entityCount = 1 ; // Count entity that spawned in this event
199+ entitiesByTypes . clear ( );
200+ }
194201
195- for (Entity entity : event .getSpawnLocation ().getChunk ().getEntities ()) {
196- if (entity .getType () != event .getType ()) continue ;
202+ @ Override
203+ public void accept (ScheduledTask task ) {
204+ for (World world : plugin .getServer ().getWorlds ()) {
205+ for (Chunk chunk : world .getLoadedChunks ()) {
206+ if (ChunkUtil .isRetrievalUnsafe (chunk )) {
207+ continue ;
208+ }
197209
198- entityCount ++;
210+ plugin .getServer ().getRegionScheduler ().execute (plugin , world , chunk .getX (), chunk .getZ (), () -> {
211+ if (!forceLoadEntities && !chunk .isEntitiesLoaded ()) return ;
212+ if (enableChunkAgeSkip && chunk .getInhabitedTime () < minChunkAge ) return ;
199213
200- if (entityCount > maxAllowedPerChunk ) {
201- event .setCancelled (true );
202- event .setShouldAbortSpawn (true );
214+ enforceEntityLimit (EntityUtil .getEntityTypeMap (chunk .getEntities ()));
215+ });
203216 }
204217 }
205218 }
206219
207220 @ EventHandler (priority = EventPriority .HIGHEST , ignoreCancelled = true )
208- private void onSpawn ( EntitySpawnEvent event ) {
209- if (! entityLimits . containsKey (event .getEntityType ())) return ;
221+ private void onEntitiesLoad ( EntitiesLoadEvent event ) {
222+ if (ChunkUtil . isRetrievalUnsafe (event .getChunk ())) return ;
210223
211- final int maxAllowedPerChunk = entityLimits . get (event .getEntityType ( ));
212- int entityCount = 1 ; // Count entity that spawned in this event
224+ enforceEntityLimit ( EntityUtil . getEntityTypeMap (event .getEntities () ));
225+ }
213226
214- for (Entity entity : event .getEntity ().getChunk ().getEntities ()) {
215- if (entity .getType () != event .getEntityType ()) continue ;
227+ @ EventHandler (priority = EventPriority .HIGHEST , ignoreCancelled = true )
228+ private void onEntitiesUnload (EntitiesUnloadEvent event ) {
229+ if (ChunkUtil .isRetrievalUnsafe (event .getChunk ())) return ;
230+
231+ enforceEntityLimit (EntityUtil .getEntityTypeMap (event .getEntities ()));
232+ }
233+
234+ @ EventHandler (priority = EventPriority .HIGHEST , ignoreCancelled = true )
235+ private void onPreCreatureSpawn (PreCreatureSpawnEvent event ) {
236+ if (!entityLimits .containsKey (event .getType ())) return ;
237+
238+ int entityLimit = entityLimits .get (event .getType ());
239+ int entityCount = 0 ;
216240
217- entityCount ++;
218- if (entityCount <= maxAllowedPerChunk ) continue ;
241+ for (Entity entity : event .getSpawnLocation ().getChunk ().getEntities ()) {
242+ if (entity .getType () == event .getType ()) {
243+ entityCount ++;
244+ }
245+ }
219246
220- entity .remove ();
221- if (logIsEnabled ) info ("Removed entity " + entity .getType () + " at " +
222- LocationUtil .toString (entity .getLocation ()) + " because reached limit of " + maxAllowedPerChunk );
247+ if (entityCount + 1 > entityLimit ) {
248+ event .setCancelled (true );
249+ logger ().info ("Cancelled {} of {} at {} => count={}, limit={}" ,
250+ event .getEventName (),
251+ event .getType (),
252+ LocationUtil .toString (event .getSpawnLocation ()),
253+ entityCount ,
254+ entityLimit );
223255 }
224256 }
225257
226- @ Override
227- public void accept (ScheduledTask task ) {
228- for (World world : plugin .getServer ().getWorlds ()) {
229- for (Chunk chunk : world .getLoadedChunks ()) {
230- if (ChunkUtil .isRetrievalUnsafe (chunk )) continue ;
258+ @ EventHandler (priority = EventPriority .HIGHEST , ignoreCancelled = true )
259+ private void onEntitySpawn (EntitySpawnEvent event ) {
260+ if (!entityLimits .containsKey (event .getEntityType ())) return ;
231261
232- plugin .getServer ().getRegionScheduler ().execute (plugin , world , chunk .getX (), chunk .getZ (), () -> {
233- if (!forceLoadEntities && !chunk .isEntitiesLoaded ()) return ;
234- if (enableChunkAgeSkip && chunk .getInhabitedTime () < minChunkAge ) return ;
262+ int entityLimit = entityLimits .get (event .getEntityType ());
263+ int entityCount = 0 ;
235264
236- Entity [] chunkEntities = chunk .getEntities ();
237-
238- for (Map .Entry <EntityType , Integer > limit : entityLimits .entrySet ()) {
239- AtomicInteger count = new AtomicInteger ();
240- for (Entity entity : chunkEntities ) {
241- entity .getScheduler ().execute (plugin , () -> {
242- if (entity .getType () != limit .getKey ()) return ;
243- if (count .incrementAndGet () <= limit .getValue ()) return ;
244-
245- entity .remove ();
246- if (logIsEnabled ) info ("Removed entity " + entity .getType () + " at " +
247- LocationUtil .toString (entity .getLocation ()) + " because reached limit of " + limit .getValue ());
248- }, null , 1L );
249- }
250- }
251- });
265+ for (Entity entity : event .getLocation ().getChunk ().getEntities ()) {
266+ if (entity .getType () == event .getEntityType ()) {
267+ entityCount ++;
252268 }
253269 }
270+
271+ if (entityCount + 1 > entityLimit ) {
272+ event .setCancelled (true );
273+ logger ().info ("Cancelled {} of {} at {} => count={}, limit={}" ,
274+ event .getEventName (),
275+ event .getEntityType (),
276+ LocationUtil .toString (event .getLocation ()),
277+ entityCount ,
278+ entityLimit );
279+ }
254280 }
255281}
0 commit comments