New concurrent cache: hopperCountByChunk keyed as "world:x,z".
Lazy initialization: If a loaded chunk has no cache entry at use time, it’s scanned and cached.
Event-driven updates:
ChunkLoadEvent: scan and seed cache.
ChunkUnloadEvent: remove cache and related per-chunk tick maps to prevent leaks.
BlockPlaceEvent (HOPPER): increment chunk count.
BlockBreakEvent (HOPPER): decrement chunk count (floors at 0).
Debug messaging for throttling per chunk uses the existing key debug.hopper.throttling from messages.yml (rate limited to once per 100 ticks per chunk).
Changed
Hopper cooldown computation now properly applies the soft cap:
Base cooldown: lag-prevention.hopper-limiter.transfer-cooldown ticks (min 1).
If hopper count in the chunk > max-hoppers-per-chunk, effective cooldown increases by 2 * (excess) ticks.
The count method is now cache-backed:
countHoppersInChunkCached(Chunk) returns the cached number, performs a synchronous scan if necessary for loaded chunks, and returns 0 for unloaded chunks (no forced chunk loads).
Fixed
Soft-cap effect was previously ineffective because hopper counts always returned 0; now it reflects real counts and increases cooldown in hopper-dense chunks.