diff --git a/src/main/java/world/bentobox/level/Level.java b/src/main/java/world/bentobox/level/Level.java index 82f6702..74f75e0 100644 --- a/src/main/java/world/bentobox/level/Level.java +++ b/src/main/java/world/bentobox/level/Level.java @@ -46,6 +46,7 @@ import world.bentobox.level.listeners.IslandActivitiesListeners; import world.bentobox.level.listeners.JoinLeaveListener; import world.bentobox.level.listeners.MigrationListener; +import world.bentobox.level.listeners.NewChunkListener; import world.bentobox.level.requests.LevelRequestHandler; import world.bentobox.level.requests.TopTenRequestHandler; import world.bentobox.visit.VisitAddon; @@ -154,6 +155,10 @@ private void registerAllListeners() { registerListener(new IslandActivitiesListeners(this)); registerListener(new JoinLeaveListener(this)); registerListener(new MigrationListener(this)); + // Accumulates generator block points into initialCount as new chunks + // are generated, so large protection ranges work with zero-new-island + // mode without forcing the initial scan to generate the whole area. + registerListener(new NewChunkListener(this)); } private void registerGameModeCommands() { diff --git a/src/main/java/world/bentobox/level/LevelsManager.java b/src/main/java/world/bentobox/level/LevelsManager.java index 3816947..3d62ba9 100644 --- a/src/main/java/world/bentobox/level/LevelsManager.java +++ b/src/main/java/world/bentobox/level/LevelsManager.java @@ -480,7 +480,7 @@ public void removeEntry(World world, String uuid) { /** * Set an initial island count - * + * * @param island - the island to set. * @param lv - initial island count */ @@ -489,6 +489,27 @@ public void setInitialIslandCount(@NonNull Island island, long lv) { handler.saveObjectAsync(levelsCache.get(island.getUniqueId())); } + /** + * Add a delta to the island's initial-count handicap. Used by the new-chunk + * listener to accumulate generator block points (sea floor, nether ceiling, + * etc.) into the initial count as chunks are generated during normal play. + * The initial count is subtracted from the live block total in the level + * calc, so generator blocks do not inflate the level. + * + * @param island the island + * @param delta the points to add (no-op when zero) + */ + public void addToInitialCount(@NonNull Island island, long delta) { + if (delta == 0) { + return; + } + // Use getInitialCount so any legacy initialLevel is migrated first. + long current = getInitialCount(island); + IslandLevels data = getLevelsData(island); + data.setInitialCount(current + delta); + handler.saveObjectAsync(data); + } + /** * Set the island level for the owner of the island that targetPlayer is a * member diff --git a/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java b/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java index dc1ddcc..4cb1df0 100644 --- a/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java +++ b/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java @@ -366,8 +366,14 @@ private void loadChunks(CompletableFuture> r2, World world, Queue p = pairList.poll(); - // We need to generate now all the time because some game modes are not voids - Util.getChunkAtAsync(world, p.x, p.z, true).thenAccept(chunk -> { + // For zero-island scans, do not force chunk generation. Forcing the + // generator for every chunk in a large protection range (e.g. 1000 → + // ~16k chunks/dim) blows past the calculation timeout. Generator + // blocks that appear later (sea floor, nether ceiling, etc.) are + // picked up incrementally by NewChunkListener as chunks generate + // during normal play. Regular scans still generate, because some game + // modes are not voids. + Util.getChunkAtAsync(world, p.x, p.z, !zeroIsland).thenAccept(chunk -> { if (chunk != null) { chunkList.add(chunk); roseStackerCheck(chunk); diff --git a/src/main/java/world/bentobox/level/listeners/NewChunkListener.java b/src/main/java/world/bentobox/level/listeners/NewChunkListener.java new file mode 100644 index 0000000..3ecdf88 --- /dev/null +++ b/src/main/java/world/bentobox/level/listeners/NewChunkListener.java @@ -0,0 +1,134 @@ +package world.bentobox.level.listeners; + +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.ChunkSnapshot; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.world.ChunkLoadEvent; + +import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.level.Level; + +/** + * Listens for freshly-generated chunks inside an island's protected area and + * adds the chunk's generator block points to the island's initial-count + * handicap. + *

+ * Together with the {@code gen=false} initial zero scan in + * {@link world.bentobox.level.calculators.IslandLevelCalculator}, this lets + * zero-new-island-level mode work on islands with very large protection + * ranges. The initial scan only records what is already generated at island + * creation time (typically just the schematic chunks). As the player + * explores and new chunks are generated, this listener accumulates their + * generator block points into the initial count so they cancel out of the + * regular level calc — players only get credit for blocks they actually + * place. + */ +public class NewChunkListener implements Listener { + + /** + * Snapshot of the main-thread state needed to score one chunk on a worker + * thread. Bundled into a record so the async scan helpers don't need to + * carry a dozen parameters each. + */ + private record ScanContext(World world, int chunkBlockX, int chunkBlockZ, int minHeight, int maxHeight, + int minProtectedX, int maxProtectedX, int minProtectedZ, int maxProtectedZ, + int seaHeight, double underwaterMultiplier) { + } + + private final Level addon; + + public NewChunkListener(Level addon) { + this.addon = addon; + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onChunkLoad(ChunkLoadEvent e) { + if (!e.isNewChunk()) { + return; + } + if (!addon.getSettings().isZeroNewIslandLevels()) { + return; + } + Chunk chunk = e.getChunk(); + World world = chunk.getWorld(); + if (!addon.isRegisteredGameModeWorld(world)) { + return; + } + // Use the chunk centre to look up the island that owns it. + // The + 8.0 keeps the addition in double arithmetic so SonarQube does not + // flag a theoretical int-overflow before the implicit widening. + Location centre = new Location(world, (chunk.getX() << 4) + 8.0, world.getMinHeight(), + (chunk.getZ() << 4) + 8.0); + Island island = addon.getIslands().getIslandAt(centre).orElse(null); + if (island == null || island.getOwner() == null) { + return; + } + // Capture all main-thread state before going async. + ChunkSnapshot snapshot = chunk.getChunkSnapshot(); + ScanContext ctx = new ScanContext(world, chunk.getX() << 4, chunk.getZ() << 4, + world.getMinHeight(), world.getMaxHeight(), + island.getMinProtectedX(), island.getMaxProtectedX(), + island.getMinProtectedZ(), island.getMaxProtectedZ(), + addon.getPlugin().getIWM().getSeaHeight(world), + addon.getSettings().getUnderWaterMultiplier()); + + Bukkit.getScheduler().runTaskAsynchronously(addon.getPlugin(), () -> { + long total = scanSnapshot(snapshot, ctx); + if (total != 0L) { + Bukkit.getScheduler().runTask(addon.getPlugin(), + () -> addon.getManager().addToInitialCount(island, total)); + } + }); + } + + private long scanSnapshot(ChunkSnapshot snapshot, ScanContext ctx) { + long total = 0L; + for (int x = 0; x < 16; x++) { + int globalX = ctx.chunkBlockX + x; + if (globalX >= ctx.minProtectedX && globalX < ctx.maxProtectedX) { + total += scanRow(snapshot, x, ctx); + } + } + return total; + } + + private long scanRow(ChunkSnapshot snapshot, int x, ScanContext ctx) { + long total = 0L; + for (int z = 0; z < 16; z++) { + int globalZ = ctx.chunkBlockZ + z; + if (globalZ >= ctx.minProtectedZ && globalZ < ctx.maxProtectedZ) { + total += scanColumn(snapshot, x, z, ctx); + } + } + return total; + } + + private long scanColumn(ChunkSnapshot snapshot, int x, int z, ScanContext ctx) { + long total = 0L; + for (int y = ctx.minHeight; y < ctx.maxHeight; y++) { + total += valueAt(snapshot, x, y, z, ctx); + } + return total; + } + + private long valueAt(ChunkSnapshot snapshot, int x, int y, int z, ScanContext ctx) { + Material mat = snapshot.getBlockType(x, y, z); + if (mat.isAir()) { + return 0L; + } + Integer value = addon.getBlockConfig().getValue(ctx.world, mat); + if (value == null || value == 0) { + return 0L; + } + if (ctx.seaHeight > 0 && y <= ctx.seaHeight) { + return (long) (value * ctx.underwaterMultiplier); + } + return value; + } +}