44import com .github .benmanes .caffeine .cache .Cache ;
55import com .github .benmanes .caffeine .cache .Caffeine ;
66import io .github .thatsmusic99 .configurationmaster .api .ConfigSection ;
7+ import me .xginko .aef .AnarchyExploitFixes ;
78import me .xginko .aef .modules .AEFModule ;
9+ import me .xginko .aef .utils .LocationUtil ;
10+ import net .kyori .adventure .text .TextReplacementConfig ;
811import org .bukkit .Chunk ;
12+ import org .bukkit .Location ;
913import org .bukkit .Material ;
1014import org .bukkit .event .EventHandler ;
1115import org .bukkit .event .EventPriority ;
1216import org .bukkit .event .HandlerList ;
1317import org .bukkit .event .Listener ;
18+ import org .bukkit .event .block .Action ;
1419import org .bukkit .event .block .BlockPlaceEvent ;
1520import org .bukkit .event .player .PlayerInteractEvent ;
21+ import org .jetbrains .annotations .NotNull ;
1622
1723import java .time .Duration ;
1824import java .util .EnumMap ;
1925import java .util .Map ;
26+ import java .util .Objects ;
2027import java .util .TreeMap ;
2128
2229public class BlockLimit extends AEFModule implements Listener {
2330
2431 private final Map <Material , Integer > blockLimits = new EnumMap <>(Material .class );
2532 private final long materialCountCacheMillis ;
33+ private final boolean notifyPlayer ;
2634
27- private Cache <Chunk , Cache <Material , Boolean >> chunkMaterialCache ;
35+ private Cache <@ NotNull Chunk , Cache <@ NotNull Material , Integer >> chunkMaterialCountCache ;
2836
2937 public BlockLimit () {
3038 super ("chunk-limits.block-limit" , false );
39+ this .notifyPlayer = config .getBoolean (configPath + ".notify-player" , true ,
40+ "Send a message to the player when they cant place a block due to chunk limit." );
3141 this .materialCountCacheMillis = config .getLong (configPath + ".material-count-cache-millis" , 1000 ,
3242 "Recommended to not go higher than 5000ms." );
3343
@@ -135,14 +145,15 @@ public BlockLimit() {
135145 }
136146 }
137147
138- ConfigSection section = config .getConfigSection (configPath + ".max-blocks-per-chunk" , compatible ,
148+ ConfigSection limitsSection = config .getConfigSection (configPath + ".max-blocks-per-chunk" , compatible ,
139149 "Attempt to prevent ChunkBan / Client FPS Lag" );
140- for (String configuredMaterial : section .getKeys (false )) {
150+ for (String configuredMaterial : limitsSection .getKeys (false )) {
151+ String configuredMaxAmount = limitsSection .getString (configuredMaterial );
141152 try {
142- Material blockMaterial = Material . valueOf ( configuredMaterial );
143- Integer maxAmountPerChunk = Integer . parseInt ( section . getString ( configuredMaterial ));
144- this . blockLimits . put ( blockMaterial , maxAmountPerChunk );
145- } catch (NumberFormatException e ) {
153+ this . blockLimits . put (
154+ Material . valueOf ( configuredMaterial ),
155+ Integer . parseInt ( Objects . requireNonNull ( configuredMaxAmount )) );
156+ } catch (NumberFormatException | NullPointerException e ) {
146157 notRecognized (Integer .class , configuredMaterial );
147158 } catch (IllegalArgumentException e ) {
148159 notRecognized (Material .class , configuredMaterial );
@@ -152,70 +163,149 @@ public BlockLimit() {
152163
153164 @ Override
154165 public void enable () {
155- chunkMaterialCache = Caffeine .newBuilder ().expireAfterWrite (Duration .ofMinutes (1 )).build ();
166+ chunkMaterialCountCache = Caffeine .newBuilder ().expireAfterWrite (Duration .ofMinutes (1 )).build ();
156167 plugin .getServer ().getPluginManager ().registerEvents (this , plugin );
157168 }
158169
159170 @ Override
160171 public void disable () {
161172 HandlerList .unregisterAll (this );
162- if (chunkMaterialCache != null ) {
163- for (Map .Entry <Chunk , Cache <Material , Boolean >> entry : chunkMaterialCache .asMap ().entrySet ()) {
173+ if (chunkMaterialCountCache != null ) {
174+ for (Map .Entry <@ NotNull Chunk , Cache <@ NotNull Material , Integer >> entry : chunkMaterialCountCache .asMap ().entrySet ()) {
164175 entry .getValue ().invalidateAll ();
165176 entry .getValue ().cleanUp ();
166177 }
167- chunkMaterialCache .invalidateAll ();
168- chunkMaterialCache .cleanUp ();
169- chunkMaterialCache = null ;
178+ chunkMaterialCountCache .invalidateAll ();
179+ chunkMaterialCountCache .cleanUp ();
180+ chunkMaterialCountCache = null ;
170181 }
171182 }
172183
173184 @ EventHandler (priority = EventPriority .HIGHEST , ignoreCancelled = true )
174185 private void onBlockPlace (BlockPlaceEvent event ) {
175- if (blockLimits .containsKey (event .getBlock ().getType ())
176- && exceedsPerChunkLimit (event .getBlock ().getType (), event .getBlock ().getChunk ())) {
177- event .setCancelled (true );
186+ if (!blockLimits .containsKey (event .getBlock ().getType ())) {
187+ return ;
178188 }
189+
190+ int materialCount = getMaterialCountForChunk (event .getBlock ().getType (), event .getBlock ().getChunk ());
191+ int limit = blockLimits .get (event .getBlock ().getType ());
192+
193+ if (materialCount <= limit ) {
194+ logger ().debug ("Player '{}' may place block {} at location {} => count={} limit={}" ,
195+ event .getPlayer ().getName (),
196+ event .getBlock ().getType (),
197+ LocationUtil .toString (event .getBlock ().getLocation ()),
198+ materialCount ,
199+ limit );
200+ return ;
201+ }
202+
203+ event .setCancelled (true );
204+
205+ if (notifyPlayer ) event .getPlayer ().sendMessage (
206+ AnarchyExploitFixes .translation (event .getPlayer ()).chunklimits_block_limit_exceeded
207+ .replaceText (TextReplacementConfig .builder ()
208+ .matchLiteral ("%block%" )
209+ .replacement (event .getBlock ().getType ().name ())
210+ .build ())
211+ .replaceText (TextReplacementConfig .builder ()
212+ .matchLiteral ("%existing%" )
213+ .replacement (Integer .toString (materialCount ))
214+ .build ())
215+ .replaceText (TextReplacementConfig .builder ()
216+ .matchLiteral ("%limit%" )
217+ .replacement (Integer .toString (limit ))
218+ .build ()));
219+
220+ logger ().info ("Cancelled placement of {} for player '{}' at location {} => count={} limit={}" ,
221+ event .getBlock ().getType (),
222+ event .getPlayer ().getName (),
223+ LocationUtil .toString (event .getBlock ().getLocation ()),
224+ materialCount ,
225+ blockLimits .get (event .getBlock ().getType ()));
179226 }
180227
228+ @ SuppressWarnings ("deprecation" )
181229 @ EventHandler (priority = EventPriority .HIGHEST , ignoreCancelled = true )
182230 private void onPlayerInteract (PlayerInteractEvent event ) {
183- if (event .getAction ().isLeftClick ()) return ;
231+ if (event .getAction () != Action .RIGHT_CLICK_BLOCK || event .getClickedBlock () == null ) return ;
232+
233+ if (!blockLimits .containsKey (event .getMaterial ())) {
234+ return ;
235+ }
236+
237+ // Ignore interactions like for ex. players wanting to flick a lever
238+ if (event .getMaterial ().isInteractable () && !event .getPlayer ().isSneaking ()) {
239+ return ; // Let BlockPlaceEvent deal with this
240+ }
241+
242+ Location placeLocation = event .getClickedBlock ().getRelative (event .getBlockFace ()).getLocation ();
243+ int materialCount = getMaterialCountForChunk (event .getMaterial (), placeLocation .getChunk ());
244+ int limit = blockLimits .get (event .getMaterial ());
184245
185- if (blockLimits .containsKey (event .getMaterial ())
186- && exceedsPerChunkLimit (event .getMaterial (), event .getPlayer ().getChunk ())) {
187- event .setCancelled (true );
246+ if (materialCount <= limit ) {
247+ logger ().debug ("Player '{}' may place block {} at location {} => count={} limit={}" ,
248+ event .getPlayer ().getName (),
249+ event .getMaterial (),
250+ LocationUtil .toString (placeLocation ),
251+ materialCount ,
252+ limit );
253+ return ;
188254 }
255+
256+ event .setCancelled (true );
257+
258+ if (notifyPlayer ) event .getPlayer ().sendMessage (
259+ AnarchyExploitFixes .translation (event .getPlayer ()).chunklimits_block_limit_exceeded
260+ .replaceText (TextReplacementConfig .builder ()
261+ .matchLiteral ("%block%" )
262+ .replacement (event .getMaterial ().name ())
263+ .build ())
264+ .replaceText (TextReplacementConfig .builder ()
265+ .matchLiteral ("%existing%" )
266+ .replacement (Integer .toString (materialCount ))
267+ .build ())
268+ .replaceText (TextReplacementConfig .builder ()
269+ .matchLiteral ("%limit%" )
270+ .replacement (Integer .toString (limit ))
271+ .build ()));
272+
273+ logger ().info ("Cancelled placement of {} for player '{}' at location {} => count={} limit={}" ,
274+ event .getMaterial (),
275+ event .getPlayer ().getName (),
276+ LocationUtil .toString (placeLocation ),
277+ materialCount ,
278+ blockLimits .get (event .getMaterial ()));
189279 }
190280
191- private boolean exceedsPerChunkLimit (Material blockType , Chunk chunk ) {
192- final int limit = blockLimits .get (blockType );
193- final int minY = chunk .getWorld ().getMinHeight ();
194- final int maxY = chunk .getWorld ().getMaxHeight ();
281+ private int getMaterialCountForChunk (Material blockType , Chunk chunk ) {
282+ Cache <@ NotNull Material , Integer > countCache = chunkMaterialCountCache .getIfPresent (chunk );
195283
196- Cache <Material , Boolean > exceededCache = chunkMaterialCache .getIfPresent (chunk );
197- if (exceededCache == null ) {
198- exceededCache = Caffeine .newBuilder ().expireAfterWrite (Duration .ofMillis (materialCountCacheMillis )).build ();
284+ if (countCache == null ) {
285+ countCache = Caffeine .newBuilder ().expireAfterWrite (Duration .ofMillis (materialCountCacheMillis )).build ();
199286 }
200287
201- Boolean exceeded = exceededCache .get (blockType , material -> {
288+ Integer cachedMaterialCount = countCache .getIfPresent (blockType );
289+
290+ if (cachedMaterialCount == null ) {
291+ int minY = chunk .getWorld ().getMinHeight ();
292+ int maxY = chunk .getWorld ().getMaxHeight ();
202293 int count = 0 ;
294+
203295 for (int x = 0 ; x < 16 ; x ++) {
204296 for (int z = 0 ; z < 16 ; z ++) {
205297 for (int y = minY ; y < maxY ; y ++) {
206- if (chunk .getBlock (x , y , z ).getType () == material ) {
298+ if (chunk .getBlock (x , y , z ).getType () == blockType ) {
207299 count ++;
208- if (count > limit ) {
209- return true ;
210- }
211300 }
212301 }
213302 }
214303 }
215- return false ;
216- });
217304
218- chunkMaterialCache .put (chunk , exceededCache );
219- return exceeded ;
305+ cachedMaterialCount = count ;
306+ countCache .put (blockType , count );
307+ }
308+
309+ return cachedMaterialCount ;
220310 }
221311}
0 commit comments