From b0131965996be154edf7824475ff66ff25e4e710 Mon Sep 17 00:00:00 2001 From: tastybento Date: Tue, 5 May 2026 18:30:03 -0700 Subject: [PATCH 01/24] Separate block and entity limits per dimension (fixes #43) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Track block counts, entity counts, limits, and offsets independently per World.Environment so overworld, nether, and end no longer share a single count. Counts are now persistent rather than derived from getNearbyEntities, so they stay accurate when nether/end chunks are unloaded — the original bug in #43. - IslandBlockCount: every map keyed by Environment first; legacy data migrates lazily into Environment.NORMAL on first access. - BlockLimitsListener: env-aware tracking; resolution priority is island-env, world-named, env-default. New blocklimits-nether and blocklimits-end config sections override the env default for one env each. - EntityLimitListener: persistent per-env counts maintained via spawn-handler increments and EntityRemoveEvent decrements (UNLOAD cause excluded so chunk unload doesn't drop the count). EntityPortalEvent migrates counts between envs to prevent the obvious portal exploit. - JoinListener: permission format extended. 5-segment perms still apply the limit to all envs independently; new 6-segment form '.island.limit...' targets one env (overworld/nether/end). - LimitPanel: one tab per env (skipped for envs the gamemode doesn't generate). LimitTab reads counts from the IBC. - Placeholders: env-suffixed variants added (_overworld_count etc.); the unsuffixed forms remain as sums for back-compat. - RecountCalculator: rebuilds counts per-env, including a loaded-entity scan to seed migrated servers' nether/end entity counts. - Tests: 230/230 passing, including new coverage for migration, env-prefixed perms, env-isolated counts, and per-env panel display. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 42 +- README.md | 40 +- .../java/world/bentobox/limits/Limits.java | 575 ++++----- .../java/world/bentobox/limits/Settings.java | 440 ++++--- .../limits/calculators/RecountCalculator.java | 637 ++++----- .../bentobox/limits/calculators/Results.java | 111 +- .../limits/commands/admin/OffsetCommand.java | 32 +- .../limits/commands/player/LimitPanel.java | 166 +-- .../limits/commands/player/LimitTab.java | 500 ++++---- .../limits/listeners/BlockLimitsListener.java | 1141 ++++++++--------- .../limits/listeners/EntityLimitListener.java | 455 +++---- .../limits/listeners/JoinListener.java | 663 +++++----- .../objects/EnvNamespacedKeyMapAdapter.java | 116 ++ .../limits/objects/IslandBlockCount.java | 931 +++++++------- src/main/resources/config.yml | 306 +++-- src/main/resources/locales/en-US.yml | 9 +- .../bentobox/limits/JoinListenerTest.java | 116 +- .../world/bentobox/limits/LimitsTest.java | 2 +- .../world/bentobox/limits/SettingsTest.java | 11 +- .../limits/calculators/ResultsTest.java | 11 +- .../commands/player/LimitPanelTest.java | 5 +- .../limits/commands/player/LimitTabTest.java | 192 ++- .../listeners/BlockLimitsListenerTest.java | 143 +-- .../listeners/EntityLimitListenerTest.java | 98 +- .../limits/objects/IslandBlockCountTest.java | 189 ++- 25 files changed, 3585 insertions(+), 3346 deletions(-) create mode 100644 src/main/java/world/bentobox/limits/objects/EnvNamespacedKeyMapAdapter.java diff --git a/CLAUDE.md b/CLAUDE.md index 73092fc..5a00e33 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,15 +36,34 @@ Limits is a **BentoBox addon** (not a standalone Spigot plugin). It depends on B 4. Registers three listeners: `BlockLimitsListener`, `JoinListener`, `EntityLimitListener` 5. Registers PlaceholderAPI placeholders for each material and entity type -### Three-Tier Limit System +### Per-Environment Tracking (since #43) -Limits are resolved in a priority hierarchy (highest wins): +Block counts, entity counts, limits, and offsets are all tracked **per `World.Environment`** (overworld, nether, end). The data model in `IslandBlockCount` keys every map by `Environment` first, then by material/entity. Pre-env data on disk is migrated into `Environment.NORMAL` lazily on first access. -1. **Island-specific** (`IslandBlockCount.blockLimits` / `entityLimits`): set by player permissions, checked at join -2. **World-specific** (`BlockLimitsListener.worldLimitMap`): from `config.yml` `worlds:` section -3. **Default** (`BlockLimitsListener.defaultLimitMap`): from `config.yml` `blocklimits:` section +This means: +- Counts in nether and end are persistent — they don't go to zero when chunks unload (the original bug in #43). +- A single config-defined limit applies independently to each env (`HOPPER: 10` allows 10 in overworld, 10 in nether, 10 in end). +- Entity portal teleports decrement the source-env count and increment the destination-env count via `EntityPortalEvent`. -**Offsets** (`blockLimitsOffset`, `entityLimitsOffset`) are added on top of any limit found — used by the `offset` admin command to give per-island bonus allowances. +### Limit Resolution Order + +For a block placement (or entity spawn) in environment `env`, the limit is resolved in priority order: + +1. **Island-specific env limit** (`IslandBlockCount.envBlockLimits[env]`) — set by player permissions, checked at join. +2. **World-specific limit** (`BlockLimitsListener.worldLimitMap[world]`) — from the `worlds:` section. Each world is one env so this is implicitly env-scoped. +3. **Env default** (`BlockLimitsListener.envDefaultLimitMap[env]`) — from `blocklimits` (seeded into all envs) plus `blocklimits-nether` / `blocklimits-end` overrides. + +**Offsets** (`envBlockLimitsOffset[env]`, `envEntityLimitsOffset[env]`) are added on top of any resolved limit. The legacy `/offset` admin command sets the same offset across all envs via the `setBlockLimitsOffsetAllEnvs` / `setEntityLimitsOffsetAllEnvs` helpers. + +### Persistent Entity Counts + +`EntityLimitListener` no longer counts entities via `World.getNearbyEntities(island.getBoundingBox())`. Instead it maintains the count in `IslandBlockCount.envEntityCounts`: + +- Increment on MONITOR-priority handlers for `CreatureSpawnEvent`, `VehicleCreateEvent`, `HangingPlaceEvent` (after our LOW-priority limit check has had its say). +- Decrement on `EntityRemoveEvent` whenever the cause is **not** `UNLOAD` (so death, despawn, drop, plugin removal, etc. all decrement; chunk unload does not). +- `EntityPortalEvent` (MONITOR) handles env-to-env teleport: decrement source env, increment destination env. + +This is Paper-only (the addon dropped Spigot support; `EntityRemoveEvent` is on Bukkit's API now but `EntityRemoveEvent.Cause` is needed). ### Key Classes @@ -52,7 +71,8 @@ Limits are resolved in a priority hierarchy (highest wins): |---|---| | `Limits` | Main addon; wires everything together, registers placeholders | | `Settings` | Parses `config.yml`; holds entity limits, entity group limits, game mode list | -| `IslandBlockCount` | Per-island data object stored in BentoBox's database (`@Table("IslandBlockCount")`); tracks block counts, per-island block/entity limits, and offsets | +| `IslandBlockCount` | Per-island data object stored in BentoBox's database (`@Table("IslandBlockCount")`); tracks block counts, entity counts, limits, and offsets — all keyed by `Environment` | +| `EnvNamespacedKeyMapAdapter` | Gson type adapter for `Map>` — needed because `NamespacedKey` is not enum-keyed and Gson can't handle it natively | | `BlockLimitsListener` | Core block tracking listener; handles all block place/break/grow/explode events; maintains the `islandCountMap` in memory; persists via BentoBox `Database` | | `JoinListener` | Applies permission-based limits on player join, island creation/reset, and ownership change; fires `LimitsPermCheckEvent` / `LimitsJoinPermCheckEvent` for external cancellation | | `EntityLimitListener` | Cancels entity spawn/breed/vehicle-create events when entity or group limits are exceeded; handles async golem/snowman spawning edge cases | @@ -60,11 +80,11 @@ Limits are resolved in a priority hierarchy (highest wins): ### Permission-Based Limits -Permission format: `.island.limit..` - -Example: `bskyblock.island.limit.HOPPER.20` +Two formats: +- 5-segment, all-env: `.island.limit..` — applied independently to overworld, nether, and end. +- 6-segment, env-scoped: `.island.limit...` where `` ∈ `{overworld, nether, end}`. -`JoinListener.checkPerms()` clears all permission-based limits then re-applies them from scratch on each join. The highest value wins if multiple permissions grant the same limit. Wildcards are not supported. +`JoinListener.checkPerms()` clears all permission-based limits then re-applies them from scratch on each join. The highest value wins if multiple permissions grant the same limit for the same env. Wildcards are not supported. ### Block Material Normalization diff --git a/README.md b/README.md index 69231dd..d4a1f1a 100644 --- a/README.md +++ b/README.md @@ -19,32 +19,54 @@ There is a user command and an admin command called "limits". Admins can check t The config.yml has the following sections: -* blocklimits -* worlds -* entitylimits +* `blocklimits`, `blocklimits-nether`, `blocklimits-end` +* `worlds` +* `entitylimits`, `entitylimits-nether`, `entitylimits-end` +* `entitygrouplimits`, `entitygrouplimits-nether`, `entitygrouplimits-end` + +### Per-environment separation + +Limits are tracked independently in the overworld, nether, and end. A `HOPPER: 10` limit means the player can place up to 10 hoppers in **each** of overworld, nether, and end — 30 hoppers total across the island. Likewise for entities and entity groups. + +If you want a different limit in nether or end, add a corresponding `*-nether` or `*-end` section that overrides the env-default for those entries only. ### blocklimits -This section lists the maximum number of blocks allowed for each block material. Do not use non-block materials because they will not work. The limits apply to all game worlds. +The base block-limit section. Values here apply to every environment unless overridden by `blocklimits-nether` / `blocklimits-end` or by a `worlds.` entry. ### worlds -This section lists block limits for specific worlds. You must name the world specifically, e.g. AcidIsland_world and then list the materials and the limit. +Per-world overrides. Name the world specifically (e.g. `AcidIsland_world`) and list the materials and limits. A world entry overrides the env-default for that exact world. ### entitylimits -Coming soon! +Default entity-type limits applied independently per environment. Override per env via `entitylimits-nether` / `entitylimits-end`. + +### entitygrouplimits + +Define named entity groups (icon, member set, default limit). Override the limit value per env with `entitygrouplimits-nether` / `entitygrouplimits-end`. The icon and member set are fixed in the base section — only the numeric limit can be overridden per env. ## Permissions -Island owners can have exclusive permissions that override the default or world settings. The format is: +Island owners can have exclusive permissions that override the default or world settings. Two formats are supported: -Format is `GAME-MODE-NAME.island.limit.MATERIAL.LIMIT` +* All-environment: `.island.limit..` — applies the limit independently to every environment. +* Per-environment: `.island.limit...` — applies only to one environment, where `` is `overworld`, `nether`, or `end`. -example: `bskyblock.island.limit.hopper.10` +Examples: + +``` +bskyblock.island.limit.hopper.10 # 10 hoppers in each env +bskyblock.island.limit.nether.hopper.5 # only 5 hoppers in nether +bskyblock.island.limit.end.enderman.50 # only 50 endermen in the end +``` Permissions activate when the player logs in. +### Migrating an existing server + +When upgrading from a pre-env version of Limits, existing per-island block counts are migrated to the overworld slot on first load. Counts in the nether/end will start at zero and be populated as entities and blocks change. Run `/ admin limits calc ` to fully recount any island and redistribute its counts across environments. + Usage permissions are (put the gamemode name, e.g. acidisland at the front): ``` diff --git a/src/main/java/world/bentobox/limits/Limits.java b/src/main/java/world/bentobox/limits/Limits.java index 68129d2..a56052e 100644 --- a/src/main/java/world/bentobox/limits/Limits.java +++ b/src/main/java/world/bentobox/limits/Limits.java @@ -1,316 +1,259 @@ -package world.bentobox.limits; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -import org.bukkit.Material; -import org.bukkit.NamespacedKey; -import org.bukkit.Registry; -import org.bukkit.World; -import org.bukkit.entity.Entity; -import org.bukkit.entity.EntityType; -import org.eclipse.jdt.annotation.Nullable; - -import world.bentobox.bentobox.api.addons.Addon; -import world.bentobox.bentobox.api.addons.GameModeAddon; -import world.bentobox.bentobox.api.user.User; -import world.bentobox.bentobox.database.objects.Island; -import world.bentobox.bentobox.managers.IslandWorldManager; -import world.bentobox.limits.commands.admin.AdminCommand; -import world.bentobox.limits.commands.player.PlayerCommand; -import world.bentobox.limits.listeners.BlockLimitsListener; -import world.bentobox.limits.listeners.EntityLimitListener; -import world.bentobox.limits.listeners.JoinListener; -import world.bentobox.limits.listeners.PaperShulkerLimitListener; -import world.bentobox.limits.objects.IslandBlockCount; - - -/** - * Addon to BentoBox that monitors and enforces limits - * - * @author tastybento - */ -public class Limits extends Addon { - - private static final String LIMIT_NOT_SET = "Limit not set"; - private static final String ISLAND_PLACEHOLDER = "_island_"; - private Settings settings; - private List gameModes = new ArrayList<>(); - private BlockLimitsListener blockLimitListener; - private JoinListener joinListener; - private IslandWorldManager islandWorldManager; - - @Override - public void onDisable() { - if (blockLimitListener != null) { - blockLimitListener.save(); - } - } - - @Override - public void onEnable() { - // Load the plugin's config - saveDefaultConfig(); - this.islandWorldManager = getPlugin().getIWM(); - // Load settings - settings = new Settings(this); - // Register worlds from GameModes - gameModes = getPlugin().getAddonsManager().getGameModeAddons().stream() - .filter(gm -> settings.getGameModes().contains(gm.getDescription().getName())) - .collect(Collectors.toList()); - gameModes.forEach(gm -> - { - // Register commands - gm.getAdminCommand().ifPresent(a -> new AdminCommand(this, a)); - gm.getPlayerCommand().ifPresent(a -> new PlayerCommand(this, a)); - registerPlaceholders(gm); - log("Limits will apply to " + gm.getDescription().getName()); - } - ); - // Register listener - blockLimitListener = new BlockLimitsListener(this); - registerListener(blockLimitListener); - joinListener = new JoinListener(this); - registerListener(joinListener); - EntityLimitListener entityLimitListener = new EntityLimitListener(this); - registerListener(entityLimitListener); - // Register Paper-specific listener for shulker duplication limiting if running on Paper. - // ShulkerDuplicateEvent fires before the original shulker teleports, giving an accurate - // entity count. CreatureSpawnEvent fires after the teleport, so the original shulker may - // have already left the island bounding box, causing the count to be off by one. - try { - Class.forName("io.papermc.paper.event.entity.ShulkerDuplicateEvent"); - registerListener(new PaperShulkerLimitListener(this, entityLimitListener)); - log("Paper detected: shulker duplication limiting active via ShulkerDuplicateEvent."); - } catch (ClassNotFoundException e) { - // Not running on Paper; CreatureSpawnEvent handles duplication on Spigot. - } - // Done - } - - /** - * @return the settings - */ - public Settings getSettings() { - return settings; - } - - /** - * @return the gameModes - */ - public List getGameModes() { - return gameModes; - } - - /** - * @return the blockLimitListener - */ - public BlockLimitsListener getBlockLimitListener() { - return blockLimitListener; - } - - /** - * Checks if this world is covered by the activated game modes - * - * @param world - world - * @return true or false - */ - public boolean inGameModeWorld(World world) { - return gameModes.stream().anyMatch(gm -> gm.inWorld(world)); - } - - /** - * Get the name of the game mode for this world - * - * @param world - world - * @return game mode name or empty string if none - */ - public String getGameModeName(World world) { - return gameModes.stream().filter(gm -> gm.inWorld(world)).findFirst().map(gm -> gm.getDescription().getName()).orElse(""); - } - - /** - * Get the permission prefix for this world - * - * @param world - world - * @return permisdsion prefix or empty string if none - */ - public String getGameModePermPrefix(World world) { - return gameModes.stream().filter(gm -> gm.inWorld(world)).findFirst().map(GameModeAddon::getPermissionPrefix).orElse(""); - } - - - /** - * Check if any of the game modes covered have this name - * - * @param gameMode - name of game mode - * @return true or false - */ - public boolean isCoveredGameMode(String gameMode) { - return gameModes.stream().anyMatch(gm -> gm.getDescription().getName().equals(gameMode)); - } - - /** - * @return the joinListener - */ - public JoinListener getJoinListener() { - return joinListener; - } - - private void registerPlaceholders(GameModeAddon gm) { - if (getPlugin().getPlaceholdersManager() == null) return; - Registry.MATERIAL.stream() - .filter(Material::isBlock) - .forEach(m -> registerCountAndLimitPlaceholders(m.getKey(), gm)); - - Arrays.stream(EntityType.values()) - .forEach(e -> registerCountAndLimitPlaceholders(e, gm)); - } - - /** - * Registers placeholders for the count and limit of the material - * in the format of %Limits_(gamemode prefix)_island_(lowercase material name)_count% - * and %Limits_(gamemode prefix)_island_(lowercase material name)_limit% - *

- * Example: registerCountAndLimitPlaceholders("HOPPER", gm); - * Placeholders: - * "Limits_bskyblock_island_hopper_count" - * "Limits_bskyblock_island_hopper_limit" - * "Limits_bskyblock_island_hopper_base_limit" - * "Limits_bskyblock_island_zombie_limit" - * - * @param m material - * @param gm game mode - */ - private void registerCountAndLimitPlaceholders(NamespacedKey m, GameModeAddon gm) { - getPlugin().getPlaceholdersManager().registerPlaceholder(this, - gm.getDescription().getName().toLowerCase() + ISLAND_PLACEHOLDER + m.toString().toLowerCase() + "_count", - user -> String.valueOf(getCount(user, m, gm))); - getPlugin().getPlaceholdersManager().registerPlaceholder(this, - gm.getDescription().getName().toLowerCase() + ISLAND_PLACEHOLDER + m.toString().toLowerCase() + "_limit", - user -> getLimit(user, m, gm)); - getPlugin().getPlaceholdersManager().registerPlaceholder(this, - gm.getDescription().getName().toLowerCase() + ISLAND_PLACEHOLDER + m.toString().toLowerCase() + "_base_limit", - user -> getBaseLimit(user, m, gm)); - } - - private void registerCountAndLimitPlaceholders(EntityType e, GameModeAddon gm) { - getPlugin().getPlaceholdersManager().registerPlaceholder(this, - gm.getDescription().getName().toLowerCase() + ISLAND_PLACEHOLDER + e.toString().toLowerCase() + "_limit", - user -> getLimit(user, e, gm)); - getPlugin().getPlaceholdersManager().registerPlaceholder(this, - gm.getDescription().getName().toLowerCase() + ISLAND_PLACEHOLDER + e.toString().toLowerCase() + "_base_limit", - user -> getBaseLimit(user, e, gm)); - getPlugin().getPlaceholdersManager().registerPlaceholder(this, - gm.getDescription().getName().toLowerCase() + ISLAND_PLACEHOLDER + e.toString().toLowerCase() + "_count", - user -> String.valueOf(getCount(user, e, gm))); - } - - /** - * @param user - Used to identify the island the user belongs to - * @param m - The material we are trying to count on the island - * @param gm Game Mode Addon - * @return Number of blocks of the specified material on the given user's island - */ - private int getCount(@Nullable User user, NamespacedKey m, GameModeAddon gm) { - Island is = gm.getIslands().getIsland(gm.getOverWorld(), user); - if (is == null) { - return 0; - } - @Nullable IslandBlockCount ibc = getBlockLimitListener().getIsland(is.getUniqueId()); - if (ibc == null) { - return 0; - } - return ibc.getBlockCount(m); - } - - private long getCount(@Nullable User user, EntityType e, GameModeAddon gm) { - Island is = gm.getIslands().getIsland(gm.getOverWorld(), user); - if (is == null || e.getEntityClass() == null) { - return 0; - } - Class entityClass = e.getEntityClass(); - long count = is.getWorld().getEntitiesByClass(entityClass).stream() - .filter(ent -> is.inIslandSpace(ent.getLocation())).count(); - /* NETHER */ - if (islandWorldManager.isNetherIslands(is.getWorld()) && islandWorldManager.getNetherWorld(is.getWorld()) != null) { - count += islandWorldManager.getNetherWorld(is.getWorld()).getEntitiesByClass(entityClass).stream() - .filter(ent -> is.inIslandSpace(ent.getLocation())).count(); - } - /* END */ - if (islandWorldManager.isEndIslands(is.getWorld()) && islandWorldManager.getEndWorld(is.getWorld()) != null) { - count += islandWorldManager.getEndWorld(is.getWorld()).getEntitiesByClass(entityClass).stream() - .filter(ent -> is.inIslandSpace(ent.getLocation())).count(); - } - return count; - } - - - /** - * @param user - Used to identify the island the user belongs to - * @param m - The material whose limit we are querying - * @param gm Game Mode Addon - * @return The limit of the specified material on the given user's island - */ - private String getLimit(@Nullable User user, NamespacedKey m, GameModeAddon gm) { - Island is = gm.getIslands().getIsland(gm.getOverWorld(), user); - if (is == null) { - return LIMIT_NOT_SET; - } - if (user != null) { - // Check the permissions of the user and update - this.getJoinListener().checkPerms(user.getPlayer(), gm.getPermissionPrefix() + "island.limit.", - is.getUniqueId(), gm.getDescription().getName()); - } - int limit = this.getBlockLimitListener(). - getMaterialLimits(is.getWorld(), is.getUniqueId()).getOrDefault(m, -1); - - return limit == -1 ? LIMIT_NOT_SET : String.valueOf(limit); - } - - private String getBaseLimit(@Nullable User user, NamespacedKey m, GameModeAddon gm) { - Island is = gm.getIslands().getIsland(gm.getOverWorld(), user); - if (is == null) { - return LIMIT_NOT_SET; - } - - int limit = this.getBlockLimitListener(). - getMaterialLimits(is.getWorld(), is.getUniqueId()). - getOrDefault(m, -1); - - if (limit > 0) { - limit -= this.getBlockLimitListener().getIsland(is).getBlockLimitOffset(m); - } - - return limit == -1 ? LIMIT_NOT_SET : String.valueOf(limit); - } - - private String getLimit(@Nullable User user, EntityType e, GameModeAddon gm) { - Island is = gm.getIslands().getIsland(gm.getOverWorld(), user); - if (is == null) { - return LIMIT_NOT_SET; - } - - int limit = this.getBlockLimitListener().getIsland(is).getEntityLimit(e); - if (limit < 0 && this.getSettings().getLimits().containsKey(e)) { - limit = this.getSettings().getLimits().get(e); - } - - return limit == -1 ? LIMIT_NOT_SET : String.valueOf(limit); - } - - private String getBaseLimit(@Nullable User user, EntityType e, GameModeAddon gm) { - Island is = gm.getIslands().getIsland(gm.getOverWorld(), user); - if (is == null || !this.getSettings().getLimits().containsKey(e)) { - return LIMIT_NOT_SET; - } - - int limit = this.getSettings().getLimits().get(e); - - return limit == -1 ? LIMIT_NOT_SET : String.valueOf(limit); - } - - -} +package world.bentobox.limits; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; + +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.World; +import org.bukkit.World.Environment; +import org.bukkit.entity.EntityType; +import org.eclipse.jdt.annotation.Nullable; + +import world.bentobox.bentobox.api.addons.Addon; +import world.bentobox.bentobox.api.addons.GameModeAddon; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.bentobox.managers.IslandWorldManager; +import world.bentobox.limits.commands.admin.AdminCommand; +import world.bentobox.limits.commands.player.PlayerCommand; +import world.bentobox.limits.listeners.BlockLimitsListener; +import world.bentobox.limits.listeners.EntityLimitListener; +import world.bentobox.limits.listeners.JoinListener; +import world.bentobox.limits.listeners.PaperShulkerLimitListener; +import world.bentobox.limits.objects.IslandBlockCount; + +/** + * Addon to BentoBox that monitors and enforces limits. + * + * @author tastybento + */ +public class Limits extends Addon { + + private static final String LIMIT_NOT_SET = "Limit not set"; + private static final String ISLAND_PLACEHOLDER = "_island_"; + private Settings settings; + private List gameModes = new ArrayList<>(); + private BlockLimitsListener blockLimitListener; + private JoinListener joinListener; + private IslandWorldManager islandWorldManager; + + @Override + public void onDisable() { + if (blockLimitListener != null) { + blockLimitListener.save(); + } + } + + @Override + public void onEnable() { + saveDefaultConfig(); + this.islandWorldManager = getPlugin().getIWM(); + settings = new Settings(this); + gameModes = getPlugin().getAddonsManager().getGameModeAddons().stream() + .filter(gm -> settings.getGameModes().contains(gm.getDescription().getName())) + .collect(Collectors.toList()); + gameModes.forEach(gm -> { + gm.getAdminCommand().ifPresent(a -> new AdminCommand(this, a)); + gm.getPlayerCommand().ifPresent(a -> new PlayerCommand(this, a)); + registerPlaceholders(gm); + log("Limits will apply to " + gm.getDescription().getName()); + }); + blockLimitListener = new BlockLimitsListener(this); + registerListener(blockLimitListener); + joinListener = new JoinListener(this); + registerListener(joinListener); + EntityLimitListener entityLimitListener = new EntityLimitListener(this); + registerListener(entityLimitListener); + try { + Class.forName("io.papermc.paper.event.entity.ShulkerDuplicateEvent"); + registerListener(new PaperShulkerLimitListener(this, entityLimitListener)); + log("Paper detected: shulker duplication limiting active via ShulkerDuplicateEvent."); + } catch (ClassNotFoundException e) { + // Not running on Paper; CreatureSpawnEvent handles duplication on Spigot. + } + } + + public Settings getSettings() { + return settings; + } + + public List getGameModes() { + return gameModes; + } + + public BlockLimitsListener getBlockLimitListener() { + return blockLimitListener; + } + + public boolean inGameModeWorld(World world) { + return gameModes.stream().anyMatch(gm -> gm.inWorld(world)); + } + + public String getGameModeName(World world) { + return gameModes.stream().filter(gm -> gm.inWorld(world)).findFirst() + .map(gm -> gm.getDescription().getName()).orElse(""); + } + + public String getGameModePermPrefix(World world) { + return gameModes.stream().filter(gm -> gm.inWorld(world)).findFirst() + .map(GameModeAddon::getPermissionPrefix).orElse(""); + } + + public boolean isCoveredGameMode(String gameMode) { + return gameModes.stream().anyMatch(gm -> gm.getDescription().getName().equals(gameMode)); + } + + public JoinListener getJoinListener() { + return joinListener; + } + + /* ========================================================================= + * Placeholders + * ========================================================================= */ + + private void registerPlaceholders(GameModeAddon gm) { + if (getPlugin().getPlaceholdersManager() == null) return; + Registry.MATERIAL.stream() + .filter(Material::isBlock) + .forEach(m -> registerCountAndLimitPlaceholders(m.getKey(), gm)); + Arrays.stream(EntityType.values()) + .forEach(e -> registerCountAndLimitPlaceholders(e, gm)); + } + + private static final List ENV_SUFFIXES = List.of("", "_overworld", "_nether", "_end"); + + /** + * Registers placeholders for the count and limit of the material. + * + *

Env-aware naming: + *

    + *
  • {@code Limits__island__count} — total across all envs (back-compat)
  • + *
  • {@code Limits__island__overworld_count} — overworld only
  • + *
  • {@code Limits__island__nether_count} — nether only
  • + *
  • {@code Limits__island__end_count} — end only
  • + *
+ * Same pattern for {@code _limit} and {@code _base_limit}. + */ + private void registerCountAndLimitPlaceholders(NamespacedKey m, GameModeAddon gm) { + for (String suffix : ENV_SUFFIXES) { + Environment env = envForSuffix(suffix); + String base = gm.getDescription().getName().toLowerCase(Locale.ROOT) + ISLAND_PLACEHOLDER + + m.toString().toLowerCase(Locale.ROOT); + getPlugin().getPlaceholdersManager().registerPlaceholder(this, + base + suffix + "_count", + user -> String.valueOf(getCount(user, m, gm, env))); + getPlugin().getPlaceholdersManager().registerPlaceholder(this, + base + suffix + "_limit", + user -> getLimit(user, m, gm, env)); + getPlugin().getPlaceholdersManager().registerPlaceholder(this, + base + suffix + "_base_limit", + user -> getBaseLimit(user, m, gm, env)); + } + } + + private void registerCountAndLimitPlaceholders(EntityType e, GameModeAddon gm) { + for (String suffix : ENV_SUFFIXES) { + Environment env = envForSuffix(suffix); + String base = gm.getDescription().getName().toLowerCase(Locale.ROOT) + ISLAND_PLACEHOLDER + + e.toString().toLowerCase(Locale.ROOT); + getPlugin().getPlaceholdersManager().registerPlaceholder(this, + base + suffix + "_count", + user -> String.valueOf(getCount(user, e, gm, env))); + getPlugin().getPlaceholdersManager().registerPlaceholder(this, + base + suffix + "_limit", + user -> getLimit(user, e, gm, env)); + getPlugin().getPlaceholdersManager().registerPlaceholder(this, + base + suffix + "_base_limit", + user -> getBaseLimit(user, e, gm, env)); + } + } + + /** {@code null} env means "sum/aggregate across all envs". */ + @Nullable + private static Environment envForSuffix(String suffix) { + return switch (suffix) { + case "_overworld" -> Environment.NORMAL; + case "_nether" -> Environment.NETHER; + case "_end" -> Environment.THE_END; + default -> null; + }; + } + + private int getCount(@Nullable User user, NamespacedKey m, GameModeAddon gm, @Nullable Environment env) { + Island is = gm.getIslands().getIsland(gm.getOverWorld(), user); + if (is == null) return 0; + IslandBlockCount ibc = getBlockLimitListener().getIsland(is.getUniqueId()); + if (ibc == null) return 0; + return env == null ? ibc.getBlockCount(m) : ibc.getBlockCount(env, m); + } + + private long getCount(@Nullable User user, EntityType e, GameModeAddon gm, @Nullable Environment env) { + Island is = gm.getIslands().getIsland(gm.getOverWorld(), user); + if (is == null) return 0; + IslandBlockCount ibc = getBlockLimitListener().getIsland(is.getUniqueId()); + if (ibc == null) return 0; + return env == null ? ibc.getEntityCount(e) : ibc.getEntityCount(env, e); + } + + private String getLimit(@Nullable User user, NamespacedKey m, GameModeAddon gm, @Nullable Environment env) { + Island is = gm.getIslands().getIsland(gm.getOverWorld(), user); + if (is == null) return LIMIT_NOT_SET; + if (user != null) { + getJoinListener().checkPerms(user.getPlayer(), gm.getPermissionPrefix() + "island.limit.", + is.getUniqueId(), gm.getDescription().getName()); + } + World w = worldForEnv(gm, env); + int limit = getBlockLimitListener().getMaterialLimits(w, is.getUniqueId()).getOrDefault(m, -1); + return limit == -1 ? LIMIT_NOT_SET : String.valueOf(limit); + } + + private String getBaseLimit(@Nullable User user, NamespacedKey m, GameModeAddon gm, @Nullable Environment env) { + Island is = gm.getIslands().getIsland(gm.getOverWorld(), user); + if (is == null) return LIMIT_NOT_SET; + World w = worldForEnv(gm, env); + int limit = getBlockLimitListener().getMaterialLimits(w, is.getUniqueId()).getOrDefault(m, -1); + if (limit > 0) { + IslandBlockCount ibc = getBlockLimitListener().getIsland(is); + int offset = ibc.getBlockLimitOffset(env == null ? Environment.NORMAL : env, m); + limit -= offset; + } + return limit == -1 ? LIMIT_NOT_SET : String.valueOf(limit); + } + + private String getLimit(@Nullable User user, EntityType e, GameModeAddon gm, @Nullable Environment env) { + Island is = gm.getIslands().getIsland(gm.getOverWorld(), user); + if (is == null) return LIMIT_NOT_SET; + IslandBlockCount ibc = getBlockLimitListener().getIsland(is); + Environment effective = env == null ? Environment.NORMAL : env; + int limit = ibc.getEntityLimit(effective, e); + if (limit < 0) { + Map envLimits = getSettings().getLimits(effective); + if (envLimits.containsKey(e)) limit = envLimits.get(e); + } + return limit == -1 ? LIMIT_NOT_SET : String.valueOf(limit); + } + + private String getBaseLimit(@Nullable User user, EntityType e, GameModeAddon gm, @Nullable Environment env) { + Island is = gm.getIslands().getIsland(gm.getOverWorld(), user); + Environment effective = env == null ? Environment.NORMAL : env; + Map envLimits = getSettings().getLimits(effective); + if (is == null || !envLimits.containsKey(e)) return LIMIT_NOT_SET; + int limit = envLimits.get(e); + return limit == -1 ? LIMIT_NOT_SET : String.valueOf(limit); + } + + private World worldForEnv(GameModeAddon gm, @Nullable Environment env) { + if (env == null) return gm.getOverWorld(); + return switch (env) { + case NETHER -> gm.getNetherWorld() != null ? gm.getNetherWorld() : gm.getOverWorld(); + case THE_END -> gm.getEndWorld() != null ? gm.getEndWorld() : gm.getOverWorld(); + default -> gm.getOverWorld(); + }; + } +} diff --git a/src/main/java/world/bentobox/limits/Settings.java b/src/main/java/world/bentobox/limits/Settings.java index 1d5251f..05b7b22 100644 --- a/src/main/java/world/bentobox/limits/Settings.java +++ b/src/main/java/world/bentobox/limits/Settings.java @@ -1,184 +1,256 @@ -package world.bentobox.limits; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.EnumMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; - -import org.bukkit.Material; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.entity.EntityType; - -public class Settings { - - enum GeneralGroup { - ANIMALS, MOBS - } - - private final Map general = new EnumMap<>(GeneralGroup.class); - private final Map limits = new EnumMap<>(EntityType.class); - private final Map> groupLimits = new EnumMap<>(EntityType.class); - private final List gameModes; - private final boolean logLimitsOnJoin; - private final boolean asyncGolums; - private static final List DISALLOWED = Arrays.asList( - EntityType.TNT, - EntityType.EVOKER_FANGS, - EntityType.LLAMA_SPIT, - EntityType.DRAGON_FIREBALL, - EntityType.AREA_EFFECT_CLOUD, - EntityType.END_CRYSTAL, - EntityType.SMALL_FIREBALL, - EntityType.FIREBALL, - EntityType.EXPERIENCE_BOTTLE, - EntityType.EXPERIENCE_ORB, - EntityType.SHULKER_BULLET, - EntityType.WITHER_SKULL, - EntityType.TRIDENT, - EntityType.ARROW, - EntityType.SPECTRAL_ARROW, - EntityType.SNOWBALL, - EntityType.EGG, - EntityType.LEASH_KNOT, - EntityType.GIANT, - EntityType.ENDER_PEARL, - EntityType.ENDER_DRAGON, - EntityType.ITEM_FRAME, - EntityType.PAINTING); - - public Settings(Limits addon) { - - // GameModes - gameModes = addon.getConfig().getStringList("gamemodes"); - - ConfigurationSection el = addon.getConfig().getConfigurationSection("entitylimits"); - if (el != null) { - for (String key : el.getKeys(false)) { - if (key.equalsIgnoreCase("ANIMALS")) { - general.put(GeneralGroup.ANIMALS, el.getInt(key, 0)); - } else if (key.equalsIgnoreCase("MOBS")) { - general.put(GeneralGroup.MOBS, el.getInt(key, 0)); - } else { - EntityType type = getType(key); - if (type != null) { - if (DISALLOWED.contains(type)) { - addon.logError("Entity type: " + key + " is not supported - skipping..."); - } else { - limits.put(type, el.getInt(key, 0)); - } - } else { - addon.logError("Unknown entity type: " + key + " - skipping..."); - } - } - } - } - // Log limits on join - logLimitsOnJoin = addon.getConfig().getBoolean("log-limits-on-join", true); - // Async Golums - asyncGolums = addon.getConfig().getBoolean("async-golums", true); - - addon.log("Entity limits:"); - limits.entrySet().stream().map(e -> "Limit " + e.getKey().toString() + " to " + e.getValue()).forEach(addon::log); - - //group limits - el = addon.getConfig().getConfigurationSection("entitygrouplimits"); - if (el != null) { - for (String name : el.getKeys(false)) { - int limit = el.getInt(name + ".limit"); - String iconName = el.getString(name + ".icon", "BARRIER"); - Material icon = Material.BARRIER; - try { - icon = Material.valueOf(iconName.toUpperCase(Locale.ENGLISH)); - } catch (Exception e) { - addon.logError("Invalid group icon name: " + iconName + ". Use a Bukkit Material."); - icon = Material.BARRIER; - } - Set entities = el.getStringList(name + ".entities").stream().map(s -> { - EntityType type = getType(s); - if (type != null) { - if (DISALLOWED.contains(type)) { - addon.logError("Entity type: " + s + " is not supported - skipping..."); - } else { - return type; - } - } else { - addon.logError("Unknown entity type: " + s + " - skipping..."); - } - return null; - }).filter(Objects::nonNull).collect(Collectors.toCollection(LinkedHashSet::new)); - if (entities.isEmpty()) - continue; - EntityGroup group = new EntityGroup(name, entities, limit, icon); - entities.forEach(e -> { - List groups = groupLimits.getOrDefault(e, new ArrayList<>()); - groups.add(group); - groupLimits.put(e, groups); - }); - } - } - - addon.log("Entity group limits:"); - getGroupLimitDefinitions().stream().map(e -> "Limit " + e.getName() + " (" + e.getTypes().stream().map(Enum::name).collect(Collectors.joining(", ")) + ") to " + e.getLimit()).forEach(addon::log); - } - - private EntityType getType(String key) { - return Arrays.stream(EntityType.values()).filter(v -> v.name().equalsIgnoreCase(key)).findFirst().orElse(null); - } - - /** - * @return the entity limits - */ - public Map getLimits() { - return Collections.unmodifiableMap(limits); - } - - /** - * @return the group limits - */ - public Map> getGroupLimits() { - return groupLimits; - } - - /** - * @return the group limit definitions - */ - public List getGroupLimitDefinitions() { - return groupLimits.values().stream().flatMap(Collection::stream).distinct().toList(); - } - - /** - * @return the gameModes - */ - public List getGameModes() { - return gameModes; - } - - /** - * @return the logLimitsOnJoin - */ - public boolean isLogLimitsOnJoin() { - return logLimitsOnJoin; - } - - /** - * @return the asyncGolums - */ - public boolean isAsyncGolums() { - return asyncGolums; - } - - /** - * @return the general coverage map - */ - public Map getGeneral() { - return general; - } -} +package world.bentobox.limits; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import org.bukkit.Material; +import org.bukkit.World.Environment; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.EntityType; + +public class Settings { + + enum GeneralGroup { + ANIMALS, MOBS + } + + /** + * Standard environments with separated limits. {@link Environment#CUSTOM} is intentionally excluded — + * limits-aware game modes target the three vanilla environments. + */ + public static final List ENVIRONMENTS = List.of(Environment.NORMAL, Environment.NETHER, + Environment.THE_END); + + private static final Map ENV_SUFFIX = Map.of( + Environment.NORMAL, "", + Environment.NETHER, "-nether", + Environment.THE_END, "-end"); + + private final Map general = new EnumMap<>(GeneralGroup.class); + /** Per-env entity type limits (env defaults from config). */ + private final Map> envLimits = new EnumMap<>(Environment.class); + /** Group definitions and which groups each entity type belongs to. */ + private final Map> groupLimits = new EnumMap<>(EntityType.class); + /** Per-env group-limit overrides (env defaults from config). */ + private final Map> envGroupLimits = new EnumMap<>(Environment.class); + private final List gameModes; + private final boolean logLimitsOnJoin; + private final boolean asyncGolums; + private static final List DISALLOWED = Arrays.asList( + EntityType.TNT, + EntityType.EVOKER_FANGS, + EntityType.LLAMA_SPIT, + EntityType.DRAGON_FIREBALL, + EntityType.AREA_EFFECT_CLOUD, + EntityType.END_CRYSTAL, + EntityType.SMALL_FIREBALL, + EntityType.FIREBALL, + EntityType.EXPERIENCE_BOTTLE, + EntityType.EXPERIENCE_ORB, + EntityType.SHULKER_BULLET, + EntityType.WITHER_SKULL, + EntityType.TRIDENT, + EntityType.ARROW, + EntityType.SPECTRAL_ARROW, + EntityType.SNOWBALL, + EntityType.EGG, + EntityType.LEASH_KNOT, + EntityType.GIANT, + EntityType.ENDER_PEARL, + EntityType.ENDER_DRAGON, + EntityType.ITEM_FRAME, + EntityType.PAINTING); + + public Settings(Limits addon) { + + // GameModes + gameModes = addon.getConfig().getStringList("gamemodes"); + + // Initialise empty env maps + for (Environment env : ENVIRONMENTS) { + envLimits.put(env, new EnumMap<>(EntityType.class)); + envGroupLimits.put(env, new java.util.HashMap<>()); + } + + // Pass 1: parse the unsuffixed entitylimits as the default for every env + loadEntityLimits(addon, "entitylimits", ENVIRONMENTS); + // Pass 2: env-suffixed overrides + loadEntityLimits(addon, "entitylimits-nether", List.of(Environment.NETHER)); + loadEntityLimits(addon, "entitylimits-end", List.of(Environment.THE_END)); + + // Log limits on join + logLimitsOnJoin = addon.getConfig().getBoolean("log-limits-on-join", true); + // Async Golums + asyncGolums = addon.getConfig().getBoolean("async-golums", true); + + addon.log("Entity limits:"); + envLimits.forEach((env, m) -> m.entrySet().stream() + .map(e -> "Limit " + e.getKey() + " in " + env + " to " + e.getValue()) + .forEach(addon::log)); + + // Group definitions live in the unsuffixed entitygrouplimits section. The entire + // group's limit is the "default for all envs"; env-suffixed sections override that + // limit per-group-name (icon and member set are not env-overridable). + loadGroupDefinitions(addon); + loadGroupLimitOverrides(addon, "entitygrouplimits-nether", Environment.NETHER); + loadGroupLimitOverrides(addon, "entitygrouplimits-end", Environment.THE_END); + + addon.log("Entity group limits:"); + getGroupLimitDefinitions().stream() + .map(e -> "Limit " + e.getName() + " (" + + e.getTypes().stream().map(Enum::name).collect(Collectors.joining(", ")) + ") to " + + e.getLimit()) + .forEach(addon::log); + } + + private void loadEntityLimits(Limits addon, String section, List targetEnvs) { + ConfigurationSection el = addon.getConfig().getConfigurationSection(section); + if (el == null) return; + for (String key : el.getKeys(false)) { + if (key.equalsIgnoreCase("ANIMALS")) { + if (targetEnvs.contains(Environment.NORMAL)) { + general.put(GeneralGroup.ANIMALS, el.getInt(key, 0)); + } + } else if (key.equalsIgnoreCase("MOBS")) { + if (targetEnvs.contains(Environment.NORMAL)) { + general.put(GeneralGroup.MOBS, el.getInt(key, 0)); + } + } else { + EntityType type = getType(key); + if (type == null) { + addon.logError("Unknown entity type in " + section + ": " + key + " - skipping..."); + } else if (DISALLOWED.contains(type)) { + addon.logError("Entity type in " + section + " not supported: " + key + " - skipping..."); + } else { + int value = el.getInt(key, 0); + targetEnvs.forEach(env -> envLimits.get(env).put(type, value)); + } + } + } + } + + private void loadGroupDefinitions(Limits addon) { + ConfigurationSection el = addon.getConfig().getConfigurationSection("entitygrouplimits"); + if (el == null) return; + for (String name : el.getKeys(false)) { + int limit = el.getInt(name + ".limit"); + String iconName = el.getString(name + ".icon", "BARRIER"); + Material icon; + try { + icon = Material.valueOf(iconName.toUpperCase(Locale.ENGLISH)); + } catch (Exception e) { + addon.logError("Invalid group icon name: " + iconName + ". Use a Bukkit Material."); + icon = Material.BARRIER; + } + Set entities = el.getStringList(name + ".entities").stream().map(s -> { + EntityType type = getType(s); + if (type == null) { + addon.logError("Unknown entity type: " + s + " - skipping..."); + return null; + } + if (DISALLOWED.contains(type)) { + addon.logError("Entity type: " + s + " is not supported - skipping..."); + return null; + } + return type; + }).filter(Objects::nonNull).collect(Collectors.toCollection(LinkedHashSet::new)); + if (entities.isEmpty()) continue; + EntityGroup group = new EntityGroup(name, entities, limit, icon); + entities.forEach(e -> groupLimits.computeIfAbsent(e, k -> new ArrayList<>()).add(group)); + // Default group limit applies to every env unless overridden. + ENVIRONMENTS.forEach(env -> envGroupLimits.get(env).put(name, limit)); + } + } + + private void loadGroupLimitOverrides(Limits addon, String section, Environment env) { + ConfigurationSection el = addon.getConfig().getConfigurationSection(section); + if (el == null) return; + for (String name : el.getKeys(false)) { + // Accept either a flat int (Monsters: 100) or a nested .limit (Monsters.limit: 100) + int limit; + if (el.isInt(name)) { + limit = el.getInt(name); + } else if (el.isInt(name + ".limit")) { + limit = el.getInt(name + ".limit"); + } else { + addon.logError("Group override " + section + "." + name + " missing limit - skipping."); + continue; + } + // Group must already be defined in the base entitygrouplimits section + boolean exists = getGroupLimitDefinitions().stream().anyMatch(g -> g.getName().equals(name)); + if (!exists) { + addon.logError("Group override " + section + "." + name + + " refers to an undefined group - define it under entitygrouplimits first."); + continue; + } + envGroupLimits.get(env).put(name, limit); + } + } + + private EntityType getType(String key) { + return Arrays.stream(EntityType.values()).filter(v -> v.name().equalsIgnoreCase(key)).findFirst().orElse(null); + } + + /** + * Return the env-specific entity limit map. Mutate via Limits API only. + */ + public Map getLimits(Environment env) { + return Collections.unmodifiableMap(envLimits.getOrDefault(env, Collections.emptyMap())); + } + + /** + * Group-name → limit overrides for this environment. + */ + public Map getGroupLimits(Environment env) { + return Collections.unmodifiableMap(envGroupLimits.getOrDefault(env, Collections.emptyMap())); + } + + /** + * @return the entity-type → group-list lookup + */ + public Map> getGroupLimits() { + return groupLimits; + } + + /** + * @return the group definitions + */ + public List getGroupLimitDefinitions() { + return groupLimits.values().stream().flatMap(Collection::stream).distinct().toList(); + } + + public List getGameModes() { + return gameModes; + } + + public boolean isLogLimitsOnJoin() { + return logLimitsOnJoin; + } + + public boolean isAsyncGolums() { + return asyncGolums; + } + + public Map getGeneral() { + return general; + } + + /** + * Config-file suffix used for environment-specific sections. + * @param env environment + * @return suffix like "-nether", "-end", or "" for overworld + */ + public static String suffixFor(Environment env) { + return ENV_SUFFIX.getOrDefault(env, ""); + } +} diff --git a/src/main/java/world/bentobox/limits/calculators/RecountCalculator.java b/src/main/java/world/bentobox/limits/calculators/RecountCalculator.java index 64cf4b3..6ec1551 100644 --- a/src/main/java/world/bentobox/limits/calculators/RecountCalculator.java +++ b/src/main/java/world/bentobox/limits/calculators/RecountCalculator.java @@ -1,361 +1,276 @@ -package world.bentobox.limits.calculators; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.EnumMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Queue; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.function.BooleanSupplier; -import java.util.function.LongSupplier; - -import org.bukkit.Bukkit; -import org.bukkit.Chunk; -import org.bukkit.ChunkSnapshot; -import org.bukkit.Location; -import org.bukkit.NamespacedKey; -import org.bukkit.Tag; -import org.bukkit.World; -import org.bukkit.World.Environment; -import org.bukkit.block.data.BlockData; -import org.bukkit.block.data.type.Slab; -import org.bukkit.scheduler.BukkitTask; - -import world.bentobox.bentobox.BentoBox; -import world.bentobox.bentobox.database.objects.Island; -import world.bentobox.bentobox.util.Pair; -import world.bentobox.bentobox.util.Util; -import world.bentobox.limits.Limits; -import world.bentobox.limits.calculators.Results.Result; -import world.bentobox.limits.listeners.BlockLimitsListener; -import world.bentobox.limits.objects.IslandBlockCount; - -/** - * Counter for limits - * @author tastybento - * - */ -public class RecountCalculator { - public static final long MAX_AMOUNT = 10000; - private static final int CHUNKS_TO_SCAN = 100; - private static final int CALCULATION_TIMEOUT = 5; // Minutes - - - private final Limits addon; - private final Queue> chunksToCheck; - private final Island island; - private final CompletableFuture r; - - - private final Results results; - private final Map worlds = new EnumMap<>(Environment.class); - private final List stackedBlocks = new ArrayList<>(); - private BukkitTask finishTask; - private final BlockLimitsListener bll; - private final World world; - private IslandBlockCount ibc; - - - /** - * Constructor to get the level for an island - * @param addon - addon - * @param island - the island to scan - * @param r - completable result that will be completed when the calculation is complete - */ - public RecountCalculator(Limits addon, Island island, CompletableFuture r) { - this.addon = addon; - this.bll = addon.getBlockLimitListener(); - this.island = island; - this.ibc = bll.getIsland(Objects.requireNonNull(island).getUniqueId()); - this.r = r; - results = new Results(); - chunksToCheck = getChunksToScan(island); - // Set up the worlds - this.world = Objects.requireNonNull(Util.getWorld(island.getWorld())); - worlds.put(Environment.NORMAL, world); - boolean isNether = addon.getPlugin().getIWM().isNetherGenerate(world) && addon.getPlugin().getIWM().isNetherIslands(world); - boolean isEnd = addon.getPlugin().getIWM().isEndGenerate(world) && addon.getPlugin().getIWM().isEndIslands(world); - - // Nether - if (isNether) { - World nether = addon.getPlugin().getIWM().getNetherWorld(island.getWorld()); - if (nether != null) { - worlds.put(Environment.NETHER, nether); - } - } - // End - if (isEnd) { - World end = addon.getPlugin().getIWM().getEndWorld(island.getWorld()); - if (end != null) { - worlds.put(Environment.THE_END, end); - } - } - } - - private void checkBlock(BlockData b) { - NamespacedKey md = bll.fixMaterial(b); - // md is limited - if (bll.getMaterialLimits(world, island.getUniqueId()).containsKey(md)) { - results.mdCount.add(md); - } - } - /** - * Get a set of all the chunks in island - * @param island - island - * @return - set of pairs of x,z coordinates to check - */ - private Queue> getChunksToScan(Island island) { - Queue> chunkQueue = new ConcurrentLinkedQueue<>(); - for (int x = island.getMinProtectedX(); x < (island.getMinProtectedX() + island.getProtectionRange() * 2 + 16); x += 16) { - for (int z = island.getMinProtectedZ(); z < (island.getMinProtectedZ() + island.getProtectionRange() * 2 + 16); z += 16) { - chunkQueue.add(new Pair<>(x >> 4, z >> 4)); - } - } - return chunkQueue; - } - - - /** - * @return the island - */ - public Island getIsland() { - return island; - } - - /** - * Get the completable result for this calculation - * @return the r - */ - public CompletableFuture getR() { - return r; - } - - /** - * @return the results - */ - public Results getResults() { - return results; - } - - /** - * Get a chunk async - * @param env - the environment - * @param x - chunk x coordinate - * @param z - chunk z coordinate - * @return a future chunk or future null if there is no chunk to load, e.g., there is no island nether - */ - private CompletableFuture> getWorldChunk(Environment env, Queue> pairList) { - if (worlds.containsKey(env)) { - CompletableFuture> r2 = new CompletableFuture<>(); - List chunkList = new ArrayList<>(); - World world = worlds.get(env); - // Get the chunk, and then coincidentally check the RoseStacker - loadChunks(r2, world, pairList, chunkList); - return r2; - } - return CompletableFuture.completedFuture(Collections.emptyList()); - } - - private void loadChunks(CompletableFuture> r2, World world, Queue> pairList, - List chunkList) { - if (pairList.isEmpty()) { - r2.complete(chunkList); - return; - } - Pair p = pairList.poll(); - Util.getChunkAtAsync(world, p.x, p.z, world.getEnvironment().equals(Environment.NETHER)).thenAccept(chunk -> { - if (chunk != null) { - chunkList.add(chunk); - // roseStackerCheck(chunk); - } - loadChunks(r2, world, pairList, chunkList); // Iteration - }); - } - /* - private void roseStackerCheck(Chunk chunk) { - if (addon.isRoseStackersEnabled()) { - RoseStackerAPI.getInstance().getStackedBlocks(Collections.singletonList(chunk)).forEach(e -> { - // Blocks below sea level can be scored differently - boolean belowSeaLevel = seaHeight > 0 && e.getLocation().getY() <= seaHeight; - // Check block once because the base block will be counted in the chunk snapshot - for (int _x = 0; _x < e.getStackSize() - 1; _x++) { - checkBlock(e.getBlock().getType(), belowSeaLevel); - } - }); - } - } - */ - /** - * Count the blocks on the island - * @param chunk chunk to scan - */ - private void scanAsync(Chunk chunk) { - ChunkSnapshot chunkSnapshot = chunk.getChunkSnapshot(); - for (int x = 0; x< 16; x++) { - // Check if the block coordinate is inside the protection zone and if not, don't count it - if (chunkSnapshot.getX() * 16 + x < island.getMinProtectedX() || chunkSnapshot.getX() * 16 + x >= island.getMinProtectedX() + island.getProtectionRange() * 2) { - continue; - } - for (int z = 0; z < 16; z++) { - // Check if the block coordinate is inside the protection zone and if not, don't count it - if (chunkSnapshot.getZ() * 16 + z < island.getMinProtectedZ() || chunkSnapshot.getZ() * 16 + z >= island.getMinProtectedZ() + island.getProtectionRange() * 2) { - continue; - } - // Only count to the highest block in the world for some optimization - for (int y = chunk.getWorld().getMinHeight(); y < chunk.getWorld().getMaxHeight(); y++) { - BlockData blockData = chunkSnapshot.getBlockData(x, y, z); - // Slabs can be doubled, so check them twice - if (Tag.SLABS.isTagged(blockData.getMaterial())) { - Slab slab = (Slab)blockData; - if (slab.getType().equals(Slab.Type.DOUBLE)) { - checkBlock(blockData); - } - } - // Hook for Wild Stackers (Blocks Only) - this has to use the real chunk - /* - if (addon.isStackersEnabled() && blockData.getMaterial() == Material.CAULDRON) { - stackedBlocks.add(new Location(chunk.getWorld(), x + chunkSnapshot.getX() * 16,y,z + chunkSnapshot.getZ() * 16)); - } - */ - // Add the value of the block's material - checkBlock(blockData); - } - } - } - } - - /** - * Scan the chunk chests and count the blocks - * @param chunks - the chunk to scan - * @return future that completes when the scan is done and supplies a boolean that will be true if the scan was successful, false if not - */ - private CompletableFuture scanChunk(List chunks) { - // If the chunk hasn't been generated, return - if (chunks == null || chunks.isEmpty()) { - return CompletableFuture.completedFuture(false); - } - // Count blocks in chunk - CompletableFuture result = new CompletableFuture<>(); - - Bukkit.getScheduler().runTaskAsynchronously(BentoBox.getInstance(), () -> { - chunks.forEach(chunk -> scanAsync(chunk)); - Bukkit.getScheduler().runTask(addon.getPlugin(),() -> result.complete(true)); - }); - return result; - } - - /** - * Scan the next chunk on the island - * @return completable boolean future that will be true if more chunks are left to be scanned, and false if not - */ - public CompletableFuture scanNextChunk() { - if (chunksToCheck.isEmpty()) { - addon.logError("Unexpected: no chunks to scan!"); - // This should not be needed, but just in case - return CompletableFuture.completedFuture(false); - } - // Retrieve and remove from the queue - Queue> pairList = new ConcurrentLinkedQueue<>(); - int i = 0; - while (!chunksToCheck.isEmpty() && i++ < CHUNKS_TO_SCAN) { - pairList.add(chunksToCheck.poll()); - } - Queue> endPairList = new ConcurrentLinkedQueue<>(pairList); - Queue> netherPairList = new ConcurrentLinkedQueue<>(pairList); - // Set up the result - CompletableFuture result = new CompletableFuture<>(); - // Get chunks and scan - // Get chunks and scan - getWorldChunk(Environment.THE_END, endPairList).thenAccept(endChunks -> - scanChunk(endChunks).thenAccept(b -> - getWorldChunk(Environment.NETHER, netherPairList).thenAccept(netherChunks -> - scanChunk(netherChunks).thenAccept(b2 -> - getWorldChunk(Environment.NORMAL, pairList).thenAccept(normalChunks -> - scanChunk(normalChunks).thenAccept(b3 -> - // Complete the result now that all chunks have been scanned - result.complete(!chunksToCheck.isEmpty())))) - ) - ) - ); - - return result; - } - - /** - * Finalizes the calculations and makes the report - */ - public void tidyUp() { - // Finalize calculations - if (ibc == null) { - ibc = new IslandBlockCount(island.getUniqueId(), addon.getPlugin().getIWM().getAddon(world).map(a -> a.getDescription().getName()).orElse("default")); - } - ibc.getBlockCounts().clear(); - results.getMdCount().forEach(ibc::add); - bll.setIsland(island.getUniqueId(), ibc); - //Bukkit.getScheduler().runTask(addon.getPlugin(), () -> sender.sendMessage("admin.limits.calc.finished")); - - // All done. - } - - public void scanIsland(LongSupplier startTime, Runnable onRemove, BooleanSupplier isCancelled, Runnable recurse) { - // Scan the next chunk - scanNextChunk().thenAccept(r -> { - if (!Bukkit.isPrimaryThread()) { - addon.getPlugin().logError("scanChunk not on Primary Thread!"); - } - // Timeout check - if (System.currentTimeMillis() - startTime.getAsLong() > CALCULATION_TIMEOUT * 60000) { - // Done - onRemove.run(); - getR().complete(new Results(Result.TIMEOUT)); - addon.logError("Level calculation timed out after " + CALCULATION_TIMEOUT + "m for island: " + getIsland()); - return; - } - if (Boolean.TRUE.equals(r) && !isCancelled.getAsBoolean()) { - // scanNextChunk returns true if there are more chunks to scan - recurse.run(); - } else { - // Done - onRemove.run(); - // Chunk finished - // This was the last chunk - handleStackedBlocks(); - long checkTime = System.currentTimeMillis(); - finishTask = Bukkit.getScheduler().runTaskTimer(addon.getPlugin(), () -> { - // Check every half second if all the chests and stacks have been cleared - if ((stackedBlocks.isEmpty()) || System.currentTimeMillis() - checkTime > MAX_AMOUNT) { - this.tidyUp(); - this.getR().complete(getResults()); - finishTask.cancel(); - } - }, 0, 10L); - - } - }); - } - - private void handleStackedBlocks() { - // Deal with any stacked blocks - /* - Iterator it = stackedBlocks.iterator(); - while (it.hasNext()) { - Location v = it.next(); - Util.getChunkAtAsync(v).thenAccept(c -> { - Block cauldronBlock = v.getBlock(); - boolean belowSeaLevel = seaHeight > 0 && v.getBlockY() <= seaHeight; - if (WildStackerAPI.getWildStacker().getSystemManager().isStackedBarrel(cauldronBlock)) { - StackedBarrel barrel = WildStackerAPI.getStackedBarrel(cauldronBlock); - int barrelAmt = WildStackerAPI.getBarrelAmount(cauldronBlock); - for (int _x = 0; _x < barrelAmt; _x++) { - checkBlock(barrel.getType(), belowSeaLevel); - } - } - it.remove(); - }); - } - */ - } -} +package world.bentobox.limits.calculators; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.BooleanSupplier; +import java.util.function.LongSupplier; + +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.ChunkSnapshot; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.Tag; +import org.bukkit.World; +import org.bukkit.World.Environment; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.type.Slab; +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; +import org.bukkit.scheduler.BukkitTask; + +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.bentobox.util.Pair; +import world.bentobox.bentobox.util.Util; +import world.bentobox.limits.Limits; +import world.bentobox.limits.calculators.Results.Result; +import world.bentobox.limits.listeners.BlockLimitsListener; +import world.bentobox.limits.objects.IslandBlockCount; + +/** + * Counter for limits. + * + *

Scans every loaded chunk in each of the island's environment worlds (overworld, + * nether, end) and rebuilds {@link IslandBlockCount}'s per-env block and entity + * counts from scratch. + * + * @author tastybento + */ +public class RecountCalculator { + public static final long MAX_AMOUNT = 10000; + private static final int CHUNKS_TO_SCAN = 100; + private static final int CALCULATION_TIMEOUT = 5; + + private final Limits addon; + private final Queue> chunksToCheck; + private final Island island; + private final CompletableFuture r; + private final Results results; + private final Map worlds = new EnumMap<>(Environment.class); + private final List stackedBlocks = new ArrayList<>(); + private BukkitTask finishTask; + private final BlockLimitsListener bll; + private final World world; + private IslandBlockCount ibc; + + public RecountCalculator(Limits addon, Island island, CompletableFuture r) { + this.addon = addon; + this.bll = addon.getBlockLimitListener(); + this.island = island; + this.ibc = bll.getIsland(Objects.requireNonNull(island).getUniqueId()); + this.r = r; + results = new Results(); + chunksToCheck = getChunksToScan(island); + this.world = Objects.requireNonNull(Util.getWorld(island.getWorld())); + worlds.put(Environment.NORMAL, world); + boolean isNether = addon.getPlugin().getIWM().isNetherGenerate(world) + && addon.getPlugin().getIWM().isNetherIslands(world); + boolean isEnd = addon.getPlugin().getIWM().isEndGenerate(world) + && addon.getPlugin().getIWM().isEndIslands(world); + if (isNether) { + World nether = addon.getPlugin().getIWM().getNetherWorld(island.getWorld()); + if (nether != null) worlds.put(Environment.NETHER, nether); + } + if (isEnd) { + World end = addon.getPlugin().getIWM().getEndWorld(island.getWorld()); + if (end != null) worlds.put(Environment.THE_END, end); + } + } + + private void checkBlock(Environment env, BlockData b) { + NamespacedKey md = bll.fixMaterial(b); + // Only count materials that are tracked at any level (env default, world override, island). + if (bll.getMaterialLimits(worlds.get(env), island.getUniqueId()).containsKey(md)) { + results.getBlockCount(env).add(md); + } + } + + private Queue> getChunksToScan(Island island) { + Queue> chunkQueue = new ConcurrentLinkedQueue<>(); + for (int x = island.getMinProtectedX(); x < (island.getMinProtectedX() + island.getProtectionRange() * 2 + + 16); x += 16) { + for (int z = island.getMinProtectedZ(); z < (island.getMinProtectedZ() + island.getProtectionRange() * 2 + + 16); z += 16) { + chunkQueue.add(new Pair<>(x >> 4, z >> 4)); + } + } + return chunkQueue; + } + + public Island getIsland() { + return island; + } + + public CompletableFuture getR() { + return r; + } + + public Results getResults() { + return results; + } + + private CompletableFuture> getWorldChunk(Environment env, Queue> pairList) { + if (worlds.containsKey(env)) { + CompletableFuture> r2 = new CompletableFuture<>(); + List chunkList = new ArrayList<>(); + World world = worlds.get(env); + loadChunks(r2, world, pairList, chunkList); + return r2; + } + return CompletableFuture.completedFuture(Collections.emptyList()); + } + + private void loadChunks(CompletableFuture> r2, World world, Queue> pairList, + List chunkList) { + if (pairList.isEmpty()) { + r2.complete(chunkList); + return; + } + Pair p = pairList.poll(); + Util.getChunkAtAsync(world, p.x, p.z, world.getEnvironment().equals(Environment.NETHER)).thenAccept(chunk -> { + if (chunk != null) chunkList.add(chunk); + loadChunks(r2, world, pairList, chunkList); + }); + } + + private void scanAsync(Environment env, Chunk chunk) { + ChunkSnapshot chunkSnapshot = chunk.getChunkSnapshot(); + for (int x = 0; x < 16; x++) { + if (chunkSnapshot.getX() * 16 + x < island.getMinProtectedX() + || chunkSnapshot.getX() * 16 + x >= island.getMinProtectedX() + + island.getProtectionRange() * 2) { + continue; + } + for (int z = 0; z < 16; z++) { + if (chunkSnapshot.getZ() * 16 + z < island.getMinProtectedZ() + || chunkSnapshot.getZ() * 16 + z >= island.getMinProtectedZ() + + island.getProtectionRange() * 2) { + continue; + } + for (int y = chunk.getWorld().getMinHeight(); y < chunk.getWorld().getMaxHeight(); y++) { + BlockData blockData = chunkSnapshot.getBlockData(x, y, z); + if (Tag.SLABS.isTagged(blockData.getMaterial())) { + Slab slab = (Slab) blockData; + if (slab.getType().equals(Slab.Type.DOUBLE)) { + checkBlock(env, blockData); + } + } + checkBlock(env, blockData); + } + } + } + } + + private CompletableFuture scanChunk(Environment env, List chunks) { + if (chunks == null || chunks.isEmpty()) { + return CompletableFuture.completedFuture(false); + } + CompletableFuture result = new CompletableFuture<>(); + Bukkit.getScheduler().runTaskAsynchronously(BentoBox.getInstance(), () -> { + chunks.forEach(chunk -> scanAsync(env, chunk)); + Bukkit.getScheduler().runTask(addon.getPlugin(), () -> result.complete(true)); + }); + return result; + } + + public CompletableFuture scanNextChunk() { + if (chunksToCheck.isEmpty()) { + addon.logError("Unexpected: no chunks to scan!"); + return CompletableFuture.completedFuture(false); + } + Queue> pairList = new ConcurrentLinkedQueue<>(); + int i = 0; + while (!chunksToCheck.isEmpty() && i++ < CHUNKS_TO_SCAN) { + pairList.add(chunksToCheck.poll()); + } + Queue> endPairList = new ConcurrentLinkedQueue<>(pairList); + Queue> netherPairList = new ConcurrentLinkedQueue<>(pairList); + CompletableFuture result = new CompletableFuture<>(); + getWorldChunk(Environment.THE_END, endPairList).thenAccept(endChunks -> + scanChunk(Environment.THE_END, endChunks).thenAccept(b -> + getWorldChunk(Environment.NETHER, netherPairList).thenAccept(netherChunks -> + scanChunk(Environment.NETHER, netherChunks).thenAccept(b2 -> + getWorldChunk(Environment.NORMAL, pairList).thenAccept(normalChunks -> + scanChunk(Environment.NORMAL, normalChunks).thenAccept(b3 -> + result.complete(!chunksToCheck.isEmpty()))))))); + return result; + } + + private void scanEntities() { + for (Map.Entry e : worlds.entrySet()) { + Environment env = e.getKey(); + World w = e.getValue(); + for (Entity entity : w.getEntities()) { + if (!island.inIslandSpace(entity.getLocation())) continue; + if (entity instanceof LivingEntity || entity.getType().name().endsWith("_MINECART") + || entity.getType().name().equals("ARMOR_STAND") + || entity.getType().name().equals("ITEM_FRAME") + || entity.getType().name().equals("PAINTING")) { + results.getEntityCount(env).add(entity.getType()); + } + } + } + } + + public void tidyUp() { + if (ibc == null) { + ibc = new IslandBlockCount(island.getUniqueId(), + addon.getPlugin().getIWM().getAddon(world).map(a -> a.getDescription().getName()).orElse("default")); + } + // Reset and write per-env block counts + ibc.clearAllBlockCounts(); + results.getEnvBlockCount().forEach((env, multiset) -> multiset.forEach(key -> ibc.add(env, key))); + + // Recount entities (loaded only) and write per-env + scanEntities(); + ibc.clearAllEntityCounts(); + results.getEnvEntityCount().forEach((env, multiset) -> multiset + .entrySet().forEach(en -> { + for (int i = 0; i < en.getCount(); i++) { + ibc.incrementEntity(env, en.getElement()); + } + })); + bll.setIsland(island.getUniqueId(), ibc); + } + + public void scanIsland(LongSupplier startTime, Runnable onRemove, BooleanSupplier isCancelled, Runnable recurse) { + scanNextChunk().thenAccept(r -> { + if (!Bukkit.isPrimaryThread()) { + addon.getPlugin().logError("scanChunk not on Primary Thread!"); + } + if (System.currentTimeMillis() - startTime.getAsLong() > CALCULATION_TIMEOUT * 60000) { + onRemove.run(); + getR().complete(new Results(Result.TIMEOUT)); + addon.logError("Level calculation timed out after " + CALCULATION_TIMEOUT + "m for island: " + + getIsland()); + return; + } + if (Boolean.TRUE.equals(r) && !isCancelled.getAsBoolean()) { + recurse.run(); + } else { + onRemove.run(); + handleStackedBlocks(); + long checkTime = System.currentTimeMillis(); + finishTask = Bukkit.getScheduler().runTaskTimer(addon.getPlugin(), () -> { + if ((stackedBlocks.isEmpty()) || System.currentTimeMillis() - checkTime > MAX_AMOUNT) { + this.tidyUp(); + this.getR().complete(getResults()); + finishTask.cancel(); + } + }, 0, 10L); + } + }); + } + + private void handleStackedBlocks() { + // Stacked-block plugin support is currently disabled; placeholder for future re-enablement. + } +} diff --git a/src/main/java/world/bentobox/limits/calculators/Results.java b/src/main/java/world/bentobox/limits/calculators/Results.java index daf50fc..a5aad31 100644 --- a/src/main/java/world/bentobox/limits/calculators/Results.java +++ b/src/main/java/world/bentobox/limits/calculators/Results.java @@ -1,57 +1,54 @@ -package world.bentobox.limits.calculators; - -import org.bukkit.NamespacedKey; -import org.bukkit.entity.EntityType; - -import com.google.common.collect.HashMultiset; -import com.google.common.collect.Multiset; - -public class Results { - public enum Result { - /** - * A level calc is already in progress - */ - IN_PROGRESS, - /** - * Results will be available - */ - AVAILABLE, - /** - * Result if calculation timed out - */ - TIMEOUT - } - final Multiset mdCount = HashMultiset.create(); - final Multiset entityCount = HashMultiset.create(); - - final Result state; - - public Results(Result state) { - this.state = state; - } - - public Results() { - this.state = Result.AVAILABLE; - } - /** - * @return the mdCount - */ - public Multiset getMdCount() { - return mdCount; - } - - /** - * @return the state - */ - public Result getState() { - return state; - } - - /** - * @return the entityCount - */ - public Multiset getEntityCount() { - return entityCount; - } - -} \ No newline at end of file +package world.bentobox.limits.calculators; + +import java.util.EnumMap; +import java.util.Map; + +import org.bukkit.NamespacedKey; +import org.bukkit.World.Environment; +import org.bukkit.entity.EntityType; + +import com.google.common.collect.HashMultiset; +import com.google.common.collect.Multiset; + +public class Results { + public enum Result { + IN_PROGRESS, + AVAILABLE, + TIMEOUT + } + + /** Block counts per environment, populated by the chunk scan. */ + final Map> envBlockCount = new EnumMap<>(Environment.class); + /** Entity counts per environment, populated by the entity scan. */ + final Map> envEntityCount = new EnumMap<>(Environment.class); + + final Result state; + + public Results(Result state) { + this.state = state; + } + + public Results() { + this.state = Result.AVAILABLE; + } + + public Multiset getBlockCount(Environment env) { + return envBlockCount.computeIfAbsent(env, e -> HashMultiset.create()); + } + + public Multiset getEntityCount(Environment env) { + return envEntityCount.computeIfAbsent(env, e -> HashMultiset.create()); + } + + public Map> getEnvBlockCount() { + return envBlockCount; + } + + public Map> getEnvEntityCount() { + return envEntityCount; + } + + public Result getState() { + return state; + } +} diff --git a/src/main/java/world/bentobox/limits/commands/admin/OffsetCommand.java b/src/main/java/world/bentobox/limits/commands/admin/OffsetCommand.java index c71ab37..b7ae60c 100644 --- a/src/main/java/world/bentobox/limits/commands/admin/OffsetCommand.java +++ b/src/main/java/world/bentobox/limits/commands/admin/OffsetCommand.java @@ -151,8 +151,8 @@ public boolean execute(User user, String label, List args) int offset = Integer.parseInt(args.get(2)); - if (material != null && offset == islandData.getBlockLimitOffset(material.getKey()) || - entityType != null && offset == islandData.getEntityLimitOffset(entityType)) + if (material != null && offset == islandData.getBlockLimitOffset(org.bukkit.World.Environment.NORMAL, material.getKey()) || + entityType != null && offset == islandData.getEntityLimitOffset(org.bukkit.World.Environment.NORMAL, entityType)) { user.sendMessage("admin.limits.offset.set.same", TextVariables.NAME, @@ -164,7 +164,7 @@ public boolean execute(User user, String label, List args) if (material != null) { - islandData.setBlockLimitsOffset(material.getKey(), offset); + islandData.setBlockLimitsOffsetAllEnvs(material.getKey(), offset); islandData.setChanged(); user.sendMessage("admin.limits.offset.set.success", @@ -173,7 +173,7 @@ public boolean execute(User user, String label, List args) } else { - islandData.setEntityLimitsOffset(entityType, offset); + islandData.setEntityLimitsOffsetAllEnvs(entityType, offset); islandData.setChanged(); user.sendMessage("admin.limits.offset.set.success", @@ -277,9 +277,9 @@ public boolean execute(User user, String label, List args) if (material != null) { - offset += islandData.getBlockLimitOffset(material.getKey()); + offset += islandData.getBlockLimitOffset(org.bukkit.World.Environment.NORMAL, material.getKey()); - islandData.setBlockLimitsOffset(material.getKey(), offset); + islandData.setBlockLimitsOffsetAllEnvs(material.getKey(), offset); islandData.setChanged(); user.sendMessage("admin.limits.offset.add.success", @@ -288,9 +288,9 @@ public boolean execute(User user, String label, List args) } else { - offset += islandData.getEntityLimitOffset(entityType); + offset += islandData.getEntityLimitOffset(org.bukkit.World.Environment.NORMAL, entityType); - islandData.setEntityLimitsOffset(entityType, offset); + islandData.setEntityLimitsOffsetAllEnvs(entityType, offset); islandData.setChanged(); user.sendMessage("admin.limits.offset.add.success", @@ -394,9 +394,9 @@ public boolean execute(User user, String label, List args) if (material != null) { - offset = islandData.getBlockLimitOffset(material.getKey()) - offset; + offset = islandData.getBlockLimitOffset(org.bukkit.World.Environment.NORMAL, material.getKey()) - offset; - islandData.setBlockLimitsOffset(material.getKey(), offset); + islandData.setBlockLimitsOffsetAllEnvs(material.getKey(), offset); islandData.setChanged(); user.sendMessage("admin.limits.offset.remove.success", @@ -405,9 +405,9 @@ public boolean execute(User user, String label, List args) } else { - offset = islandData.getEntityLimitOffset(entityType) - offset; + offset = islandData.getEntityLimitOffset(org.bukkit.World.Environment.NORMAL, entityType) - offset; - islandData.setEntityLimitsOffset(entityType, offset); + islandData.setEntityLimitsOffsetAllEnvs(entityType, offset); islandData.setChanged(); user.sendMessage("admin.limits.offset.remove.success", @@ -502,7 +502,7 @@ public boolean execute(User user, String label, List args) if (material != null) { - islandData.setBlockLimitsOffset(material.getKey(), 0); + islandData.setBlockLimitsOffsetAllEnvs(material.getKey(), 0); islandData.setChanged(); user.sendMessage("admin.limits.offset.reset.success", @@ -510,7 +510,7 @@ public boolean execute(User user, String label, List args) } else { - islandData.setEntityLimitsOffset(entityType, 0); + islandData.setEntityLimitsOffsetAllEnvs(entityType, 0); islandData.setChanged(); user.sendMessage("admin.limits.offset.reset.success", @@ -604,14 +604,14 @@ public boolean execute(User user, String label, List args) if (material != null) { - int offset = islandData.getBlockLimitOffset(material.getKey()); + int offset = islandData.getBlockLimitOffset(org.bukkit.World.Environment.NORMAL, material.getKey()); user.sendMessage("admin.limits.offset.view.message", TextVariables.NAME, material.name(), TextVariables.NUMBER, String.valueOf(offset)); } else { - int offset = islandData.getEntityLimitOffset(entityType); + int offset = islandData.getEntityLimitOffset(org.bukkit.World.Environment.NORMAL, entityType); user.sendMessage("admin.limits.offset.view.message", TextVariables.NAME, entityType.name(), TextVariables.NUMBER, String.valueOf(offset)); diff --git a/src/main/java/world/bentobox/limits/commands/player/LimitPanel.java b/src/main/java/world/bentobox/limits/commands/player/LimitPanel.java index 8557526..f00ca70 100644 --- a/src/main/java/world/bentobox/limits/commands/player/LimitPanel.java +++ b/src/main/java/world/bentobox/limits/commands/player/LimitPanel.java @@ -1,78 +1,88 @@ -package world.bentobox.limits.commands.player; - -import java.util.Map; -import java.util.UUID; - -import org.bukkit.Bukkit; -import org.bukkit.NamespacedKey; -import org.bukkit.World; -import org.bukkit.entity.Player; - -import world.bentobox.bentobox.api.addons.GameModeAddon; -import world.bentobox.bentobox.api.panels.builders.TabbedPanelBuilder; -import world.bentobox.bentobox.api.user.User; -import world.bentobox.bentobox.database.objects.Island; -import world.bentobox.limits.Limits; -import world.bentobox.limits.objects.IslandBlockCount; - -/** - * Shows a panel of the blocks that are limited and their status - * @author tastybento - * - */ -public class LimitPanel { - - private final Limits addon; - - /** - * @param addon - limit addon - */ - public LimitPanel(Limits addon) { - this.addon = addon; - } - - /** - * Show the limits panel - * @param gm - game mode - * @param user - user asking - * @param target - target uuid - */ - public void showLimits(GameModeAddon gm, User user, UUID target) { - // Get world - World world = gm.getOverWorld(); - // Get the island for the target - Island island = addon.getIslands().getIsland(world, target); - if (island == null) { - if (user.getUniqueId().equals(target)) { - user.sendMessage("general.errors.no-island"); - } else { - user.sendMessage("general.errors.player-has-no-island"); - } - return; - } - // See if the target is online - Player targetPlayer = Bukkit.getPlayer(target); - if (targetPlayer != null) { - // Update perms - addon.getJoinListener().checkPerms(targetPlayer, gm.getPermissionPrefix() + "island.limit.", island.getUniqueId(), gm.getDescription().getName()); - } - // Get the limits for this island - IslandBlockCount ibc = addon.getBlockLimitListener().getIsland(island.getUniqueId()); - Map matLimits = addon.getBlockLimitListener().getMaterialLimits(world, island.getUniqueId()); - if (matLimits.isEmpty() && addon.getSettings().getLimits().isEmpty()) { - user.sendMessage("island.limits.no-limits"); - return; - } - - new TabbedPanelBuilder() - .user(user) - .world(world) - .tab(0, new LimitTab(addon, ibc, matLimits, island, world, user, LimitTab.SORT_BY.A2Z)) - .tab(1, new LimitTab(addon, ibc, matLimits, island, world, user, LimitTab.SORT_BY.Z2A)) - .startingSlot(0) - .size(54) - .build().openPanel(); - - } - -} +package world.bentobox.limits.commands.player; + +import java.util.Map; +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.World.Environment; +import org.bukkit.entity.Player; + +import world.bentobox.bentobox.api.addons.GameModeAddon; +import world.bentobox.bentobox.api.panels.builders.TabbedPanelBuilder; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.limits.Limits; +import world.bentobox.limits.objects.IslandBlockCount; + +/** + * Shows a panel of the limited blocks/entities and their counts per environment. + * + * @author tastybento + */ +public class LimitPanel { + + private final Limits addon; + + public LimitPanel(Limits addon) { + this.addon = addon; + } + + public void showLimits(GameModeAddon gm, User user, UUID target) { + World overWorld = gm.getOverWorld(); + Island island = addon.getIslands().getIsland(overWorld, target); + if (island == null) { + user.sendMessage(user.getUniqueId().equals(target) + ? "general.errors.no-island" + : "general.errors.player-has-no-island"); + return; + } + Player targetPlayer = Bukkit.getPlayer(target); + if (targetPlayer != null) { + addon.getJoinListener().checkPerms(targetPlayer, gm.getPermissionPrefix() + "island.limit.", + island.getUniqueId(), gm.getDescription().getName()); + } + IslandBlockCount ibc = addon.getBlockLimitListener().getIsland(island.getUniqueId()); + + TabbedPanelBuilder tpb = new TabbedPanelBuilder() + .user(user) + .world(overWorld) + .startingSlot(0) + .size(54); + + // Always show overworld; show nether/end only if the gamemode generates and provides them. + int slot = 0; + slot = addEnvTab(tpb, gm, overWorld, ibc, island, user, Environment.NORMAL, slot); + World netherWorld = gm.getNetherWorld(); + if (netherWorld != null && addon.getPlugin().getIWM().isNetherIslands(overWorld)) { + slot = addEnvTab(tpb, gm, netherWorld, ibc, island, user, Environment.NETHER, slot); + } + World endWorld = gm.getEndWorld(); + if (endWorld != null && addon.getPlugin().getIWM().isEndIslands(overWorld)) { + slot = addEnvTab(tpb, gm, endWorld, ibc, island, user, Environment.THE_END, slot); + } + + Map overworldLimits = addon.getBlockLimitListener().getMaterialLimits(overWorld, + island.getUniqueId()); + if (overworldLimits.isEmpty() && addon.getSettings().getLimits(Environment.NORMAL).isEmpty() && slot == 0) { + user.sendMessage("island.limits.no-limits"); + return; + } + + tpb.build().openPanel(); + } + + private int addEnvTab(TabbedPanelBuilder tpb, GameModeAddon gm, World envWorld, IslandBlockCount ibc, Island island, + User user, Environment env, int slot) { + Map matLimits = addon.getBlockLimitListener().getMaterialLimits(envWorld, + island.getUniqueId()); + // Skip the env tab entirely if there is nothing to show for it. + if (matLimits.isEmpty() && addon.getSettings().getLimits(env).isEmpty() + && addon.getSettings().getGroupLimits(env).isEmpty()) { + return slot; + } + tpb.tab(slot, new LimitTab(addon, ibc, matLimits, island, envWorld, user, env)); + return slot + 1; + } +} diff --git a/src/main/java/world/bentobox/limits/commands/player/LimitTab.java b/src/main/java/world/bentobox/limits/commands/player/LimitTab.java index b824744..31bc151 100644 --- a/src/main/java/world/bentobox/limits/commands/player/LimitTab.java +++ b/src/main/java/world/bentobox/limits/commands/player/LimitTab.java @@ -1,271 +1,229 @@ -package world.bentobox.limits.commands.player; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Objects; -import java.util.stream.Collectors; - -import org.bukkit.Material; -import org.bukkit.NamespacedKey; -import org.bukkit.Registry; -import org.bukkit.World; -import org.bukkit.entity.EntityType; -import org.eclipse.jdt.annotation.Nullable; - -import com.google.common.collect.ImmutableMap; - -import world.bentobox.bentobox.api.localization.TextVariables; -import world.bentobox.bentobox.api.panels.PanelItem; -import world.bentobox.bentobox.api.panels.Tab; -import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; -import world.bentobox.bentobox.api.user.User; -import world.bentobox.bentobox.database.objects.Island; -import world.bentobox.bentobox.util.Util; -import world.bentobox.limits.EntityGroup; -import world.bentobox.limits.Limits; -import world.bentobox.limits.objects.IslandBlockCount; - -/** - * @author tastybento - * - */ -public class LimitTab implements Tab { - - enum SORT_BY { - A2Z, - Z2A - } - - /** - * This maps the entity types to the icon that should be shown in the panel - * If the icon is null, then the entity type is not covered by the addon - */ - private static final Map E2M = ImmutableMap.builder() - .put(EntityType.MOOSHROOM, Material.MOOSHROOM_SPAWN_EGG).put(EntityType.SNOW_GOLEM, Material.SNOW_BLOCK) - .put(EntityType.IRON_GOLEM, Material.IRON_BLOCK) - .put(EntityType.ILLUSIONER, Material.VILLAGER_SPAWN_EGG) - .put(EntityType.WITHER, Material.WITHER_SKELETON_SKULL) - //.put(EntityType.BOAT, Material.OAK_BOAT) - .put(EntityType.ARMOR_STAND, Material.ARMOR_STAND) - .put(EntityType.ITEM_FRAME, Material.ITEM_FRAME) - .put(EntityType.PAINTING, Material.PAINTING) - // Minecarts - .put(EntityType.TNT_MINECART, Material.TNT_MINECART).put(EntityType.CHEST_MINECART, Material.CHEST_MINECART) - .put(EntityType.COMMAND_BLOCK_MINECART, Material.COMMAND_BLOCK_MINECART) - .put(EntityType.FURNACE_MINECART, Material.FURNACE_MINECART) - .put(EntityType.HOPPER_MINECART, Material.HOPPER_MINECART) - .put(EntityType.SPAWNER_MINECART, Material.MINECART) - //.put(EntityType.CHEST_BOAT, Material.OAK_CHEST_BOAT) - .build(); - // This is a map of blocks to Items - private static final Map B2M; - static { - ImmutableMap.Builder builder = ImmutableMap.builder() - .put(Material.POTATOES.getKey(), Material.POTATO.getKey()) - .put(Material.CARROTS.getKey(), Material.CARROT.getKey()) - .put(Material.BEETROOTS.getKey(), Material.BEETROOT.getKey()) - .put(Material.REDSTONE_WIRE.getKey(), Material.REDSTONE.getKey()).put(Material.MELON_STEM.getKey(), Material.MELON.getKey()) - .put(Material.PUMPKIN_STEM.getKey(), Material.PUMPKIN.getKey()) - .put(Material.SWEET_BERRY_BUSH.getKey(), Material.SWEET_BERRIES.getKey()) - .put(Material.BAMBOO_SAPLING.getKey(), Material.BAMBOO.getKey()) - ; - B2M = builder.build(); - } - - private final World world; - private final User user; - private final Limits addon; - private final List<@Nullable PanelItem> result; - private final SORT_BY sortBy; - - public LimitTab(Limits addon, IslandBlockCount ibc, Map matLimits, Island island, World world, User user, SORT_BY sortBy) { - this.addon = addon; - this.world = world; - this.user = user; - this.sortBy = sortBy; - result = new ArrayList<>(); - addMaterialIcons(ibc, matLimits); - addEntityLimits(ibc, island); - addEntityGroupLimits(ibc, island); - // Sort - if (sortBy == SORT_BY.Z2A) { - result.sort((o1, o2) -> o2.getName().compareTo(o1.getName())); - } else { - result.sort(Comparator.comparing(PanelItem::getName)); - } - - } - - private void addEntityGroupLimits(IslandBlockCount ibc, Island island) { - // Entity group limits - Map groupMap = addon.getSettings().getGroupLimitDefinitions().stream().collect(Collectors.toMap(e -> e, EntityGroup::getLimit)); - // Group by same loop up map - Map groupByName = groupMap.keySet().stream().collect(Collectors.toMap(EntityGroup::getName, e -> e)); - // Merge in any permission-based limits - if (ibc == null) { - return; - } - ibc.getEntityGroupLimits().entrySet().stream() - .filter(e -> groupByName.containsKey(e.getKey())) - .forEach(e -> groupMap.put(groupByName.get(e.getKey()), e.getValue())); - // Update the group map for each group limit offset. If the value already exists add it - ibc.getEntityGroupLimitsOffset().forEach((key, value) -> { - if (groupByName.get(key) != null) { - groupMap.put(groupByName.get(key), (groupMap.getOrDefault(groupByName.get(key), 0) + value)); - } - }); - groupMap.forEach((v, limit) -> { - PanelItemBuilder pib = new PanelItemBuilder(); - pib.name(user.getTranslation("island.limits.panel.entity-group-name-syntax", TextVariables.NAME, - v.getName())); - String description = ""; - description += "(" + prettyNames(v) + ")\n"; - pib.icon(v.getIcon()); - long count = getCount(island, v); - String color = count >= limit ? user.getTranslation("island.limits.max-color") : user.getTranslation("island.limits.regular-color"); - description += color - + user.getTranslation("island.limits.block-limit-syntax", - TextVariables.NUMBER, String.valueOf(count), - "[limit]", String.valueOf(limit)); - pib.description(description); - result.add(pib.build()); - }); - } - - private void addEntityLimits(IslandBlockCount ibc, Island island) { - // Entity limits - Map map = new HashMap<>(addon.getSettings().getLimits()); - // Merge in any permission-based limits - if (ibc != null) { - map.putAll(ibc.getEntityLimits()); - ibc.getEntityLimitsOffset().forEach((k,v) -> map.put(k, map.getOrDefault(k, 0) + v)); - } - - map.forEach((k,v) -> { - PanelItemBuilder pib = new PanelItemBuilder(); - pib.name(user.getTranslation("island.limits.panel.entity-name-syntax", TextVariables.NAME, - Util.prettifyText(k.toString()))); - Material m; - try { - if (E2M.containsKey(k)) { - m = E2M.get(k); - } else if (k.isAlive()) { - m = Material.valueOf(k + "_SPAWN_EGG"); - } else { - // Regular material - m = Material.valueOf(k.toString()); - } - } catch (Exception e) { - m = Material.BARRIER; - } - pib.icon(m); - long count = getCount(island, k); - String color = count >= v ? user.getTranslation("island.limits.max-color") : user.getTranslation("island.limits.regular-color"); - pib.description(color - + user.getTranslation("island.limits.block-limit-syntax", - TextVariables.NUMBER, String.valueOf(count), - "[limit]", String.valueOf(v))); - result.add(pib.build()); - }); - - } - - private void addMaterialIcons(IslandBlockCount ibc, Map matLimits) { - // Material limits - for (Entry en : matLimits.entrySet()) { - PanelItemBuilder pib = new PanelItemBuilder(); - pib.name(user.getTranslation("island.limits.panel.block-name-syntax", TextVariables.NAME, - Util.prettifyText(en.getKey().getKey()))); - // Adjust icon - Material mat = Registry.MATERIAL.get(B2M.getOrDefault(en.getKey(), en.getKey())); - pib.icon(Objects.requireNonNullElse(mat, Material.PAPER)); - - int count = ibc == null ? 0 : ibc.getBlockCounts().getOrDefault(en.getKey(), 0); - int value = en.getValue(); - String color = count >= value ? user.getTranslation("island.limits.max-color") : user.getTranslation("island.limits.regular-color"); - pib.description(color - + user.getTranslation("island.limits.block-limit-syntax", - TextVariables.NUMBER, String.valueOf(count), - "[limit]", String.valueOf(value))); - result.add(pib.build()); - } - } - - @Override - public PanelItem getIcon() { - return new PanelItemBuilder().icon(Material.MAGENTA_GLAZED_TERRACOTTA).name(this.getName()).build(); - } - - @Override - public String getName() { - String sort = user.getTranslation(world, "island.limits.panel." + sortBy); - return user.getTranslation(world, "island.limits.panel.title-syntax", "[title]", - user.getTranslation(world, "limits.panel-title"), "[sort]", sort); - } - - @Override - public List<@Nullable PanelItem> getPanelItems() { - return result; - } - - @Override - public String getPermission() { - return ""; - } - - private String prettyNames(EntityGroup v) { - StringBuilder sb = new StringBuilder(); - List l = new ArrayList<>(v.getTypes()); - for(int i = 0; i < l.size(); i++) - { - sb.append(Util.prettifyText(l.get(i).toString())); - if (i + 1 < l.size()) - sb.append(", "); - if((i+1) % 5 == 0) - sb.append("\n"); - } - return sb.toString(); - } - - long getCount(Island island, EntityType ent) { - long count = island.getWorld().getEntities().stream() - .filter(e -> e.getType().equals(ent)) - .filter(e -> island.inIslandSpace(e.getLocation())).count(); - // Nether - if (addon.getPlugin().getIWM().isNetherIslands(island.getWorld()) && addon.getPlugin().getIWM().getNetherWorld(island.getWorld()) != null) { - count += addon.getPlugin().getIWM().getNetherWorld(island.getWorld()).getEntities().stream() - .filter(e -> e.getType().equals(ent)) - .filter(e -> island.inIslandSpace(e.getLocation())).count(); - } - // End - if (addon.getPlugin().getIWM().isEndIslands(island.getWorld()) && addon.getPlugin().getIWM().getEndWorld(island.getWorld()) != null) { - count += addon.getPlugin().getIWM().getEndWorld(island.getWorld()).getEntities().stream() - .filter(e -> e.getType().equals(ent)) - .filter(e -> island.inIslandSpace(e.getLocation())).count(); - } - return count; - } - - long getCount(Island island, EntityGroup group) { - long count = island.getWorld().getEntities().stream() - .filter(e -> group.contains(e.getType())) - .filter(e -> island.inIslandSpace(e.getLocation())).count(); - // Nether - if (addon.getPlugin().getIWM().isNetherIslands(island.getWorld()) && addon.getPlugin().getIWM().getNetherWorld(island.getWorld()) != null) { - count += addon.getPlugin().getIWM().getNetherWorld(island.getWorld()).getEntities().stream() - .filter(e -> group.contains(e.getType())) - .filter(e -> island.inIslandSpace(e.getLocation())).count(); - } - // End - if (addon.getPlugin().getIWM().isEndIslands(island.getWorld()) && addon.getPlugin().getIWM().getEndWorld(island.getWorld()) != null) { - count += addon.getPlugin().getIWM().getEndWorld(island.getWorld()).getEntities().stream() - .filter(e -> group.contains(e.getType())) - .filter(e -> island.inIslandSpace(e.getLocation())).count(); - } - return count; - } -} +package world.bentobox.limits.commands.player; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.World; +import org.bukkit.World.Environment; +import org.bukkit.entity.EntityType; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.common.collect.ImmutableMap; + +import world.bentobox.bentobox.api.localization.TextVariables; +import world.bentobox.bentobox.api.panels.PanelItem; +import world.bentobox.bentobox.api.panels.Tab; +import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.bentobox.util.Util; +import world.bentobox.limits.EntityGroup; +import world.bentobox.limits.Limits; +import world.bentobox.limits.objects.IslandBlockCount; + +/** + * One tab of the limits panel. Shows counts and limits for a single environment. + * + * @author tastybento + */ +public class LimitTab implements Tab { + + /** This maps the entity types to the icon that should be shown in the panel */ + private static final Map E2M = ImmutableMap.builder() + .put(EntityType.MOOSHROOM, Material.MOOSHROOM_SPAWN_EGG).put(EntityType.SNOW_GOLEM, Material.SNOW_BLOCK) + .put(EntityType.IRON_GOLEM, Material.IRON_BLOCK) + .put(EntityType.ILLUSIONER, Material.VILLAGER_SPAWN_EGG) + .put(EntityType.WITHER, Material.WITHER_SKELETON_SKULL) + .put(EntityType.ARMOR_STAND, Material.ARMOR_STAND) + .put(EntityType.ITEM_FRAME, Material.ITEM_FRAME) + .put(EntityType.PAINTING, Material.PAINTING) + .put(EntityType.TNT_MINECART, Material.TNT_MINECART).put(EntityType.CHEST_MINECART, Material.CHEST_MINECART) + .put(EntityType.COMMAND_BLOCK_MINECART, Material.COMMAND_BLOCK_MINECART) + .put(EntityType.FURNACE_MINECART, Material.FURNACE_MINECART) + .put(EntityType.HOPPER_MINECART, Material.HOPPER_MINECART) + .put(EntityType.SPAWNER_MINECART, Material.MINECART) + .build(); + + private static final Map B2M; + static { + B2M = ImmutableMap.builder() + .put(Material.POTATOES.getKey(), Material.POTATO.getKey()) + .put(Material.CARROTS.getKey(), Material.CARROT.getKey()) + .put(Material.BEETROOTS.getKey(), Material.BEETROOT.getKey()) + .put(Material.REDSTONE_WIRE.getKey(), Material.REDSTONE.getKey()) + .put(Material.MELON_STEM.getKey(), Material.MELON.getKey()) + .put(Material.PUMPKIN_STEM.getKey(), Material.PUMPKIN.getKey()) + .put(Material.SWEET_BERRY_BUSH.getKey(), Material.SWEET_BERRIES.getKey()) + .put(Material.BAMBOO_SAPLING.getKey(), Material.BAMBOO.getKey()) + .build(); + } + + private final World world; + private final User user; + private final Limits addon; + private final Environment env; + private final List<@Nullable PanelItem> result; + + public LimitTab(Limits addon, IslandBlockCount ibc, Map matLimits, Island island, + World world, User user, Environment env) { + this.addon = addon; + this.world = world; + this.user = user; + this.env = env; + result = new ArrayList<>(); + addMaterialIcons(ibc, matLimits); + addEntityLimits(ibc, island); + addEntityGroupLimits(ibc, island); + result.sort(Comparator.comparing(PanelItem::getName)); + } + + private void addEntityGroupLimits(IslandBlockCount ibc, Island island) { + Map envGroupLimits = new HashMap<>(addon.getSettings().getGroupLimits(env)); + Map groupMap = addon.getSettings().getGroupLimitDefinitions().stream() + .filter(g -> envGroupLimits.containsKey(g.getName())) + .collect(Collectors.toMap(g -> g, g -> envGroupLimits.get(g.getName()))); + Map groupByName = addon.getSettings().getGroupLimitDefinitions().stream() + .collect(Collectors.toMap(EntityGroup::getName, e -> e)); + if (ibc != null) { + ibc.getEntityGroupLimits(env).forEach((name, limit) -> { + EntityGroup g = groupByName.get(name); + if (g != null) groupMap.put(g, limit); + }); + ibc.getEntityGroupLimitsOffset(env).forEach((name, offset) -> { + EntityGroup g = groupByName.get(name); + if (g != null) groupMap.put(g, groupMap.getOrDefault(g, 0) + offset); + }); + } + groupMap.forEach((g, limit) -> { + PanelItemBuilder pib = new PanelItemBuilder(); + pib.name(user.getTranslation("island.limits.panel.entity-group-name-syntax", TextVariables.NAME, + g.getName())); + String description = "(" + prettyNames(g) + ")\n"; + pib.icon(g.getIcon()); + int count = ibc == null ? 0 : sumGroupCount(ibc, g); + String color = count >= limit ? user.getTranslation("island.limits.max-color") + : user.getTranslation("island.limits.regular-color"); + description += color + user.getTranslation("island.limits.block-limit-syntax", + TextVariables.NUMBER, String.valueOf(count), + "[limit]", String.valueOf(limit)); + pib.description(description); + result.add(pib.build()); + }); + } + + private int sumGroupCount(IslandBlockCount ibc, EntityGroup group) { + Map counts = ibc.getEntityCounts(env); + int total = 0; + for (EntityType t : group.getTypes()) { + total += counts.getOrDefault(t, 0); + } + return total; + } + + private void addEntityLimits(IslandBlockCount ibc, Island island) { + Map map = new HashMap<>(addon.getSettings().getLimits(env)); + if (ibc != null) { + map.putAll(ibc.getEntityLimits(env)); + ibc.getEntityLimitsOffset(env).forEach((k, v) -> map.put(k, map.getOrDefault(k, 0) + v)); + } + map.forEach((k, v) -> { + PanelItemBuilder pib = new PanelItemBuilder(); + pib.name(user.getTranslation("island.limits.panel.entity-name-syntax", TextVariables.NAME, + Util.prettifyText(k.toString()))); + Material m; + try { + if (E2M.containsKey(k)) { + m = E2M.get(k); + } else if (k.isAlive()) { + m = Material.valueOf(k + "_SPAWN_EGG"); + } else { + m = Material.valueOf(k.toString()); + } + } catch (Exception e) { + m = Material.BARRIER; + } + pib.icon(m); + int count = ibc == null ? 0 : ibc.getEntityCount(env, k); + String color = count >= v ? user.getTranslation("island.limits.max-color") + : user.getTranslation("island.limits.regular-color"); + pib.description(color + user.getTranslation("island.limits.block-limit-syntax", + TextVariables.NUMBER, String.valueOf(count), + "[limit]", String.valueOf(v))); + result.add(pib.build()); + }); + } + + private void addMaterialIcons(IslandBlockCount ibc, Map matLimits) { + for (Entry en : matLimits.entrySet()) { + PanelItemBuilder pib = new PanelItemBuilder(); + pib.name(user.getTranslation("island.limits.panel.block-name-syntax", TextVariables.NAME, + Util.prettifyText(en.getKey().getKey()))); + Material mat = Registry.MATERIAL.get(B2M.getOrDefault(en.getKey(), en.getKey())); + pib.icon(Objects.requireNonNullElse(mat, Material.PAPER)); + + int count = ibc == null ? 0 : ibc.getBlockCount(env, en.getKey()); + int value = en.getValue(); + String color = count >= value ? user.getTranslation("island.limits.max-color") + : user.getTranslation("island.limits.regular-color"); + pib.description(color + user.getTranslation("island.limits.block-limit-syntax", + TextVariables.NUMBER, String.valueOf(count), + "[limit]", String.valueOf(value))); + result.add(pib.build()); + } + } + + @Override + public PanelItem getIcon() { + Material icon = switch (env) { + case NETHER -> Material.NETHERRACK; + case THE_END -> Material.END_STONE; + default -> Material.GRASS_BLOCK; + }; + return new PanelItemBuilder().icon(icon).name(this.getName()).build(); + } + + @Override + public String getName() { + return user.getTranslation(world, "island.limits.panel.title-syntax", + "[title]", user.getTranslation(world, "limits.panel-title"), + "[env]", user.getTranslation(world, envKey())); + } + + private String envKey() { + return switch (env) { + case NETHER -> "island.limits.panel.env-nether"; + case THE_END -> "island.limits.panel.env-end"; + default -> "island.limits.panel.env-overworld"; + }; + } + + @Override + public List<@Nullable PanelItem> getPanelItems() { + return result; + } + + @Override + public String getPermission() { + return ""; + } + + private String prettyNames(EntityGroup v) { + StringBuilder sb = new StringBuilder(); + List l = new ArrayList<>(v.getTypes()); + for (int i = 0; i < l.size(); i++) { + sb.append(Util.prettifyText(l.get(i).toString())); + if (i + 1 < l.size()) sb.append(", "); + if ((i + 1) % 5 == 0) sb.append("\n"); + } + return sb.toString(); + } +} diff --git a/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java b/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java index feaa37f..6fc12cc 100644 --- a/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java +++ b/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java @@ -1,600 +1,541 @@ -package world.bentobox.limits.listeners; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.NamespacedKey; -import org.bukkit.Registry; -import org.bukkit.Tag; -import org.bukkit.World; -import org.bukkit.block.Block; -import org.bukkit.block.BlockFace; -import org.bukkit.block.data.BlockData; -import org.bukkit.block.data.type.TechnicalPiston; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.event.Cancellable; -import org.bukkit.event.Event; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.bukkit.event.block.Action; -import org.bukkit.event.block.BlockBreakEvent; -import org.bukkit.event.block.BlockBurnEvent; -import org.bukkit.event.block.BlockExplodeEvent; -import org.bukkit.event.block.BlockFadeEvent; -import org.bukkit.event.block.BlockFormEvent; -import org.bukkit.event.block.BlockFromToEvent; -import org.bukkit.event.block.BlockGrowEvent; -import org.bukkit.event.block.BlockMultiPlaceEvent; -import org.bukkit.event.block.BlockPlaceEvent; -import org.bukkit.event.block.BlockSpreadEvent; -import org.bukkit.event.block.EntityBlockFormEvent; -import org.bukkit.event.block.LeavesDecayEvent; -import org.bukkit.event.entity.EntityChangeBlockEvent; -import org.bukkit.event.entity.EntityExplodeEvent; -import org.bukkit.event.player.PlayerInteractEvent; -import org.eclipse.jdt.annotation.NonNull; -import org.eclipse.jdt.annotation.Nullable; - -import world.bentobox.bentobox.api.events.island.IslandDeleteEvent; -import world.bentobox.bentobox.api.localization.TextVariables; -import world.bentobox.bentobox.api.user.User; -import world.bentobox.bentobox.database.Database; -import world.bentobox.bentobox.database.objects.Island; -import world.bentobox.bentobox.util.Util; -import world.bentobox.limits.Limits; -import world.bentobox.limits.objects.IslandBlockCount; - -/** - * @author tastybento - * - */ -public class BlockLimitsListener implements Listener { - - /** - * Blocks that are not counted - */ - private static final List DO_NOT_COUNT = List.of(Material.LAVA.getKey(), Material.WATER.getKey(), Material.AIR.getKey(), Material.FIRE.getKey(), Material.END_PORTAL.getKey(), Material.NETHER_PORTAL.getKey()); - private static final List STACKABLE = List.of(Material.SUGAR_CANE.getKey(), Material.BAMBOO.getKey()); - - /** - * Save every 10 blocks of change - */ - private static final Integer CHANGE_LIMIT = 9; - private final Limits addon; - private final Map islandCountMap = new HashMap<>(); - private final Map saveMap = new HashMap<>(); - private final Database handler; - private final Map> worldLimitMap = new HashMap<>(); - private Map defaultLimitMap = new HashMap<>(); - - public BlockLimitsListener(Limits addon) { - this.addon = addon; - handler = new Database<>(addon, IslandBlockCount.class); - List toBeDeleted = new ArrayList<>(); - handler.loadObjects().forEach(ibc -> { - // Clean up - if (addon.isCoveredGameMode(ibc.getGameMode())) { - ibc.getBlockCounts().keySet().removeIf(DO_NOT_COUNT::contains); - // Store - islandCountMap.put(ibc.getUniqueId(), ibc); - } else { - toBeDeleted.add(ibc.getUniqueId()); - } - }); - toBeDeleted.forEach(handler::deleteID); - loadAllLimits(); - } - - /** - * Loads the default and world-specific limits - */ - private void loadAllLimits() { - // Load the default limits - addon.log("Loading default limits"); - if (addon.getConfig().isConfigurationSection("blocklimits")) { - ConfigurationSection limitConfig = addon.getConfig().getConfigurationSection("blocklimits"); - defaultLimitMap = loadLimits(Objects.requireNonNull(limitConfig)); - } - - // Load specific worlds - if (addon.getConfig().isConfigurationSection("worlds")) { - ConfigurationSection worlds = addon.getConfig().getConfigurationSection("worlds"); - for (String worldName : Objects.requireNonNull(worlds).getKeys(false)) { - World world = Bukkit.getWorld(worldName); - if (world != null && addon.inGameModeWorld(world)) { - addon.log("Loading limits for " + world.getName()); - worldLimitMap.putIfAbsent(world, new HashMap<>()); - ConfigurationSection matsConfig = worlds.getConfigurationSection(worldName); - worldLimitMap.put(world, loadLimits(Objects.requireNonNull(matsConfig))); - } - } - } - - } - /* - private static final Set> TAGS = Set.of(Tag.ANVIL, - Tag.BAMBOO_BLOCKS, Tag.BANNERS, Tag.BEDS, Tag.BEEHIVES, Tag.BUTTONS, Tag.CAMPFIRES, Tag.CANDLE_CAKES, - Tag.CORALS, Tag.CAULDRONS, Tag.CAVE_VINES, Tag.DIRT, Tag.DOORS, - Tag.FENCE_GATES, Tag.FENCES, Tag.ICE, Tag.LEAVES, Tag.LOGS, - Tag.NYLIUM, Tag.PLANKS, Tag.PORTALS, Tag.RAILS, Tag.SAND, Tag.SAPLINGS, Tag.SHULKER_BOXES, - Tag.SIGNS, Tag.SLABS, Tag.SNOW, Tag.STAIRS, Tag.STONE_BRICKS, - Tag.PRESSURE_PLATES, Tag.TERRACOTTA, Tag.TRAPDOORS, Tag.WALLS, - Tag.WART_BLOCKS, Tag.WOOL, Tag.WOOL_CARPETS);*/ - /** - * Loads limit map from configuration section - * - * @param cs - configuration section - * @return limit map - */ - private Map loadLimits(ConfigurationSection cs) { - Map limits = new HashMap<>(); - for (String key : cs.getKeys(false)) { - int limit = cs.getInt(key); - NamespacedKey nsKey; - if (key.contains(":")) { - // Config already has a full namespaced key - nsKey = NamespacedKey.fromString(key.toLowerCase(Locale.ROOT)); - if (nsKey == null) { - Bukkit.getLogger().warning("Invalid namespaced key in config, skipping: " + key); - continue; - } - } else { - // Assume "minecraft" namespace if none provided - nsKey = new NamespacedKey(NamespacedKey.MINECRAFT, key.toLowerCase(Locale.ROOT)); - } - - boolean matched = false; - - // Try match to Material - only accept block materials that are not in DO_NOT_COUNT - Material mat = Registry.MATERIAL.get(nsKey); - if (mat != null) { - matched = true; - if (!mat.isBlock()) { - Bukkit.getLogger().warning("Non-block material in block limits config: " + key); - } else if (DO_NOT_COUNT.contains(mat.getKey())) { - Bukkit.getLogger().warning("Uncountable material in block limits config: " + key); - } else { - limits.put(mat.getKey(), limit); - } - } - - // Try match to a Tag - if (!matched) { - Tag tag = Bukkit.getTag("blocks", nsKey, Material.class); - if (tag != null) { - limits.put(tag.getKey(), limit); - matched = true; - } - } - - - // Log warning if nothing matched - if (!matched) { - Bukkit.getLogger().warning("Unknown material or tag in config: " + key); - } - } - return limits; - } - - - /** - * Save the count database completely - */ - public void save() { - islandCountMap.values().stream().filter(IslandBlockCount::isChanged).forEach(handler::saveObjectAsync); - } - - // Player-related events - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) - public void onBlock(BlockPlaceEvent e) { - notify(e, User.getInstance(e.getPlayer()), process(e.getBlock(), true), e.getBlock().getType()); - } - - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) - public void onBlock(BlockBreakEvent e) { - if (e.getBlock().hasMetadata("blockbreakevent-ignore")) { - // Ignore event due to Advanced Enchantments. See https://ae.advancedplugins.net/for-developers/plugin-compatiblity-issues - // @since 1.28.0 - return; - } - handleBreak(e, e.getBlock()); - } - - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) - public void onTurtleEggBreak(PlayerInteractEvent e) { - if (e.getAction().equals(Action.PHYSICAL) && e.getClickedBlock() != null && e.getClickedBlock().getType().equals(Material.TURTLE_EGG)) { - handleBreak(e, e.getClickedBlock()); - } - } - - private void handleBreak(Event e, Block b) { - if (!addon.inGameModeWorld(b.getWorld())) { - return; - } - Material mat = b.getType(); - - // Check for stackable plants - if (STACKABLE.contains(b.getType().getKey())) { - // Check for blocks above - Block block = b; - while(block.getRelative(BlockFace.UP).getType().equals(mat) && block.getY() < b.getWorld().getMaxHeight()) { - block = block.getRelative(BlockFace.UP); - process(block, false); - } - } - process(b, false); - // Player breaks a block and there was a redstone dust/repeater/... above - if (b.getRelative(BlockFace.UP).getType() == Material.REDSTONE_WIRE || b.getRelative(BlockFace.UP).getType() == Material.REPEATER || b.getRelative(BlockFace.UP).getType() == Material.COMPARATOR || b.getRelative(BlockFace.UP).getType() == Material.REDSTONE_TORCH) { - process(b.getRelative(BlockFace.UP), false); - } - if (b.getRelative(BlockFace.EAST).getType() == Material.REDSTONE_WALL_TORCH) { - process(b.getRelative(BlockFace.EAST), false); - } - if (b.getRelative(BlockFace.WEST).getType() == Material.REDSTONE_WALL_TORCH) { - process(b.getRelative(BlockFace.WEST), false); - } - if (b.getRelative(BlockFace.SOUTH).getType() == Material.REDSTONE_WALL_TORCH) { - process(b.getRelative(BlockFace.SOUTH), false); - } - if (b.getRelative(BlockFace.NORTH).getType() == Material.REDSTONE_WALL_TORCH) { - process(b.getRelative(BlockFace.NORTH), false); - } - } - - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) - public void onBlock(BlockMultiPlaceEvent e) { - notify(e, User.getInstance(e.getPlayer()), process(e.getBlock(), true), e.getBlock().getType()); - } - - /** - * Cancel the event and notify the user of failure - * @param e event - * @param user user - * @param limit maximum limit allowed - * @param m material - */ - private void notify(Cancellable e, User user, int limit, Material m) { - if (limit > -1) { - user.notify("block-limits.hit-limit", - "[material]", Util.prettifyText(m.toString()), - TextVariables.NUMBER, String.valueOf(limit)); - e.setCancelled(true); - } - } - - // Non-player events - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) - public void onBlock(BlockBurnEvent e) { - process(e.getBlock(), false); - } - - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) - public void onBlock(BlockExplodeEvent e) { - e.blockList().forEach(b -> process(b, false)); - } - - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) - public void onBlock(BlockFadeEvent e) { - process(e.getBlock(), false); - } - - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) - public void onBlock(BlockFormEvent e) { - // EntityBlockFormEvent and BlockSpreadEvent extend BlockFormEvent; skip here to avoid double-processing - if (e instanceof EntityBlockFormEvent || e instanceof BlockSpreadEvent) { - return; - } - // Remove the old block state count - process(e.getBlock(), false); - // Add the new block state count; cancel if the new state exceeds its limit - if (process(e.getBlock(), e.getNewState().getBlockData(), true) > -1) { - e.setCancelled(true); - process(e.getBlock(), true); // Restore old count - } - } - - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) - public void onBlock(BlockSpreadEvent e) { - process(e.getBlock(), true); - } - - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) - public void onBlock(EntityBlockFormEvent e) { - // Remove the old block state count - process(e.getBlock(), false); - // Add the new block state count; cancel if the new state exceeds its limit - if (process(e.getBlock(), e.getNewState().getBlockData(), true) > -1) { - e.setCancelled(true); - process(e.getBlock(), true); // Restore old count - } - } - - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) - public void onBlock(BlockGrowEvent e) { - if (process(e.getNewState().getBlock(), true) > -1) { - e.setCancelled(true); - e.getBlock().getWorld().getBlockAt(e.getBlock().getLocation()).setBlockData(e.getBlock().getBlockData()); - } else { - process(e.getBlock(), false); - } - } - - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) - public void onBlock(LeavesDecayEvent e) { - process(e.getBlock(), false); - } - - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) - public void onBlock(EntityExplodeEvent e) { - e.blockList().forEach(b -> process(b, false)); - } - - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) - public void onBlock(EntityChangeBlockEvent e) { - // First, if the entity is changing the block to a non-AIR material, - // attempt to add the new block state and enforce limits. - if (e.getTo() != Material.AIR) { - int limit = process(e.getBlock(), e.getBlockData(), true); - // If a non-negative value is returned, the limit would be exceeded. - // Cancel the event and keep the existing block/counts unchanged. - if (limit > -1) { - e.setCancelled(true); - return; - } - } - - // At this point either the block is being removed (to AIR) or the new - // state was accepted. Now remove the old block from the counts. - process(e.getBlock(), false); - - // If the block was farmland and the change is allowed, also remove the - // block above (e.g., crops) from the counts. - if (!e.isCancelled() && e.getBlock().getType().equals(Material.FARMLAND)) { - process(e.getBlock().getRelative(BlockFace.UP), false); - } - } - - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) - public void onBlock(BlockFromToEvent e) { - if (e.getBlock().isLiquid() - && (e.getToBlock().getType() == Material.REDSTONE_WIRE - || e.getToBlock().getType() == Material.REPEATER - || e.getToBlock().getType() == Material.COMPARATOR - || e.getToBlock().getType() == Material.REDSTONE_TORCH - || e.getToBlock().getType() == Material.REDSTONE_WALL_TORCH)) { - process(e.getToBlock(), false); - } - } - - /** - * Return equivalents. Maps things like wall materials to their non-wall equivalents - * @param b block data - * @return material that matches the block data - */ - public NamespacedKey fixMaterial(BlockData b) { - Material mat = b.getMaterial(); - if (mat.equals(Material.CHIPPED_ANVIL) || mat.equals(Material.DAMAGED_ANVIL)) { - return Material.ANVIL.getKey(); - } else if (mat == Material.REDSTONE_WALL_TORCH) { - return Material.REDSTONE_TORCH.getKey(); - } else if (mat == Material.WALL_TORCH) { - return Material.TORCH.getKey(); - } else if (mat == Material.COPPER_WALL_TORCH) { - return Material.COPPER_TORCH.getKey(); - } else if (mat == Material.ZOMBIE_WALL_HEAD) { - return Material.ZOMBIE_HEAD.getKey(); - } else if (mat == Material.CREEPER_WALL_HEAD) { - return Material.CREEPER_HEAD.getKey(); - } else if (mat == Material.PLAYER_WALL_HEAD) { - return Material.PLAYER_HEAD.getKey(); - } else if (mat == Material.DRAGON_WALL_HEAD) { - return Material.DRAGON_HEAD.getKey(); - } else if (mat == Material.BAMBOO_SAPLING) { - return Material.BAMBOO.getKey(); - } else if (mat == Material.PISTON_HEAD || mat == Material.MOVING_PISTON) { - TechnicalPiston tp = (TechnicalPiston) b; - if (tp.getType() == TechnicalPiston.Type.NORMAL) { - return Material.PISTON.getKey(); - } else { - return Material.STICKY_PISTON.getKey(); - } - } else if (mat == Material.EXPOSED_COPPER_CHEST || mat == Material.WEATHERED_COPPER_CHEST - || mat == Material.OXIDIZED_COPPER_CHEST || mat == Material.WAXED_COPPER_CHEST - || mat == Material.WAXED_EXPOSED_COPPER_CHEST || mat == Material.WAXED_WEATHERED_COPPER_CHEST - || mat == Material.WAXED_OXIDIZED_COPPER_CHEST) { - return Material.COPPER_CHEST.getKey(); - } - return mat.getKey(); - } - - /** - * Check if a block can be - * - * @param b - block - * @param add - true to add a block, false to remove - * @return limit amount if over limit, or -1 if no limitation - */ - private int process(Block b, boolean add) { - return process(b, b.getBlockData(), add); - } - - /** - * Check if a block can be placed or needs to be removed based on limits. - * This variant allows specifying block data different from the block's current data, - * which is needed for block state transitions (e.g., copper oxidation or waxing). - * - * @param b - block (used for location and world) - * @param blockData - block data to use for material checks - * @param add - true to add a block, false to remove - * @return limit amount if over limit, or -1 if no limitation - */ - private int process(Block b, BlockData blockData, boolean add) { - if (DO_NOT_COUNT.contains(fixMaterial(blockData)) || !addon.inGameModeWorld(b.getWorld())) { - return -1; - } - // Check if on island - return addon.getIslands().getIslandAt(b.getLocation()).map(i -> { - String id = i.getUniqueId(); - String gameMode = addon.getGameModeName(b.getWorld()); - if (gameMode.isEmpty()) { - // Invalid world - return -1; - } - // Ignore the center block - usually bedrock, but for AOneBlock it's the magic block - if (addon.getConfig().getBoolean("ignore-center-block", true) && i.getCenter().equals(b.getLocation())) { - return -1; - } - islandCountMap.putIfAbsent(id, new IslandBlockCount(id, gameMode)); - if (add) { - // Check limit - int limit = checkLimit(b.getWorld(), fixMaterial(blockData), id); - if (limit > -1) { - return limit; - } - islandCountMap.get(id).add(fixMaterial(blockData)); - } else { - if (islandCountMap.containsKey(id)) { - islandCountMap.get(id).remove(fixMaterial(blockData)); - } - } - updateSaveMap(id); - return -1; - }).orElse(-1); - } - - /** - * Removed a block from any island limit count - * @param b - block to remove - */ - public void removeBlock(Block b) { - // Get island - addon.getIslands().getIslandAt(b.getLocation()).ifPresent(i -> { - String id = i.getUniqueId(); - String gameMode = addon.getGameModeName(b.getWorld()); - if (gameMode.isEmpty()) { - // Invalid world - return; - } - islandCountMap.computeIfAbsent(id, k -> new IslandBlockCount(id, gameMode)).remove(fixMaterial(b.getBlockData())); - updateSaveMap(id); - }); - } - private void updateSaveMap(String id) { - saveMap.putIfAbsent(id, 0); - if (saveMap.merge(id, 1, Integer::sum) > CHANGE_LIMIT) { - handler.saveObjectAsync(islandCountMap.get(id)); - saveMap.remove(id); - } - } - - - /** - * Check if this material is at its limit for world on this island - * - * @param w - world - * @param m - material - * @param id - island id - * @return limit amount if at limit or -1 if no limit - */ - private int checkLimit(World w, NamespacedKey m, String id) { - // Check island limits - IslandBlockCount ibc = islandCountMap.get(id); - if (ibc.isBlockLimited(m)) { - return ibc.isAtLimit(m) ? ibc.getBlockLimit(m) + ibc.getBlockLimitOffset(m) : -1; - } - // Check specific world limits - if (worldLimitMap.containsKey(w) && worldLimitMap.get(w).containsKey(m)) { - // Material is overridden in world - return ibc.isAtLimit(m, worldLimitMap.get(w).get(m)) ? worldLimitMap.get(w).get(m) + ibc.getBlockLimitOffset(m) : -1; - } - // Check default limit map - if (defaultLimitMap.containsKey(m) && ibc.isAtLimit(m, defaultLimitMap.get(m))) { - return defaultLimitMap.get(m) + ibc.getBlockLimitOffset(m); - } - // No limit - return -1; - } - - /** - * Gets an aggregate map of the limits for this island - * - * @param w - world - * @param id - island id - * @return map of limits for materials - */ - public Map getMaterialLimits(World w, String id) { - // Merge limits - Map result = new HashMap<>(); - // Default - result.putAll(defaultLimitMap); - // World - if (worldLimitMap.containsKey(w)) { - result.putAll(worldLimitMap.get(w)); - } - // Island - if (islandCountMap.containsKey(id)) { - IslandBlockCount islandBlockCount = islandCountMap.get(id); - result.putAll(islandBlockCount.getBlockLimits()); - - // Add offsets to the every limit. - islandBlockCount.getBlockLimitsOffset().forEach((material, offset) -> - result.put(material, result.getOrDefault(material, 0) + offset)); - } - return result; - } - - /** - * Removes island from the database - * - * @param e - island delete event - */ - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) - public void onIslandDelete(IslandDeleteEvent e) { - islandCountMap.remove(e.getIsland().getUniqueId()); - saveMap.remove(e.getIsland().getUniqueId()); - if (handler.objectExists(e.getIsland().getUniqueId())) { - handler.deleteID(e.getIsland().getUniqueId()); - } - } - - /** - * Set the island block count values - * - * @param islandId - island unique id - * @param ibc - island block count - */ - public void setIsland(String islandId, IslandBlockCount ibc) { - islandCountMap.put(islandId, ibc); - handler.saveObjectAsync(ibc); - } - - /** - * Get the island block count - * - * @param islandId - island unique id - * @return island block count or null if there is none yet - */ - @Nullable - public IslandBlockCount getIsland(String islandId) { - return islandCountMap.get(islandId); - } - - /** - * Get the island block count for island and make one if it does not exist - * @param island island - * @return island block count - */ - @NonNull - public IslandBlockCount getIsland(Island island) { - return islandCountMap.computeIfAbsent(island.getUniqueId(), k -> new IslandBlockCount(k, island.getGameMode())); - } - -} +package world.bentobox.limits.listeners; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.Tag; +import org.bukkit.World; +import org.bukkit.World.Environment; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.type.TechnicalPiston; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockBurnEvent; +import org.bukkit.event.block.BlockExplodeEvent; +import org.bukkit.event.block.BlockFadeEvent; +import org.bukkit.event.block.BlockFormEvent; +import org.bukkit.event.block.BlockFromToEvent; +import org.bukkit.event.block.BlockGrowEvent; +import org.bukkit.event.block.BlockMultiPlaceEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.block.BlockSpreadEvent; +import org.bukkit.event.block.EntityBlockFormEvent; +import org.bukkit.event.block.LeavesDecayEvent; +import org.bukkit.event.entity.EntityChangeBlockEvent; +import org.bukkit.event.entity.EntityExplodeEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + +import world.bentobox.bentobox.api.events.island.IslandDeleteEvent; +import world.bentobox.bentobox.api.localization.TextVariables; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.database.Database; +import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.bentobox.util.Util; +import world.bentobox.limits.Limits; +import world.bentobox.limits.Settings; +import world.bentobox.limits.objects.IslandBlockCount; + +/** + * @author tastybento + */ +public class BlockLimitsListener implements Listener { + + /** Blocks that are not counted */ + private static final List DO_NOT_COUNT = List.of(Material.LAVA.getKey(), Material.WATER.getKey(), + Material.AIR.getKey(), Material.FIRE.getKey(), Material.END_PORTAL.getKey(), + Material.NETHER_PORTAL.getKey()); + private static final List STACKABLE = List.of(Material.SUGAR_CANE.getKey(), Material.BAMBOO.getKey()); + + /** Save every 10 blocks of change */ + private static final Integer CHANGE_LIMIT = 9; + private final Limits addon; + private final Map islandCountMap = new HashMap<>(); + private final Map saveMap = new HashMap<>(); + private final Database handler; + private final Map> worldLimitMap = new HashMap<>(); + /** + * Per-environment default limits. The "blocklimits" section seeds every env; + * "blocklimits-nether" / "blocklimits-end" sections override one env each. + */ + private final Map> envDefaultLimitMap = new EnumMap<>(Environment.class); + + public BlockLimitsListener(Limits addon) { + this.addon = addon; + for (Environment env : Settings.ENVIRONMENTS) { + envDefaultLimitMap.put(env, new HashMap<>()); + } + handler = new Database<>(addon, IslandBlockCount.class); + List toBeDeleted = new ArrayList<>(); + handler.loadObjects().forEach(ibc -> { + if (addon.isCoveredGameMode(ibc.getGameMode())) { + // Strip uncountable types from every env's count map + ibc.getAllBlockCounts().values().forEach(m -> m.keySet().removeIf(DO_NOT_COUNT::contains)); + islandCountMap.put(ibc.getUniqueId(), ibc); + } else { + toBeDeleted.add(ibc.getUniqueId()); + } + }); + toBeDeleted.forEach(handler::deleteID); + loadAllLimits(); + } + + /** + * Loads the default and world-specific limits + */ + private void loadAllLimits() { + // Pass 1: "blocklimits" applies to every env as the global default + if (addon.getConfig().isConfigurationSection("blocklimits")) { + ConfigurationSection limitConfig = addon.getConfig().getConfigurationSection("blocklimits"); + Map base = loadLimits(Objects.requireNonNull(limitConfig)); + for (Environment env : Settings.ENVIRONMENTS) { + envDefaultLimitMap.get(env).putAll(base); + } + addon.log("Loading default block limits"); + } + // Pass 2: env-specific sections override the base for that env + loadEnvSection("blocklimits-nether", Environment.NETHER); + loadEnvSection("blocklimits-end", Environment.THE_END); + + // Per-world named overrides + if (addon.getConfig().isConfigurationSection("worlds")) { + ConfigurationSection worlds = addon.getConfig().getConfigurationSection("worlds"); + for (String worldName : Objects.requireNonNull(worlds).getKeys(false)) { + World world = Bukkit.getWorld(worldName); + if (world != null && addon.inGameModeWorld(world)) { + addon.log("Loading limits for " + world.getName()); + ConfigurationSection matsConfig = worlds.getConfigurationSection(worldName); + worldLimitMap.put(world, loadLimits(Objects.requireNonNull(matsConfig))); + } + } + } + } + + private void loadEnvSection(String section, Environment env) { + if (!addon.getConfig().isConfigurationSection(section)) return; + ConfigurationSection cs = addon.getConfig().getConfigurationSection(section); + addon.log("Loading " + env + " block limit overrides from " + section); + envDefaultLimitMap.get(env).putAll(loadLimits(Objects.requireNonNull(cs))); + } + + /** + * Loads limit map from configuration section + */ + private Map loadLimits(ConfigurationSection cs) { + Map limits = new HashMap<>(); + for (String key : cs.getKeys(false)) { + int limit = cs.getInt(key); + NamespacedKey nsKey; + if (key.contains(":")) { + nsKey = NamespacedKey.fromString(key.toLowerCase(Locale.ROOT)); + if (nsKey == null) { + Bukkit.getLogger().warning("Invalid namespaced key in config, skipping: " + key); + continue; + } + } else { + nsKey = new NamespacedKey(NamespacedKey.MINECRAFT, key.toLowerCase(Locale.ROOT)); + } + boolean matched = false; + Material mat = Registry.MATERIAL.get(nsKey); + if (mat != null) { + matched = true; + if (!mat.isBlock()) { + Bukkit.getLogger().warning("Non-block material in block limits config: " + key); + } else if (DO_NOT_COUNT.contains(mat.getKey())) { + Bukkit.getLogger().warning("Uncountable material in block limits config: " + key); + } else { + limits.put(mat.getKey(), limit); + } + } + if (!matched) { + Tag tag = Bukkit.getTag("blocks", nsKey, Material.class); + if (tag != null) { + limits.put(tag.getKey(), limit); + matched = true; + } + } + if (!matched) { + Bukkit.getLogger().warning("Unknown material or tag in config: " + key); + } + } + return limits; + } + + /** Save the count database completely */ + public void save() { + islandCountMap.values().stream().filter(IslandBlockCount::isChanged).forEach(handler::saveObjectAsync); + } + + /** Resolve the env, normalising any non-standard values to NORMAL. */ + private Environment envOf(World w) { + Environment env = w.getEnvironment(); + return Settings.ENVIRONMENTS.contains(env) ? env : Environment.NORMAL; + } + + // Player-related events + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onBlock(BlockPlaceEvent e) { + notify(e, User.getInstance(e.getPlayer()), process(e.getBlock(), true), e.getBlock().getType()); + } + + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onBlock(BlockBreakEvent e) { + if (e.getBlock().hasMetadata("blockbreakevent-ignore")) { + return; + } + handleBreak(e, e.getBlock()); + } + + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onTurtleEggBreak(PlayerInteractEvent e) { + if (e.getAction().equals(Action.PHYSICAL) && e.getClickedBlock() != null + && e.getClickedBlock().getType().equals(Material.TURTLE_EGG)) { + handleBreak(e, e.getClickedBlock()); + } + } + + private void handleBreak(Event e, Block b) { + if (!addon.inGameModeWorld(b.getWorld())) { + return; + } + Material mat = b.getType(); + if (STACKABLE.contains(b.getType().getKey())) { + Block block = b; + while (block.getRelative(BlockFace.UP).getType().equals(mat) + && block.getY() < b.getWorld().getMaxHeight()) { + block = block.getRelative(BlockFace.UP); + process(block, false); + } + } + process(b, false); + if (b.getRelative(BlockFace.UP).getType() == Material.REDSTONE_WIRE + || b.getRelative(BlockFace.UP).getType() == Material.REPEATER + || b.getRelative(BlockFace.UP).getType() == Material.COMPARATOR + || b.getRelative(BlockFace.UP).getType() == Material.REDSTONE_TORCH) { + process(b.getRelative(BlockFace.UP), false); + } + if (b.getRelative(BlockFace.EAST).getType() == Material.REDSTONE_WALL_TORCH) { + process(b.getRelative(BlockFace.EAST), false); + } + if (b.getRelative(BlockFace.WEST).getType() == Material.REDSTONE_WALL_TORCH) { + process(b.getRelative(BlockFace.WEST), false); + } + if (b.getRelative(BlockFace.SOUTH).getType() == Material.REDSTONE_WALL_TORCH) { + process(b.getRelative(BlockFace.SOUTH), false); + } + if (b.getRelative(BlockFace.NORTH).getType() == Material.REDSTONE_WALL_TORCH) { + process(b.getRelative(BlockFace.NORTH), false); + } + } + + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onBlock(BlockMultiPlaceEvent e) { + notify(e, User.getInstance(e.getPlayer()), process(e.getBlock(), true), e.getBlock().getType()); + } + + private void notify(Cancellable e, User user, int limit, Material m) { + if (limit > -1) { + user.notify("block-limits.hit-limit", + "[material]", Util.prettifyText(m.toString()), + TextVariables.NUMBER, String.valueOf(limit)); + e.setCancelled(true); + } + } + + // Non-player events + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onBlock(BlockBurnEvent e) { + process(e.getBlock(), false); + } + + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onBlock(BlockExplodeEvent e) { + e.blockList().forEach(b -> process(b, false)); + } + + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onBlock(BlockFadeEvent e) { + process(e.getBlock(), false); + } + + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onBlock(BlockFormEvent e) { + if (e instanceof EntityBlockFormEvent || e instanceof BlockSpreadEvent) { + return; + } + process(e.getBlock(), false); + if (process(e.getBlock(), e.getNewState().getBlockData(), true) > -1) { + e.setCancelled(true); + process(e.getBlock(), true); + } + } + + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onBlock(BlockSpreadEvent e) { + process(e.getBlock(), true); + } + + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onBlock(EntityBlockFormEvent e) { + process(e.getBlock(), false); + if (process(e.getBlock(), e.getNewState().getBlockData(), true) > -1) { + e.setCancelled(true); + process(e.getBlock(), true); + } + } + + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onBlock(BlockGrowEvent e) { + if (process(e.getNewState().getBlock(), true) > -1) { + e.setCancelled(true); + e.getBlock().getWorld().getBlockAt(e.getBlock().getLocation()).setBlockData(e.getBlock().getBlockData()); + } else { + process(e.getBlock(), false); + } + } + + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onBlock(LeavesDecayEvent e) { + process(e.getBlock(), false); + } + + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onBlock(EntityExplodeEvent e) { + e.blockList().forEach(b -> process(b, false)); + } + + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onBlock(EntityChangeBlockEvent e) { + if (e.getTo() != Material.AIR) { + int limit = process(e.getBlock(), e.getBlockData(), true); + if (limit > -1) { + e.setCancelled(true); + return; + } + } + process(e.getBlock(), false); + if (!e.isCancelled() && e.getBlock().getType().equals(Material.FARMLAND)) { + process(e.getBlock().getRelative(BlockFace.UP), false); + } + } + + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onBlock(BlockFromToEvent e) { + if (e.getBlock().isLiquid() + && (e.getToBlock().getType() == Material.REDSTONE_WIRE + || e.getToBlock().getType() == Material.REPEATER + || e.getToBlock().getType() == Material.COMPARATOR + || e.getToBlock().getType() == Material.REDSTONE_TORCH + || e.getToBlock().getType() == Material.REDSTONE_WALL_TORCH)) { + process(e.getToBlock(), false); + } + } + + /** + * Map variant materials to their canonical form. + */ + public NamespacedKey fixMaterial(BlockData b) { + Material mat = b.getMaterial(); + if (mat.equals(Material.CHIPPED_ANVIL) || mat.equals(Material.DAMAGED_ANVIL)) { + return Material.ANVIL.getKey(); + } else if (mat == Material.REDSTONE_WALL_TORCH) { + return Material.REDSTONE_TORCH.getKey(); + } else if (mat == Material.WALL_TORCH) { + return Material.TORCH.getKey(); + } else if (mat == Material.COPPER_WALL_TORCH) { + return Material.COPPER_TORCH.getKey(); + } else if (mat == Material.ZOMBIE_WALL_HEAD) { + return Material.ZOMBIE_HEAD.getKey(); + } else if (mat == Material.CREEPER_WALL_HEAD) { + return Material.CREEPER_HEAD.getKey(); + } else if (mat == Material.PLAYER_WALL_HEAD) { + return Material.PLAYER_HEAD.getKey(); + } else if (mat == Material.DRAGON_WALL_HEAD) { + return Material.DRAGON_HEAD.getKey(); + } else if (mat == Material.BAMBOO_SAPLING) { + return Material.BAMBOO.getKey(); + } else if (mat == Material.PISTON_HEAD || mat == Material.MOVING_PISTON) { + TechnicalPiston tp = (TechnicalPiston) b; + if (tp.getType() == TechnicalPiston.Type.NORMAL) { + return Material.PISTON.getKey(); + } else { + return Material.STICKY_PISTON.getKey(); + } + } else if (mat == Material.EXPOSED_COPPER_CHEST || mat == Material.WEATHERED_COPPER_CHEST + || mat == Material.OXIDIZED_COPPER_CHEST || mat == Material.WAXED_COPPER_CHEST + || mat == Material.WAXED_EXPOSED_COPPER_CHEST || mat == Material.WAXED_WEATHERED_COPPER_CHEST + || mat == Material.WAXED_OXIDIZED_COPPER_CHEST) { + return Material.COPPER_CHEST.getKey(); + } + return mat.getKey(); + } + + private int process(Block b, boolean add) { + return process(b, b.getBlockData(), add); + } + + /** + * Check if a block can be placed or needs to be removed based on limits. + * + * @return limit amount if over limit, or -1 if no limitation + */ + private int process(Block b, BlockData blockData, boolean add) { + if (DO_NOT_COUNT.contains(fixMaterial(blockData)) || !addon.inGameModeWorld(b.getWorld())) { + return -1; + } + Environment env = envOf(b.getWorld()); + return addon.getIslands().getIslandAt(b.getLocation()).map(i -> { + String id = i.getUniqueId(); + String gameMode = addon.getGameModeName(b.getWorld()); + if (gameMode.isEmpty()) { + return -1; + } + if (addon.getConfig().getBoolean("ignore-center-block", true) + && i.getCenter().equals(b.getLocation())) { + return -1; + } + islandCountMap.putIfAbsent(id, new IslandBlockCount(id, gameMode)); + NamespacedKey key = fixMaterial(blockData); + if (add) { + int limit = checkLimit(b.getWorld(), env, key, id); + if (limit > -1) { + return limit; + } + islandCountMap.get(id).add(env, key); + } else { + islandCountMap.get(id).remove(env, key); + } + updateSaveMap(id); + return -1; + }).orElse(-1); + } + + /** + * Remove a block from any island limit count + */ + public void removeBlock(Block b) { + addon.getIslands().getIslandAt(b.getLocation()).ifPresent(i -> { + String id = i.getUniqueId(); + String gameMode = addon.getGameModeName(b.getWorld()); + if (gameMode.isEmpty()) return; + Environment env = envOf(b.getWorld()); + islandCountMap.computeIfAbsent(id, k -> new IslandBlockCount(id, gameMode)) + .remove(env, fixMaterial(b.getBlockData())); + updateSaveMap(id); + }); + } + + private void updateSaveMap(String id) { + saveMap.putIfAbsent(id, 0); + if (saveMap.merge(id, 1, Integer::sum) > CHANGE_LIMIT) { + handler.saveObjectAsync(islandCountMap.get(id)); + saveMap.remove(id); + } + } + + /** + * Resolve the active limit for this material in this world for this island. + * Priority: island-env limit, world-named limit, env-default, none. + * + * @return limit if at or over, or -1 if no limit + */ + private int checkLimit(World w, Environment env, NamespacedKey m, String id) { + IslandBlockCount ibc = islandCountMap.get(id); + if (ibc.isBlockLimited(env, m)) { + int offset = ibc.getBlockLimitOffset(env, m); + return ibc.isAtLimit(env, m) ? ibc.getBlockLimit(env, m) + offset : -1; + } + Map worldMap = worldLimitMap.get(w); + if (worldMap != null && worldMap.containsKey(m)) { + int worldLimit = worldMap.get(m); + int offset = ibc.getBlockLimitOffset(env, m); + return ibc.isAtLimit(env, m, worldLimit) ? worldLimit + offset : -1; + } + Map envDefaults = envDefaultLimitMap.get(env); + if (envDefaults != null && envDefaults.containsKey(m)) { + int envLimit = envDefaults.get(m); + int offset = ibc.getBlockLimitOffset(env, m); + if (ibc.isAtLimit(env, m, envLimit)) { + return envLimit + offset; + } + } + return -1; + } + + /** + * Aggregate map of the resolved limits for this island in the given world. + */ + public Map getMaterialLimits(World w, String id) { + Environment env = envOf(w); + Map result = new HashMap<>(); + Map envDefaults = envDefaultLimitMap.get(env); + if (envDefaults != null) { + result.putAll(envDefaults); + } + Map worldMap = worldLimitMap.get(w); + if (worldMap != null) { + result.putAll(worldMap); + } + IslandBlockCount ibc = islandCountMap.get(id); + if (ibc != null) { + result.putAll(ibc.getBlockLimits(env)); + ibc.getBlockLimitsOffset(env).forEach( + (material, offset) -> result.put(material, result.getOrDefault(material, 0) + offset)); + } + return result; + } + + /** + * Per-environment map of the env-default limits, used for tests and external introspection. + */ + public Map> getEnvDefaultLimitMap() { + return envDefaultLimitMap; + } + + public Map> getWorldLimitMap() { + return worldLimitMap; + } + + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onIslandDelete(IslandDeleteEvent e) { + islandCountMap.remove(e.getIsland().getUniqueId()); + saveMap.remove(e.getIsland().getUniqueId()); + if (handler.objectExists(e.getIsland().getUniqueId())) { + handler.deleteID(e.getIsland().getUniqueId()); + } + } + + public void setIsland(String islandId, IslandBlockCount ibc) { + islandCountMap.put(islandId, ibc); + handler.saveObjectAsync(ibc); + } + + @Nullable + public IslandBlockCount getIsland(String islandId) { + return islandCountMap.get(islandId); + } + + @NonNull + public IslandBlockCount getIsland(Island island) { + return islandCountMap.computeIfAbsent(island.getUniqueId(), + k -> new IslandBlockCount(k, island.getGameMode())); + } +} diff --git a/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java b/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java index 145cb00..d8452f9 100644 --- a/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java +++ b/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java @@ -1,6 +1,7 @@ package world.bentobox.limits.listeners; import org.bukkit.*; +import org.bukkit.World.Environment; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.entity.*; @@ -11,6 +12,8 @@ import org.bukkit.event.entity.CreatureSpawnEvent; import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason; import org.bukkit.event.entity.EntityBreedEvent; +import org.bukkit.event.entity.EntityPortalEvent; +import org.bukkit.event.entity.EntityRemoveEvent; import org.bukkit.event.hanging.HangingPlaceEvent; import org.bukkit.event.vehicle.VehicleCreateEvent; import org.eclipse.jdt.annotation.Nullable; @@ -21,6 +24,7 @@ import world.bentobox.bentobox.util.Util; import world.bentobox.limits.EntityGroup; import world.bentobox.limits.Limits; +import world.bentobox.limits.Settings; import world.bentobox.limits.objects.IslandBlockCount; import java.util.*; @@ -28,130 +32,103 @@ /** * Listener for entity limit events and logic. + * + *

Counts are stored persistently per-island per-environment in {@link IslandBlockCount} + * (see {@code envEntityCounts}). This listener increments on successful spawn and + * decrements on {@link EntityRemoveEvent} (excluding chunk unload), so counts remain + * accurate even when nether/end chunks are unloaded — fixes + * issue #43. */ public class EntityLimitListener implements Listener { - /** - * Permission node for bypassing limits. - */ + /** Permission node for bypassing limits. */ private static final String MOD_BYPASS = "mod.bypass"; - /** - * Reference to the Limits addon. - */ private final Limits addon; - /** - * List of entity UUIDs that have just spawned to prevent double-processing. - */ + /** Entity UUIDs that have just spawned to prevent double-processing. */ private final List justSpawned = new ArrayList<>(); - /** - * Cardinal directions used for block structure detection. - */ - private static final List CARDINALS = List.of(BlockFace.UP, BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST, BlockFace.DOWN); + /** Cardinal directions used for block structure detection. */ + private static final List CARDINALS = List.of(BlockFace.UP, BlockFace.NORTH, BlockFace.SOUTH, + BlockFace.EAST, BlockFace.WEST, BlockFace.DOWN); - /** - * Constructs the EntityLimitListener. - * - * @param addon Limits addon instance - */ public EntityLimitListener(Limits addon) { this.addon = addon; } - /** - * Handles minecart placement and checks entity limits. - * - * @param vehicleCreateEvent VehicleCreateEvent instance - */ + private static Environment envOf(World w) { + Environment env = w.getEnvironment(); + return Settings.ENVIRONMENTS.contains(env) ? env : Environment.NORMAL; + } + + /* ========================================================================= + * Limit-check handlers (LOW priority — cancel if at limit) + * ========================================================================= */ + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) public void onMinecart(VehicleCreateEvent vehicleCreateEvent) { - // Return if not in a known world - if (!addon.inGameModeWorld(vehicleCreateEvent.getVehicle().getWorld())) { - return; - } - // Debounce + if (!addon.inGameModeWorld(vehicleCreateEvent.getVehicle().getWorld())) return; if (justSpawned.contains(vehicleCreateEvent.getVehicle().getUniqueId())) { justSpawned.remove(vehicleCreateEvent.getVehicle().getUniqueId()); return; } - // Check island addon.getIslands().getProtectedIslandAt(vehicleCreateEvent.getVehicle().getLocation()) - // Ignore spawn .filter(i -> !i.isSpawn()) .ifPresent(island -> { - // Check if the player is at the limit AtLimitResult res = atLimit(island, vehicleCreateEvent.getVehicle()); if (res.hit()) { vehicleCreateEvent.setCancelled(true); - this.tellPlayers(vehicleCreateEvent.getVehicle().getLocation(), vehicleCreateEvent.getVehicle(), SpawnReason.MOUNT, res); + this.tellPlayers(vehicleCreateEvent.getVehicle().getLocation(), + vehicleCreateEvent.getVehicle(), SpawnReason.MOUNT, res); } }); } - /** - * Handles entity breeding and checks entity limits. - * - * @param entityBreedEvent EntityBreedEvent instance - */ @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) public void onBreed(final EntityBreedEvent entityBreedEvent) { if (addon.inGameModeWorld(entityBreedEvent.getEntity().getWorld()) && entityBreedEvent.getBreeder() != null && (entityBreedEvent.getBreeder() instanceof Player player) - && !(player.isOp() || player.hasPermission(addon.getPlugin().getIWM().getPermissionPrefix(entityBreedEvent.getEntity().getWorld()) + MOD_BYPASS)) + && !(player.isOp() || player.hasPermission(addon.getPlugin().getIWM() + .getPermissionPrefix(entityBreedEvent.getEntity().getWorld()) + MOD_BYPASS)) && !checkLimit(entityBreedEvent, entityBreedEvent.getEntity(), SpawnReason.BREEDING, false) - && entityBreedEvent.getFather() instanceof Breedable father && entityBreedEvent.getMother() instanceof Breedable mother) { + && entityBreedEvent.getFather() instanceof Breedable father + && entityBreedEvent.getMother() instanceof Breedable mother) { father.setBreed(false); mother.setBreed(false); } } - /** - * Handles creature spawning and checks entity limits. - * - * @param creatureSpawnEvent CreatureSpawnEvent instance - */ @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) public void onCreatureSpawn(final CreatureSpawnEvent creatureSpawnEvent) { - // Return if not in a known world - if (!addon.inGameModeWorld(creatureSpawnEvent.getLocation().getWorld())) { - return; - } + if (!addon.inGameModeWorld(creatureSpawnEvent.getLocation().getWorld())) return; if (justSpawned.contains(creatureSpawnEvent.getEntity().getUniqueId())) { justSpawned.remove(creatureSpawnEvent.getEntity().getUniqueId()); return; } - if (creatureSpawnEvent.getSpawnReason().equals(SpawnReason.SHOULDER_ENTITY) || (!(creatureSpawnEvent.getEntity() instanceof Villager) && creatureSpawnEvent.getSpawnReason().equals(SpawnReason.BREEDING))) { - // Special case - do nothing - jumping around spawns parrots as they drop off player's shoulder - // Ignore breeding because it's handled in the EntityBreedEvent listener + if (creatureSpawnEvent.getSpawnReason().equals(SpawnReason.SHOULDER_ENTITY) + || (!(creatureSpawnEvent.getEntity() instanceof Villager) + && creatureSpawnEvent.getSpawnReason().equals(SpawnReason.BREEDING))) { return; } - // Some checks can be done async, some not - if (creatureSpawnEvent.getSpawnReason().equals(SpawnReason.BUILD_SNOWMAN) || creatureSpawnEvent.getSpawnReason().equals(SpawnReason.BUILD_IRONGOLEM) ) { - checkLimit(creatureSpawnEvent, creatureSpawnEvent.getEntity(), creatureSpawnEvent.getSpawnReason(), addon.getSettings().isAsyncGolums()); + if (creatureSpawnEvent.getSpawnReason().equals(SpawnReason.BUILD_SNOWMAN) + || creatureSpawnEvent.getSpawnReason().equals(SpawnReason.BUILD_IRONGOLEM)) { + checkLimit(creatureSpawnEvent, creatureSpawnEvent.getEntity(), creatureSpawnEvent.getSpawnReason(), + addon.getSettings().isAsyncGolums()); } else { - // Check limit sync - checkLimit(creatureSpawnEvent, creatureSpawnEvent.getEntity(), creatureSpawnEvent.getSpawnReason(), false); + checkLimit(creatureSpawnEvent, creatureSpawnEvent.getEntity(), creatureSpawnEvent.getSpawnReason(), + false); } - } - /** - * handles paintings and item frames - * - * @param hangingPlaceEvent - event - */ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onBlock(HangingPlaceEvent hangingPlaceEvent) { - if (!addon.inGameModeWorld(hangingPlaceEvent.getBlock().getWorld())) { - return; - } + if (!addon.inGameModeWorld(hangingPlaceEvent.getBlock().getWorld())) return; Player player = hangingPlaceEvent.getPlayer(); if (player == null) return; addon.getIslands().getIslandAt(hangingPlaceEvent.getEntity().getLocation()).ifPresent(island -> { - boolean bypass = Objects.requireNonNull(player).isOp() || player.hasPermission(addon.getPlugin().getIWM().getPermissionPrefix(hangingPlaceEvent.getEntity().getWorld()) + MOD_BYPASS); - // Check if entity can be hung + boolean bypass = Objects.requireNonNull(player).isOp() || player.hasPermission( + addon.getPlugin().getIWM().getPermissionPrefix(hangingPlaceEvent.getEntity().getWorld()) + + MOD_BYPASS); AtLimitResult res; if (!bypass && !island.isSpawn() && (res = atLimit(island, hangingPlaceEvent.getEntity())).hit()) { - // Not allowed hangingPlaceEvent.setCancelled(true); if (res.getTypelimit() != null) { User.getInstance(player).notify("block-limits.hit-limit", "[material]", @@ -159,23 +136,112 @@ public void onBlock(HangingPlaceEvent hangingPlaceEvent) { TextVariables.NUMBER, String.valueOf(res.getTypelimit().getValue())); } else { User.getInstance(player).notify("block-limits.hit-limit", "[material]", - res.getGrouplimit().getKey().getName() + " (" + res.getGrouplimit().getKey().getTypes().stream().map(x -> Util.prettifyText(x.toString())).collect(Collectors.joining(", ")) + ")", + res.getGrouplimit().getKey().getName() + " (" + + res.getGrouplimit().getKey().getTypes().stream() + .map(x -> Util.prettifyText(x.toString())) + .collect(Collectors.joining(", ")) + + ")", TextVariables.NUMBER, String.valueOf(res.getGrouplimit().getValue())); } } }); } - /** - * Checks if a creature is allowed to spawn or not. - * - * @param cancelableEvent Cancelable event instance - * @param livingEntity Entity being spawned - * @param spawnReason Reason for spawning - * @param runAsync Whether to run asynchronously - * @return true if allowed or async, false if not - */ - private boolean checkLimit(Cancellable cancelableEvent, LivingEntity livingEntity, SpawnReason spawnReason, boolean runAsync) { + /* ========================================================================= + * Increment handlers (MONITOR priority — count entities that actually spawned) + * ========================================================================= */ + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onCreatureSpawnTrack(final CreatureSpawnEvent e) { + trackSpawn(e.getEntity()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onVehicleCreateTrack(final VehicleCreateEvent e) { + trackSpawn(e.getVehicle()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onHangingPlaceTrack(final HangingPlaceEvent e) { + trackSpawn(e.getEntity()); + } + + private void trackSpawn(Entity entity) { + if (entity == null) return; + World w = entity.getWorld(); + if (!addon.inGameModeWorld(w)) return; + addon.getIslands().getIslandAt(entity.getLocation()) + .filter(island -> !island.isSpawn()) + .ifPresent(island -> { + IslandBlockCount ibc = addon.getBlockLimitListener().getIsland(island); + ibc.incrementEntity(envOf(w), entity.getType()); + }); + } + + /* ========================================================================= + * Decrement on permanent removal + * ========================================================================= */ + + @EventHandler(priority = EventPriority.MONITOR) + public void onEntityRemove(EntityRemoveEvent e) { + // Entities unloaded with their chunk are still alive — don't decrement. + if (e.getCause() == EntityRemoveEvent.Cause.UNLOAD) return; + Entity entity = e.getEntity(); + World w = entity.getWorld(); + if (!addon.inGameModeWorld(w)) return; + addon.getIslands().getIslandAt(entity.getLocation()) + .filter(island -> !island.isSpawn()) + .ifPresent(island -> { + IslandBlockCount ibc = addon.getBlockLimitListener().getIsland(island.getUniqueId()); + if (ibc != null) { + ibc.decrementEntity(envOf(w), entity.getType()); + } + }); + } + + /* ========================================================================= + * Portal: move count between envs on same island + * ========================================================================= */ + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onEntityPortal(EntityPortalEvent e) { + if (e.getTo() == null || e.getTo().getWorld() == null) return; + Entity entity = e.getEntity(); + World fromWorld = entity.getWorld(); + World toWorld = e.getTo().getWorld(); + Environment fromEnv = envOf(fromWorld); + Environment toEnv = envOf(toWorld); + if (fromEnv == toEnv) return; + if (!addon.inGameModeWorld(fromWorld) && !addon.inGameModeWorld(toWorld)) return; + + // Decrement at source if on a tracked island + if (addon.inGameModeWorld(fromWorld)) { + addon.getIslands().getIslandAt(entity.getLocation()) + .filter(island -> !island.isSpawn()) + .ifPresent(island -> { + IslandBlockCount ibc = addon.getBlockLimitListener().getIsland(island.getUniqueId()); + if (ibc != null) { + ibc.decrementEntity(fromEnv, entity.getType()); + } + }); + } + // Increment at destination if on a tracked island + if (addon.inGameModeWorld(toWorld)) { + addon.getIslands().getIslandAt(e.getTo()) + .filter(island -> !island.isSpawn()) + .ifPresent(island -> { + IslandBlockCount ibc = addon.getBlockLimitListener().getIsland(island); + ibc.incrementEntity(toEnv, entity.getType()); + }); + } + } + + /* ========================================================================= + * Limit-checking core + * ========================================================================= */ + + private boolean checkLimit(Cancellable cancelableEvent, LivingEntity livingEntity, SpawnReason spawnReason, + boolean runAsync) { Location l = livingEntity.getLocation(); if (runAsync) { cancelableEvent.setCancelled(true); @@ -183,72 +249,45 @@ private boolean checkLimit(Cancellable cancelableEvent, LivingEntity livingEntit return processIsland(cancelableEvent, livingEntity, l, spawnReason, runAsync); } - /** - * Processes the island for entity limit checks. - * - * @param cancelableEvent Cancelable event instance - * @param livingEntity Entity being spawned - * @param location Location of entity - * @param spawnReason Reason for spawning - * @param runAsync Whether to run asynchronously - * @return true if allowed, false if not - */ - private boolean processIsland(Cancellable cancelableEvent, LivingEntity livingEntity, Location location, SpawnReason spawnReason, boolean runAsync) { + private boolean processIsland(Cancellable cancelableEvent, LivingEntity livingEntity, Location location, + SpawnReason spawnReason, boolean runAsync) { if (addon.getIslands().getIslandAt(livingEntity.getLocation()).isEmpty()) { cancelableEvent.setCancelled(false); return true; } Island island = addon.getIslands().getIslandAt(livingEntity.getLocation()).get(); - // Check if creature is allowed to spawn or not AtLimitResult res = atLimit(island, livingEntity); if (island.isSpawn() || !res.hit()) { - // Allowed if (runAsync) { - Bukkit.getScheduler().runTask(BentoBox.getInstance(), () -> preSpawn(livingEntity.getType(), spawnReason, location)); - } // else do nothing + Bukkit.getScheduler().runTask(BentoBox.getInstance(), + () -> preSpawn(livingEntity.getType(), spawnReason, location)); + } } else { if (runAsync) { livingEntity.remove(); } else { cancelableEvent.setCancelled(true); } - // If the reason is anything but because of a spawner then tell players within range tellPlayers(location, livingEntity, spawnReason, res); return false; } return true; } - /** - * Spawns an entity and handles special cases for golems, snowmen, and withers. - * - * @param entityType Type of entity to spawn - * @param spawnReason Reason for spawning - * @param location Location to spawn - */ private void preSpawn(EntityType entityType, SpawnReason spawnReason, Location location) { - // Check for entities that need cleanup switch (spawnReason) { case BUILD_IRONGOLEM -> detectIronGolem(location); case BUILD_SNOWMAN -> detectSnowman(location); - case BUILD_WITHER -> { // Not used right now - detectWither(location); - } + case BUILD_WITHER -> detectWither(location); default -> throw new IllegalArgumentException("Unexpected value: " + spawnReason); } Entity entity = location.getWorld().spawnEntity(location, entityType); justSpawned.add(entity.getUniqueId()); if (spawnReason == SpawnReason.BUILD_WITHER) { - // Create explosion location.getWorld().createExplosion(location, 7F, true, true, entity); } } - /** - * Detects and removes iron golem construction blocks. - * - * @param location Location of golem spawn - */ private void detectIronGolem(Location location) { Block legs = location.getBlock(); addon.getBlockLimitListener().removeBlock(legs); @@ -289,41 +328,25 @@ private boolean eraseGolemIfArmsMatch(Block body, Block head, BlockFace bf) { return false; } - /** - * Detects and removes snowman construction blocks. - * - * @param location Location of snowman spawn - */ private void detectSnowman(Location location) { Block legs = location.getBlock(); - // Erase legs addon.getBlockLimitListener().removeBlock(legs); legs.setType(Material.AIR); - // Look around for possible constructions for (BlockFace bf : CARDINALS) { Block body = legs.getRelative(bf); if (body.getType().equals(Material.SNOW_BLOCK)) { - // Check for head Block head = body.getRelative(bf); if (head.getType() == Material.CARVED_PUMPKIN || head.getType() == Material.JACK_O_LANTERN) { - // Erase addon.getBlockLimitListener().removeBlock(body); addon.getBlockLimitListener().removeBlock(head); - body.setType(Material.AIR); head.setType(Material.AIR); return; } } } - } - /** - * Detects and removes wither construction blocks. - * - * @param location Location of wither spawn - */ private void detectWither(Location location) { Block legs = location.getBlock(); addon.getBlockLimitListener().removeBlock(legs); @@ -340,7 +363,8 @@ private void detectWither(Location location) { } private boolean isWitherHead(Block block) { - return block.getType().equals(Material.WITHER_SKELETON_SKULL) || block.getType().equals(Material.WITHER_SKELETON_WALL_SKULL); + return block.getType().equals(Material.WITHER_SKELETON_SKULL) + || block.getType().equals(Material.WITHER_SKELETON_WALL_SKULL); } private boolean eraseWitherIfArmsMatch(Block body, Block head, BlockFace bf) { @@ -371,12 +395,6 @@ && isWitherHead(head2) && isWitherHead(head3)) { return false; } - /** - * Checks if the block is a valid wither base block. - * - * @param block Block to check - * @return true if block is a wither base block - */ private boolean isWither(Block block) { if (Util.getMinecraftVersion() < 16) { return block.getType().equals(Material.SOUL_SAND); @@ -384,15 +402,7 @@ private boolean isWither(Block block) { return Tag.WITHER_SUMMON_BASE_BLOCKS.isTagged(block.getType()); } - /** - * Notifies players within a 5x5x5 radius that entity spawning was denied. - * - * @param location Location of denied spawn - * @param entity Entity that was denied - * @param spawnReason Reason for spawning - * @param atLimitResult Result of limit check - */ - private void tellPlayers(Location location, Entity entity, SpawnReason spawnReason, AtLimitResult atLimitResult) { + private void tellPlayers(Location location, Entity entity, SpawnReason spawnReason, AtLimitResult res) { if (spawnReason.equals(SpawnReason.SPAWNER) || spawnReason.equals(SpawnReason.NATURAL) || spawnReason.equals(SpawnReason.INFECTION) || spawnReason.equals(SpawnReason.NETHER_PORTAL) || spawnReason.equals(SpawnReason.REINFORCEMENTS) || spawnReason.equals(SpawnReason.SLIME_SPLIT)) { @@ -404,104 +414,72 @@ private void tellPlayers(Location location, Entity entity, SpawnReason spawnReas for (Entity ent : w.getNearbyEntities(location, 5, 5, 5)) { if (ent instanceof Player p) { p.updateInventory(); - if (atLimitResult.getTypelimit() != null) { + if (res.getTypelimit() != null) { User.getInstance(p).notify("entity-limits.hit-limit", "[entity]", Util.prettifyText(entity.getType().toString()), - TextVariables.NUMBER, String.valueOf(atLimitResult.getTypelimit().getValue())); + TextVariables.NUMBER, String.valueOf(res.getTypelimit().getValue())); } else { User.getInstance(p).notify("entity-limits.hit-limit", "[entity]", - atLimitResult.getGrouplimit().getKey().getName() + " (" + atLimitResult.getGrouplimit().getKey().getTypes().stream().map(x -> Util.prettifyText(x.toString())).collect(Collectors.joining(", ")) + ")", - TextVariables.NUMBER, String.valueOf(atLimitResult.getGrouplimit().getValue())); + res.getGrouplimit().getKey().getName() + " (" + + res.getGrouplimit().getKey().getTypes().stream() + .map(x -> Util.prettifyText(x.toString())) + .collect(Collectors.joining(", ")) + + ")", + TextVariables.NUMBER, String.valueOf(res.getGrouplimit().getValue())); } } } }); - } /** - * Checks if new entities can be added to the island. - * - * @param island Island to check - * @param entity Entity to check - * @return AtLimitResult indicating if at limit + * Check whether a new entity of the given type would put the island over its limit + * for the entity's current environment. */ AtLimitResult atLimit(Island island, Entity entity) { - // Check island settings first - int limitAmount = -1; - Map groupsLimits = new HashMap<>(); - + Environment env = envOf(entity.getWorld()); + EntityType type = entity.getType(); @Nullable IslandBlockCount ibc = addon.getBlockLimitListener().getIsland(island.getUniqueId()); + + // 1. Resolve effective per-type limit + int limitAmount = -1; if (ibc != null) { - // Get the limit amount for this type - limitAmount = ibc.getEntityLimit(entity.getType()); - // Handle entity groups from island settings - collectIslandGroupLimits(ibc, entity, groupsLimits); + limitAmount = ibc.getEntityLimit(env, type); } - // If no island settings then try global settings - if (limitAmount < 0 && addon.getSettings().getLimits().containsKey(entity.getType())) { - limitAmount = addon.getSettings().getLimits().get(entity.getType()); + if (limitAmount < 0) { + Integer cfg = addon.getSettings().getLimits(env).get(type); + if (cfg != null) limitAmount = cfg; } - // Apply global group limits where not already set by island settings - applyGlobalGroupLimits(entity, groupsLimits); + + // 2. Build effective group-limits for this env, applying island and config overrides + Map groupsLimits = new HashMap<>(); + List groupdefs = addon.getSettings().getGroupLimits().getOrDefault(type, Collections.emptyList()); + Map envGroupCfg = addon.getSettings().getGroupLimits(env); + Map islandGroupOverrides = ibc == null ? Collections.emptyMap() : ibc.getEntityGroupLimits(env); + for (EntityGroup def : groupdefs) { + int limit = islandGroupOverrides.getOrDefault(def.getName(), envGroupCfg.getOrDefault(def.getName(), -1)); + if (limit >= 0) { + groupsLimits.put(def, limit); + } + } + if (limitAmount < 0 && groupsLimits.isEmpty()) { return new AtLimitResult(); } - // We have to count the entities + // 3. Read counts from the IBC; use 0 if no IBC has been created yet if (limitAmount >= 0) { - int count = (int) entity.getWorld().getNearbyEntities(island.getBoundingBox()).stream() - .filter(e -> e.getType().equals(entity.getType())) - .count(); - int max = limitAmount + (ibc == null ? 0 : ibc.getEntityLimitOffset(entity.getType())); + int count = ibc == null ? 0 : ibc.getEntityCount(env, type); + int max = limitAmount + (ibc == null ? 0 : ibc.getEntityLimitOffset(env, type)); if (count >= max) { - return new AtLimitResult(entity.getType(), max); + return new AtLimitResult(type, max); } } - // Update group limits from island-specific overrides - updateGroupLimitsFromIbc(ibc, groupsLimits); - // Now do the group limits - return checkGroupLimits(island, entity, ibc, groupsLimits); - } - - private void collectIslandGroupLimits(IslandBlockCount ibc, Entity entity, Map groupsLimits) { - List groupdefs = addon.getSettings().getGroupLimits().getOrDefault(entity.getType(), - new ArrayList<>()); - groupdefs.forEach(def -> { - int limit = ibc.getEntityGroupLimit(def.getName()); - if (limit >= 0) - groupsLimits.put(def, limit); - }); - } - - private void applyGlobalGroupLimits(Entity entity, Map groupsLimits) { - if (addon.getSettings().getGroupLimits().containsKey(entity.getType())) { - addon.getSettings().getGroupLimits().getOrDefault(entity.getType(), new ArrayList<>()).stream() - .filter(group -> !groupsLimits.containsKey(group) || groupsLimits.get(group) > group.getLimit()) - .forEach(group -> groupsLimits.put(group, group.getLimit())); - } - } - - private void updateGroupLimitsFromIbc(@Nullable IslandBlockCount ibc, Map groupsLimits) { - if (ibc != null) { - Map groupbyname = groupsLimits.keySet().stream() - .collect(Collectors.toMap(EntityGroup::getName, e -> e)); - ibc.getEntityGroupLimits().entrySet().stream() - .filter(e -> groupbyname.containsKey(e.getKey())) - .forEach(e -> groupsLimits.put(groupbyname.get(e.getKey()), e.getValue())); - } - } - - private AtLimitResult checkGroupLimits(Island island, Entity entity, @Nullable IslandBlockCount ibc, - Map groupsLimits) { - for (Map.Entry group : groupsLimits.entrySet()) { //do not use lambda - if (group.getValue() < 0) - continue; - int count = (int) entity.getWorld().getNearbyEntities(island.getBoundingBox()).stream() - .filter(e -> group.getKey().contains(e.getType())) - .count(); - int max = group.getValue() + (ibc == null ? 0 : ibc.getEntityGroupLimitOffset(group.getKey().getName())); + for (Map.Entry group : groupsLimits.entrySet()) { + if (group.getValue() < 0) continue; + int count = sumGroupCount(ibc, env, group.getKey()); + int max = group.getValue() + (ibc == null ? 0 : ibc.getEntityGroupLimitOffset(env, group.getKey().getName())); if (count >= max) { return new AtLimitResult(group.getKey(), max); } @@ -509,66 +487,41 @@ private AtLimitResult checkGroupLimits(Island island, Entity entity, @Nullable I return new AtLimitResult(); } - /** - * Result class for entity limit checks. - */ + private int sumGroupCount(@Nullable IslandBlockCount ibc, Environment env, EntityGroup group) { + if (ibc == null) return 0; + Map counts = ibc.getEntityCounts(env); + int total = 0; + for (EntityType t : group.getTypes()) { + total += counts.getOrDefault(t, 0); + } + return total; + } + static class AtLimitResult { private Map.Entry typelimit; private Map.Entry grouplimit; - /** - * Default constructor for AtLimitResult. - */ public AtLimitResult() { } - /** - * Constructor for type limit result. - * - * @param type EntityType at limit - * @param limit Limit value - */ public AtLimitResult(EntityType type, int limit) { typelimit = new AbstractMap.SimpleEntry<>(type, limit); } - /** - * Constructor for group limit result. - * - * @param type EntityGroup at limit - * @param limit Limit value - */ public AtLimitResult(EntityGroup type, int limit) { grouplimit = new AbstractMap.SimpleEntry<>(type, limit); } - /** - * Returns true if at limit. - * - * @return true if at limit - */ public boolean hit() { return typelimit != null || grouplimit != null; } - /** - * Gets the type limit entry. - * - * @return Entry of EntityType and limit - */ public Map.Entry getTypelimit() { return typelimit; } - /** - * Gets the group limit entry. - * - * @return Entry of EntityGroup and limit - */ public Map.Entry getGrouplimit() { return grouplimit; } } } - - diff --git a/src/main/java/world/bentobox/limits/listeners/JoinListener.java b/src/main/java/world/bentobox/limits/listeners/JoinListener.java index 386a322..948e837 100644 --- a/src/main/java/world/bentobox/limits/listeners/JoinListener.java +++ b/src/main/java/world/bentobox/limits/listeners/JoinListener.java @@ -1,349 +1,314 @@ -package world.bentobox.limits.listeners; - -import java.util.Arrays; -import java.util.Locale; -import java.util.Objects; -import java.util.UUID; - -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.OfflinePlayer; -import org.bukkit.World; -import org.bukkit.entity.EntityType; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.permissions.PermissionAttachmentInfo; -import org.eclipse.jdt.annotation.NonNull; - -import world.bentobox.bentobox.api.events.island.IslandEvent; -import world.bentobox.bentobox.api.events.island.IslandEvent.Reason; -import world.bentobox.bentobox.api.events.team.TeamSetownerEvent; -import world.bentobox.bentobox.database.objects.Island; -import world.bentobox.limits.EntityGroup; -import world.bentobox.limits.Limits; -import world.bentobox.limits.events.LimitsJoinPermCheckEvent; -import world.bentobox.limits.events.LimitsPermCheckEvent; -import world.bentobox.limits.objects.IslandBlockCount; - -/** - * This listener handles events related to players joining, island creation, and ownership changes - * to apply permission-based block, entity, and entity group limits to islands. - * It checks player permissions and dynamically adjusts the limits for each island they own. - * - * @author tastybento - * - */ -public class JoinListener implements Listener { - - private final Limits addon; - - /** - * Constructs the listener. - * @param addon The Limits addon instance. - */ - public JoinListener(Limits addon) { - this.addon = addon; - } - - /** - * Checks a player's permissions and applies any limits to a specific island. - * The permissions are expected to be in the format: [permissionPrefix].[MATERIAL|ENTITY|GROUP]. - * E.g., "bskyblock.island.limit.COBBLESTONE.100" - * - * @param player - The player whose permissions are being checked. - * @param permissionPrefix - The prefix for the limit permissions (e.g., "bskyblock.island.limit."). - * @param islandId - The unique ID of the island to apply limits to. - * @param gameMode - The name of the game mode the island belongs to. - */ - public void checkPerms(Player player, String permissionPrefix, String islandId, String gameMode) { - // Get the existing block counts and limits for the island. - IslandBlockCount islandBlockCount = addon.getBlockLimitListener().getIsland(islandId); - // Check permissions - if (islandBlockCount != null) { - // Clear any previously set permission-based limits before re-evaluating. - // This ensures that limits are fresh on each check and removed permissions are respected. - islandBlockCount.getEntityLimits().clear(); - islandBlockCount.getEntityGroupLimits().clear(); - islandBlockCount.getBlockLimits().clear(); - } - // Iterate through all effective permissions of the player. - for (PermissionAttachmentInfo permissionInfo : player.getEffectivePermissions()) { - // Skip permissions that are not set to true, don't match the prefix, or have bad syntax. - if (!permissionInfo.getValue() || !permissionInfo.getPermission().startsWith(permissionPrefix) - || badSyntaxCheck(permissionInfo, player.getName(), permissionPrefix)) { - continue; - } - // Split the permission string to parse it. E.g., "bskyblock.island.limit.COBBLESTONE.100" - String[] permissionParts = permissionInfo.getPermission().split("\\."); - // Try to match the relevant part of the permission to an EntityType, Material, or a defined EntityGroup. - EntityType entityType = Arrays.stream(EntityType.values()).filter(type -> type.name().equalsIgnoreCase(permissionParts[3])) - .findFirst().orElse(null); - Material material = Arrays.stream(Material.values()).filter(mat -> mat.name().equalsIgnoreCase(permissionParts[3])).findFirst() - .orElse(null); - EntityGroup entityGroup = addon.getSettings().getGroupLimitDefinitions().stream() - .filter(group -> group.getName().equalsIgnoreCase(permissionParts[3])).findFirst().orElse(null); - - // If the permission part is not a valid type, log an error and stop processing this permission. - if (entityGroup == null && entityType == null && material == null) { - logError(player.getName(), permissionInfo.getPermission(), - permissionParts[3].toUpperCase(Locale.ENGLISH) + " is not a valid material or entity type/group."); - break; - } - // If this is the first limit being applied, create a new IslandBlockCount object. - if (islandBlockCount == null) { - islandBlockCount = new IslandBlockCount(islandId, gameMode); - } - // The last part of the permission is the limit value. - int limitValue = Integer.parseInt(permissionParts[4]); - logIfEnabled("Setting login limit via perm for " + player.getName() + "..."); - - // Fire a custom event to allow other plugins to modify or cancel the limit application. - LimitsPermCheckEvent limitsPermCheckEvent = new LimitsPermCheckEvent(player, islandId, islandBlockCount, entityGroup, entityType, material, limitValue); - Bukkit.getPluginManager().callEvent(limitsPermCheckEvent); - if (limitsPermCheckEvent.isCancelled()) { - addon.log("Permissions not set because another addon/plugin canceled setting."); - continue; - } - // Update local variables with any changes from the event. - islandBlockCount = limitsPermCheckEvent.getIbc(); - // If the event handler nulled the IslandBlockCount, create a new one. - if (islandBlockCount == null) { - islandBlockCount = new IslandBlockCount(islandId, gameMode); - } - // Apply the limit to the island's block/entity counts. - runNullCheckAndSet(islandBlockCount, limitsPermCheckEvent); - } - // After checking all permissions, save the updated IslandBlockCount if it has been modified. - if (islandBlockCount != null) - addon.getBlockLimitListener().setIsland(islandId, islandBlockCount); - } - - /** - * Validates the syntax of a limit permission string. - * @param permissionInfo The permission to check. - * @param playerName The name of the player for logging purposes. - * @param permissionPrefix The required prefix for the permission. - * @return true if the syntax is bad, false otherwise. - */ - private boolean badSyntaxCheck(PermissionAttachmentInfo permissionInfo, String playerName, String permissionPrefix) { - // Wildcard permissions are not supported for limits. - if (permissionInfo.getPermission().contains(permissionPrefix + "*")) { - logError(playerName, permissionInfo.getPermission(), "wildcards are not allowed."); - return true; - } - // The permission must have exactly 5 parts separated by dots. - String[] permissionParts = permissionInfo.getPermission().split("\\."); - if (permissionParts.length != 5) { - logError(playerName, permissionInfo.getPermission(), "format must be '" + permissionPrefix + "MATERIAL.NUMBER', '" - + permissionPrefix + "ENTITY-TYPE.NUMBER', or '" + permissionPrefix + "ENTITY-GROUP.NUMBER'"); - return true; - } - // The last part of the permission must be a valid integer. - try { - Integer.parseInt(permissionParts[4]); - } catch (Exception e) { - logError(playerName, permissionInfo.getPermission(), "the last part MUST be an integer!"); - return true; - } - return false; - } - - /** - * Applies the limit from a permission check event to the island's block/entity counts. - * It ensures that if multiple permissions grant a limit for the same thing, the highest limit is used. - * @param islandBlockCount The island's count object to modify. - * @param event The event containing the limit information. - */ - private void runNullCheckAndSet(@NonNull IslandBlockCount islandBlockCount, @NonNull LimitsPermCheckEvent event) { - EntityGroup entityGroup = event.getEntityGroup(); - EntityType entityType = event.getEntityType(); - Material material = event.getMaterial(); - int limitValue = event.getValue(); - if (entityGroup != null) { - int newLimit = Math.max(islandBlockCount.getEntityGroupLimit(entityGroup.getName()), limitValue); - islandBlockCount.setEntityGroupLimit(entityGroup.getName(), newLimit); - logIfEnabled("Setting group limit " + entityGroup.getName() + " " + newLimit); - } else if (entityType != null && material == null) { - int newLimit = Math.max(islandBlockCount.getEntityLimit(entityType), limitValue); - islandBlockCount.setEntityLimit(entityType, newLimit); - logIfEnabled("Setting entity limit " + entityType + " " + newLimit); - } else if (material != null && entityType == null) { - int newLimit = Math.max(islandBlockCount.getBlockLimit(material.getKey()), limitValue); - logIfEnabled("Setting block limit " + material + " " + newLimit); - islandBlockCount.setBlockLimit(material.getKey(), newLimit); - } else { - applyAmbiguousLimit(islandBlockCount, entityType, material, limitValue); - } - } - - private void applyAmbiguousLimit(@NonNull IslandBlockCount islandBlockCount, EntityType entityType, Material material, int limitValue) { - if (material != null && material.isBlock()) { - int newLimit = Math.max(islandBlockCount.getBlockLimit(material.getKey()), limitValue); - logIfEnabled("Setting block limit " + material + " " + newLimit); - islandBlockCount.setBlockLimit(material.getKey(), newLimit); - } else if (entityType != null) { - int newLimit = Math.max(islandBlockCount.getEntityLimit(entityType), limitValue); - logIfEnabled("Setting entity limit " + entityType + " " + newLimit); - islandBlockCount.setEntityLimit(entityType, newLimit); - } - } - - private void logIfEnabled(String message) { - if (addon.getSettings().isLogLimitsOnJoin()) { - addon.log(message); - } - } - - /** - * Logs an error message related to a permission. - * @param playerName The player with the problematic permission. - * @param permission The permission string. - * @param errorMessage The error description. - */ - private void logError(String playerName, String permission, String errorMessage) { - addon.logError("Player " + playerName + " has permission: '" + permission + "' but " + errorMessage + " Ignoring..."); - } - - /* - * Event handling - */ - - /** - * Handles island creation, reset, and registration events. - * When a new island is made available, set the owner's permission-based limits. - * @param event The island event. - */ - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onNewIsland(IslandEvent event) { - if (!event.getReason().equals(Reason.CREATED) && !event.getReason().equals(Reason.RESETTED) - && !event.getReason().equals(Reason.REGISTERED)) { - return; - } - setOwnerPerms(event.getIsland(), event.getOwner()); - } - - /** - * Handles island ownership changes. - * Removes the old owner's limits and applies the new owner's limits. - * @param event The team set owner event. - */ - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onOwnerChange(TeamSetownerEvent event) { - removeOwnerPerms(event.getIsland()); - setOwnerPerms(event.getIsland(), event.getNewOwner()); - } - - /** - * Handles player join events. - * When a player joins, iterate through all game modes and check their owned islands - * to ensure their limits are up-to-date. - * @param event The player join event. - */ - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) - public void onPlayerJoin(PlayerJoinEvent event) { - // Check if player has any islands in the game modes - addon.getGameModes().forEach(gameMode -> { - addon.getIslands().getIslands(gameMode.getOverWorld(), event.getPlayer().getUniqueId()).stream() - // Filter to only islands they own. - .filter(island -> event.getPlayer().getUniqueId().equals(island.getOwner())) - .map(Island::getUniqueId).forEach(islandId -> { - IslandBlockCount islandBlockCount = addon.getBlockLimitListener().getIsland(islandId); - // Fire an event to see if this check should be cancelled, then run the permission check. - if (!joinEventCheck(event.getPlayer(), islandId, islandBlockCount)) { - checkPerms(event.getPlayer(), gameMode.getPermissionPrefix() + "island.limit.", islandId, - gameMode.getDescription().getName()); - } - }); - }); - } - - /** - * Fires a custom event before applying permissions on player join. - * This allows other addons to cancel the permission check or provide a custom IslandBlockCount object. - * - * @param player The player joining. - * @param islandId The ID of the island being checked. - * @param islandBlockCount The current IslandBlockCount for the island. - * @return true if the permission check should be cancelled, false otherwise. - */ - private boolean joinEventCheck(Player player, String islandId, IslandBlockCount islandBlockCount) { - // Fire event, so other addons can cancel this permissions change - LimitsJoinPermCheckEvent event = new LimitsJoinPermCheckEvent(player, islandId, islandBlockCount); - Bukkit.getPluginManager().callEvent(event); - if (event.isCancelled()) { - return true; - } - // Get islandBlockCount from event if it has changed - islandBlockCount = event.getIbc(); - // If perms should be ignored, but the IBC given in the event used, then set it - // and return true to cancel the normal permission check. - if (event.isIgnorePerms() && islandBlockCount != null) { - addon.getBlockLimitListener().setIsland(islandId, islandBlockCount); - return true; - } - return false; - } - - /** - * Handles island un-registration (deletion). - * Removes any limits associated with the island. - * @param event The island event. - */ - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onUnregisterIsland(IslandEvent event) { - if (!event.getReason().equals(Reason.UNREGISTERED)) { - return; - } - removeOwnerPerms(event.getIsland()); - } - - /* - * Utility methods - */ - - /** - * Removes all permission-based limits from an island. - * This is typically done when an owner changes or the island is deleted. - * @param island The island to clear limits from. - */ - private void removeOwnerPerms(Island island) { - World world = island.getWorld(); - if (addon.inGameModeWorld(world)) { - IslandBlockCount islandBlockCount = addon.getBlockLimitListener().getIsland(island.getUniqueId()); - if (islandBlockCount != null) { - // Just clear the maps. This preserves any actual block counts. - islandBlockCount.getBlockLimits().clear(); - islandBlockCount.getEntityLimits().clear(); - islandBlockCount.getEntityGroupLimits().clear(); - } - } - } - - /** - * Applies permission-based limits for an island's owner. - * It only runs if the owner is online. - * @param island The island to apply limits to. - * @param ownerUUID The UUID of the owner. - */ - private void setOwnerPerms(Island island, UUID ownerUUID) { - World world = island.getWorld(); - if (addon.inGameModeWorld(world)) { - // Check if owner is online - OfflinePlayer owner = Bukkit.getOfflinePlayer(ownerUUID); - if (owner.isOnline()) { - // Set perm-based limits - String permissionPrefix = addon.getGameModePermPrefix(world); - String gameModeName = addon.getGameModeName(world); - if (!permissionPrefix.isEmpty() && !gameModeName.isEmpty() && owner.getPlayer() != null) { - // Run the main permission check logic. - checkPerms(Objects.requireNonNull(owner.getPlayer()), permissionPrefix + "island.limit.", - island.getUniqueId(), gameModeName); - } - } - } - } - -} +package world.bentobox.limits.listeners; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.OfflinePlayer; +import org.bukkit.World; +import org.bukkit.World.Environment; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.permissions.PermissionAttachmentInfo; +import org.eclipse.jdt.annotation.NonNull; + +import world.bentobox.bentobox.api.events.island.IslandEvent; +import world.bentobox.bentobox.api.events.island.IslandEvent.Reason; +import world.bentobox.bentobox.api.events.team.TeamSetownerEvent; +import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.limits.EntityGroup; +import world.bentobox.limits.Limits; +import world.bentobox.limits.Settings; +import world.bentobox.limits.events.LimitsJoinPermCheckEvent; +import world.bentobox.limits.events.LimitsPermCheckEvent; +import world.bentobox.limits.objects.IslandBlockCount; + +/** + * Applies permission-based block, entity, and entity-group limits to islands. + * + *

Permission format: + *

    + *
  • 5-segment: {@code .island.limit..} — same limit applied independently + * to every environment.
  • + *
  • 6-segment: {@code .island.limit...} — applied only to the named + * environment, where {@code env} is one of {@code overworld}, {@code nether}, + * {@code end}.
  • + *
+ * + * @author tastybento + */ +public class JoinListener implements Listener { + + private final Limits addon; + + public JoinListener(Limits addon) { + this.addon = addon; + } + + /** + * Reads every limit-shaped permission the player has and re-applies it to the island + * after first clearing existing permission-based limits. + */ + public void checkPerms(Player player, String permissionPrefix, String islandId, String gameMode) { + IslandBlockCount islandBlockCount = addon.getBlockLimitListener().getIsland(islandId); + if (islandBlockCount != null) { + islandBlockCount.clearAllEntityLimits(); + islandBlockCount.clearAllEntityGroupLimits(); + islandBlockCount.clearAllBlockLimits(); + } + for (PermissionAttachmentInfo permissionInfo : player.getEffectivePermissions()) { + if (!permissionInfo.getValue() || !permissionInfo.getPermission().startsWith(permissionPrefix)) { + continue; + } + ParsedPerm parsed = parsePerm(permissionInfo, player.getName(), permissionPrefix); + if (parsed == null) continue; + + // Try to match the key part to an EntityType, Material, or EntityGroup + EntityType entityType = matchEntityType(parsed.key); + Material material = matchMaterial(parsed.key); + EntityGroup entityGroup = matchEntityGroup(parsed.key); + + if (entityGroup == null && entityType == null && material == null) { + logError(player.getName(), permissionInfo.getPermission(), + parsed.key.toUpperCase(Locale.ENGLISH) + " is not a valid material or entity type/group."); + continue; + } + + if (islandBlockCount == null) { + islandBlockCount = new IslandBlockCount(islandId, gameMode); + } + logIfEnabled("Setting login limit via perm for " + player.getName() + "..."); + + LimitsPermCheckEvent limitsPermCheckEvent = new LimitsPermCheckEvent(player, islandId, islandBlockCount, + entityGroup, entityType, material, parsed.value); + Bukkit.getPluginManager().callEvent(limitsPermCheckEvent); + if (limitsPermCheckEvent.isCancelled()) { + addon.log("Permissions not set because another addon/plugin canceled setting."); + continue; + } + islandBlockCount = limitsPermCheckEvent.getIbc(); + if (islandBlockCount == null) { + islandBlockCount = new IslandBlockCount(islandId, gameMode); + } + applyLimit(islandBlockCount, parsed.envs, limitsPermCheckEvent); + } + if (islandBlockCount != null) { + addon.getBlockLimitListener().setIsland(islandId, islandBlockCount); + } + } + + /** + * Parses a limit permission. Returns {@code null} if invalid (and logs). + */ + private ParsedPerm parsePerm(PermissionAttachmentInfo info, String playerName, String permissionPrefix) { + String permission = info.getPermission(); + if (permission.contains(permissionPrefix + "*")) { + logError(playerName, permission, "wildcards are not allowed."); + return null; + } + String[] parts = permission.split("\\."); + // 5-segment: .island.limit.. + // 6-segment: .island.limit... + if (parts.length != 5 && parts.length != 6) { + logError(playerName, permission, "format must be '" + permissionPrefix + "[ENV.]KEY.NUMBER' " + + "where ENV is overworld|nether|end and KEY is a material, entity type, or group name."); + return null; + } + String numberPart = parts[parts.length - 1]; + int value; + try { + value = Integer.parseInt(numberPart); + } catch (NumberFormatException e) { + logError(playerName, permission, "the last part MUST be an integer!"); + return null; + } + ParsedPerm out = new ParsedPerm(); + out.value = value; + if (parts.length == 5) { + out.key = parts[3]; + out.envs = Settings.ENVIRONMENTS; + } else { + Environment env = parseEnv(parts[3]); + if (env == null) { + logError(playerName, permission, "'" + parts[3] + + "' is not a recognised environment (use overworld, nether, or end)."); + return null; + } + out.key = parts[4]; + out.envs = List.of(env); + } + return out; + } + + private static Environment parseEnv(String token) { + return switch (token.toLowerCase(Locale.ROOT)) { + case "overworld", "normal" -> Environment.NORMAL; + case "nether" -> Environment.NETHER; + case "end", "the_end" -> Environment.THE_END; + default -> null; + }; + } + + private EntityType matchEntityType(String key) { + return Arrays.stream(EntityType.values()).filter(t -> t.name().equalsIgnoreCase(key)).findFirst().orElse(null); + } + + private Material matchMaterial(String key) { + return Arrays.stream(Material.values()).filter(m -> m.name().equalsIgnoreCase(key)).findFirst().orElse(null); + } + + private EntityGroup matchEntityGroup(String key) { + return addon.getSettings().getGroupLimitDefinitions().stream() + .filter(group -> group.getName().equalsIgnoreCase(key)).findFirst().orElse(null); + } + + private void applyLimit(@NonNull IslandBlockCount ibc, List envs, @NonNull LimitsPermCheckEvent event) { + EntityGroup entityGroup = event.getEntityGroup(); + EntityType entityType = event.getEntityType(); + Material material = event.getMaterial(); + int limitValue = event.getValue(); + + if (entityGroup != null) { + for (Environment env : envs) { + int newLimit = Math.max(ibc.getEntityGroupLimit(env, entityGroup.getName()), limitValue); + ibc.setEntityGroupLimit(env, entityGroup.getName(), newLimit); + logIfEnabled("Setting group limit " + entityGroup.getName() + " in " + env + " to " + newLimit); + } + } else if (entityType != null && material == null) { + for (Environment env : envs) { + int newLimit = Math.max(ibc.getEntityLimit(env, entityType), limitValue); + ibc.setEntityLimit(env, entityType, newLimit); + logIfEnabled("Setting entity limit " + entityType + " in " + env + " to " + newLimit); + } + } else if (material != null && entityType == null) { + for (Environment env : envs) { + int newLimit = Math.max(ibc.getBlockLimit(env, material.getKey()), limitValue); + ibc.setBlockLimit(env, material.getKey(), newLimit); + logIfEnabled("Setting block limit " + material + " in " + env + " to " + newLimit); + } + } else { + applyAmbiguousLimit(ibc, envs, entityType, material, limitValue); + } + } + + private void applyAmbiguousLimit(@NonNull IslandBlockCount ibc, List envs, EntityType entityType, + Material material, int limitValue) { + if (material != null && material.isBlock()) { + for (Environment env : envs) { + int newLimit = Math.max(ibc.getBlockLimit(env, material.getKey()), limitValue); + ibc.setBlockLimit(env, material.getKey(), newLimit); + logIfEnabled("Setting block limit " + material + " in " + env + " to " + newLimit); + } + } else if (entityType != null) { + for (Environment env : envs) { + int newLimit = Math.max(ibc.getEntityLimit(env, entityType), limitValue); + ibc.setEntityLimit(env, entityType, newLimit); + logIfEnabled("Setting entity limit " + entityType + " in " + env + " to " + newLimit); + } + } + } + + private void logIfEnabled(String message) { + if (addon.getSettings().isLogLimitsOnJoin()) { + addon.log(message); + } + } + + private void logError(String playerName, String permission, String errorMessage) { + addon.logError("Player " + playerName + " has permission: '" + permission + "' but " + errorMessage + + " Ignoring..."); + } + + /* ========================================================================= + * Event handlers + * ========================================================================= */ + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onNewIsland(IslandEvent event) { + if (!event.getReason().equals(Reason.CREATED) && !event.getReason().equals(Reason.RESETTED) + && !event.getReason().equals(Reason.REGISTERED)) { + return; + } + setOwnerPerms(event.getIsland(), event.getOwner()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onOwnerChange(TeamSetownerEvent event) { + removeOwnerPerms(event.getIsland()); + setOwnerPerms(event.getIsland(), event.getNewOwner()); + } + + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onPlayerJoin(PlayerJoinEvent event) { + addon.getGameModes().forEach(gameMode -> { + addon.getIslands().getIslands(gameMode.getOverWorld(), event.getPlayer().getUniqueId()).stream() + .filter(island -> event.getPlayer().getUniqueId().equals(island.getOwner())) + .map(Island::getUniqueId).forEach(islandId -> { + IslandBlockCount islandBlockCount = addon.getBlockLimitListener().getIsland(islandId); + if (!joinEventCheck(event.getPlayer(), islandId, islandBlockCount)) { + checkPerms(event.getPlayer(), gameMode.getPermissionPrefix() + "island.limit.", islandId, + gameMode.getDescription().getName()); + } + }); + }); + } + + private boolean joinEventCheck(Player player, String islandId, IslandBlockCount islandBlockCount) { + LimitsJoinPermCheckEvent event = new LimitsJoinPermCheckEvent(player, islandId, islandBlockCount); + Bukkit.getPluginManager().callEvent(event); + if (event.isCancelled()) return true; + islandBlockCount = event.getIbc(); + if (event.isIgnorePerms() && islandBlockCount != null) { + addon.getBlockLimitListener().setIsland(islandId, islandBlockCount); + return true; + } + return false; + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onUnregisterIsland(IslandEvent event) { + if (!event.getReason().equals(Reason.UNREGISTERED)) return; + removeOwnerPerms(event.getIsland()); + } + + private void removeOwnerPerms(Island island) { + World world = island.getWorld(); + if (addon.inGameModeWorld(world)) { + IslandBlockCount islandBlockCount = addon.getBlockLimitListener().getIsland(island.getUniqueId()); + if (islandBlockCount != null) { + islandBlockCount.clearAllBlockLimits(); + islandBlockCount.clearAllEntityLimits(); + islandBlockCount.clearAllEntityGroupLimits(); + } + } + } + + private void setOwnerPerms(Island island, UUID ownerUUID) { + World world = island.getWorld(); + if (addon.inGameModeWorld(world)) { + OfflinePlayer owner = Bukkit.getOfflinePlayer(ownerUUID); + if (owner.isOnline()) { + String permissionPrefix = addon.getGameModePermPrefix(world); + String gameModeName = addon.getGameModeName(world); + if (!permissionPrefix.isEmpty() && !gameModeName.isEmpty() && owner.getPlayer() != null) { + checkPerms(Objects.requireNonNull(owner.getPlayer()), permissionPrefix + "island.limit.", + island.getUniqueId(), gameModeName); + } + } + } + } + + private static class ParsedPerm { + String key; + int value; + List envs; + } +} diff --git a/src/main/java/world/bentobox/limits/objects/EnvNamespacedKeyMapAdapter.java b/src/main/java/world/bentobox/limits/objects/EnvNamespacedKeyMapAdapter.java new file mode 100644 index 0000000..68124ea --- /dev/null +++ b/src/main/java/world/bentobox/limits/objects/EnvNamespacedKeyMapAdapter.java @@ -0,0 +1,116 @@ +package world.bentobox.limits.objects; + +import java.io.IOException; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import org.bukkit.NamespacedKey; +import org.bukkit.World.Environment; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +/** + * Gson {@link TypeAdapter} for {@code Map>}. + * Outer keys are {@link Environment} names; inner keys are written/read as + * "namespace:key" strings via the same logic as {@link NamespacedKeyMapAdapter}. + */ +public class EnvNamespacedKeyMapAdapter extends TypeAdapter>> { + + @Override + public void write(JsonWriter out, Map> value) throws IOException { + if (value == null) { + out.nullValue(); + return; + } + out.beginObject(); + for (Map.Entry> envEntry : value.entrySet()) { + if (envEntry.getKey() == null || envEntry.getValue() == null || envEntry.getValue().isEmpty()) { + continue; + } + out.name(envEntry.getKey().name()); + out.beginObject(); + for (Map.Entry entry : envEntry.getValue().entrySet()) { + if (entry.getKey() == null || entry.getValue() == null) { + continue; + } + out.name(entry.getKey().toString()); + out.value(entry.getValue()); + } + out.endObject(); + } + out.endObject(); + } + + @Override + public Map> read(JsonReader in) throws IOException { + Map> result = new EnumMap<>(Environment.class); + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return result; + } + in.beginObject(); + while (in.hasNext()) { + String envName = in.nextName(); + Environment env = parseEnvironment(envName); + Map inner = readInner(in); + if (env != null && !inner.isEmpty()) { + result.put(env, inner); + } + } + in.endObject(); + return result; + } + + private static Environment parseEnvironment(String raw) { + if (raw == null) return null; + try { + return Environment.valueOf(raw.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + return null; + } + } + + private static Map readInner(JsonReader in) throws IOException { + Map inner = new HashMap<>(); + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return inner; + } + in.beginObject(); + while (in.hasNext()) { + String rawKey = in.nextName(); + Integer value = readIntOrNull(in); + NamespacedKey key = parseKey(rawKey); + if (key != null && value != null) { + inner.put(key, value); + } + } + in.endObject(); + return inner; + } + + private static NamespacedKey parseKey(String raw) { + if (raw == null || raw.isEmpty()) return null; + if (raw.indexOf(':') >= 0) { + return NamespacedKey.fromString(raw.toLowerCase(Locale.ROOT)); + } + try { + return new NamespacedKey(NamespacedKey.MINECRAFT, raw.toLowerCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + return null; + } + } + + private static Integer readIntOrNull(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + return in.nextInt(); + } +} diff --git a/src/main/java/world/bentobox/limits/objects/IslandBlockCount.java b/src/main/java/world/bentobox/limits/objects/IslandBlockCount.java index ae410d9..4dd103b 100644 --- a/src/main/java/world/bentobox/limits/objects/IslandBlockCount.java +++ b/src/main/java/world/bentobox/limits/objects/IslandBlockCount.java @@ -1,432 +1,499 @@ -package world.bentobox.limits.objects; - -import java.util.EnumMap; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; - -import org.bukkit.NamespacedKey; -import org.bukkit.entity.EntityType; - -import com.google.gson.annotations.Expose; -import com.google.gson.annotations.JsonAdapter; - -import world.bentobox.bentobox.database.objects.DataObject; -import world.bentobox.bentobox.database.objects.Table; - -/** - * @author tastybento - * - */ -@Table(name = "IslandBlockCount") -public class IslandBlockCount implements DataObject { - - @Expose - @JsonAdapter(NamespacedKeyMapAdapter.class) - private Map blockCounts = new HashMap<>(); - - /** - * Permission based limits - */ - @Expose - @JsonAdapter(NamespacedKeyMapAdapter.class) - private Map blockLimits = new HashMap<>(); - - @Expose - @JsonAdapter(NamespacedKeyMapAdapter.class) - private Map blockLimitsOffset = new HashMap<>(); - - private boolean changed; - - @Expose - private Map entityGroupLimits = new HashMap<>(); - @Expose - private Map entityGroupLimitsOffset = new HashMap<>(); - @Expose - private Map entityLimits = new EnumMap<>(EntityType.class); - @Expose - private Map entityLimitsOffset = new EnumMap<>(EntityType.class); - @Expose - private String gameMode; - @Expose - private String uniqueId; - - /** - * Create an island block count object - * - * @param islandId - unique Island ID string - * @param gameMode - Game mode name from gm.getDescription().getName() - */ - public IslandBlockCount(String islandId, String gameMode) { - this.uniqueId = islandId; - this.gameMode = gameMode; - setChanged(); - } - - /** - * Add a material to the count - * - * @param material - material - */ - public void add(NamespacedKey material) { - getBlockCounts().merge(material, 1, Integer::sum); - setChanged(); - } - - /** - * Clear all island-specific entity group limits - */ - public void clearEntityGroupLimits() { - entityGroupLimits.clear(); - setChanged(); - } - - /** - * Clear all island-specific entity type limits - */ - public void clearEntityLimits() { - entityLimits.clear(); - setChanged(); - } - - /** - * Get the block count for this material for this island - * - * @param m - material - * @return count - */ - public Integer getBlockCount(NamespacedKey m) { - return getBlockCounts().getOrDefault(m, 0); - } - - /** - * @return the blockCount - */ - public Map getBlockCounts() { - if (blockCounts == null) { - blockCounts = new HashMap<>(); - } - return blockCounts; - } - - /** - * Get the block limit for this material for this island - * - * @param m - material - * @return limit or -1 for unlimited - */ - public int getBlockLimit(NamespacedKey m) { - return getBlockLimits().getOrDefault(m, -1); - } - - /** - * Get the block offset for this material for this island - * - * @param m - material - * @return offset - */ - public int getBlockLimitOffset(NamespacedKey m) { - return getBlockLimitsOffset().getOrDefault(m, 0); - } - - /** - * @return the blockLimits - */ - public Map getBlockLimits() { - if (blockLimits == null) { - blockLimits = new HashMap<>(); - } - return blockLimits; - } - - /** - * @return the blockLimitsOffset - */ - public Map getBlockLimitsOffset() { - if (blockLimitsOffset == null) { - blockLimitsOffset = new HashMap<>(); - } - return blockLimitsOffset; - } - - /** - * Get the limit for an entity group - * - * @param name - entity group - * @return limit or -1 for unlimited - */ - public int getEntityGroupLimit(String name) { - return getEntityGroupLimits().getOrDefault(name, -1); - } - - /** - * Get the offset for an entity group - * - * @param name - entity group - * @return offset - */ - public int getEntityGroupLimitOffset(String name) { - return getEntityGroupLimitsOffset().getOrDefault(name, 0); - } - - /** - * @return the entityGroupLimits - */ - public Map getEntityGroupLimits() { - return Objects.requireNonNullElse(entityGroupLimits, new HashMap<>()); - } - - /** - * @return the entityGroupLimitsOffset - */ - public Map getEntityGroupLimitsOffset() { - if (entityGroupLimitsOffset == null) { - entityGroupLimitsOffset = new HashMap<>(); - } - return entityGroupLimitsOffset; - } - - /** - * Get the limit for an entity type - * - * @param t - entity type - * @return limit or -1 for unlimited - */ - public int getEntityLimit(EntityType t) { - return getEntityLimits().getOrDefault(t, -1); - } - - /** - * Get the limit offset for an entity type - * - * @param t - entity type - * @return offset - */ - public int getEntityLimitOffset(EntityType t) { - return getEntityLimitsOffset().getOrDefault(t, 0); - } - - /** - * @return the entityLimits - */ - public Map getEntityLimits() { - return Objects.requireNonNullElse(entityLimits, new EnumMap<>(EntityType.class)); - } - - /** - * @return the entityLimitsOffset - */ - public Map getEntityLimitsOffset() { - if (entityLimitsOffset == null) { - entityLimitsOffset = new EnumMap<>(EntityType.class); - } - return entityLimitsOffset; - } - - /** - * @return the gameMode - */ - public String getGameMode() { - return gameMode; - } - - /* - * (non-Javadoc) - * - * @see world.bentobox.bentobox.database.objects.DataObject#getUniqueId() - */ - @Override - public String getUniqueId() { - return uniqueId; - } - - /** - * Check if no more of this material can be added to this island - * - * @param m - material - * @return true if no more material can be added - */ - public boolean isAtLimit(NamespacedKey m) { - return getBlockLimits().containsKey(m) - && getBlockCount(m) >= getBlockLimit(m) + this.getBlockLimitOffset(m); - } - - /** - * Check if this material is at or over a limit - * - * @param material - block material - * @param limit - limit to check - * @return true if count is >= limit - */ - public boolean isAtLimit(NamespacedKey material, int limit) { - return getBlockCount(material) >= limit + this.getBlockLimitOffset(material); - } - - public boolean isBlockLimited(NamespacedKey m) { - return getBlockLimits().containsKey(m); - } - - /** - * @return the changed - */ - public boolean isChanged() { - return changed; - } - - public boolean isGameMode(String gameMode) { - return getGameMode().equals(gameMode); - } - - /** - * Remove a material from the count - * - * @param material - material - */ - public void remove(NamespacedKey material) { - // Check if this material is currently tracked - boolean existed = getBlockCounts().containsKey(material); - // Otherwise, decrement/remove individual block - getBlockCounts().computeIfPresent(material, (m, count) -> { - int newCount = count - 1; - return (newCount > 0) ? newCount : null; // null removes the entry - }); - // Mark this object as changed only if a modification actually occurred - if (existed) { - setChanged(); - } - } - - /** - * @param blockCounts the blockCount to set - */ - public void setBlockCounts(Map blockCounts) { - this.blockCounts = blockCounts; - setChanged(); - } - - /** - * Set the block limit for this material for this island - * - * @param m - material - * @param limit - maximum number allowed - */ - public void setBlockLimit(NamespacedKey m, int limit) { - getBlockLimits().put(m, limit); - setChanged(); - } - - /** - * @param blockLimits the blockLimits to set - */ - public void setBlockLimits(Map blockLimits) { - this.blockLimits = blockLimits; - setChanged(); - } - - /** - * Set an offset to a block limit. This will increase/decrease the value of the - * limit. - * - * @param m material - * @param blockLimitsOffset the blockLimitsOffset to set - */ - public void setBlockLimitsOffset(NamespacedKey m, Integer blockLimitsOffset) { - getBlockLimitsOffset().put(m, blockLimitsOffset); - } - - /** - * Mark changed - */ - public void setChanged() { - this.changed = true; - } - - /** - * @param changed the changed to set - */ - public void setChanged(boolean changed) { - this.changed = changed; - } - - /** - * Set an island-specific entity group limit - * - * @param name - entity group - * @param limit - limit - */ - public void setEntityGroupLimit(String name, int limit) { - getEntityGroupLimits().put(name, limit); - setChanged(); - } - - /** - * @param entityGroupLimits the entityGroupLimits to set - */ - public void setEntityGroupLimits(Map entityGroupLimits) { - this.entityGroupLimits = entityGroupLimits; - setChanged(); - } - - /** - * Set an offset to an entity group limit. This will increase/decrease the value - * of the limit. - * - * @param name group name - * @param entityGroupLimitsOffset the entityGroupLimitsOffset to set - */ - public void setEntityGroupLimitsOffset(String name, Integer entityGroupLimitsOffset) { - getEntityGroupLimitsOffset().put(name, entityGroupLimitsOffset); - } - - /** - * Set an island-specific entity type limit - * - * @param t - entity type - * @param limit - limit - */ - public void setEntityLimit(EntityType t, int limit) { - getEntityLimits().put(t, limit); - setChanged(); - } - - /** - * @param entityLimits the entityLimits to set - */ - public void setEntityLimits(Map entityLimits) { - this.entityLimits = entityLimits; - setChanged(); - } - - /** - * Set an offset to an entity limit. This will increase/decrease the value of - * the limit. - * - * @param type Entity Type - * @param entityLimitsOffset the entityLimitsOffset to set - */ - public void setEntityLimitsOffset(EntityType type, Integer entityLimitsOffset) { - this.getEntityLimitsOffset().put(type, entityLimitsOffset); - } - - /** - * @param gameMode the gameMode to set - */ - public void setGameMode(String gameMode) { - this.gameMode = gameMode; - setChanged(); - } - - /* - * (non-Javadoc) - * - * @see - * world.bentobox.bentobox.database.objects.DataObject#setUniqueId(java.lang. - * String) - */ - @Override - public void setUniqueId(String uniqueId) { - this.uniqueId = uniqueId; - setChanged(); - } - - } +package world.bentobox.limits.objects; + +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; + +import org.bukkit.NamespacedKey; +import org.bukkit.World.Environment; +import org.bukkit.entity.EntityType; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.JsonAdapter; + +import world.bentobox.bentobox.database.objects.DataObject; +import world.bentobox.bentobox.database.objects.Table; + +/** + * Per-island, per-environment block and entity tracking. + * + *

Each map is keyed by {@link Environment} so overworld, nether, and end have + * independent counts, limits and offsets. Pre-1.x data stored a single map per + * field (no environment); on first load that legacy data is migrated into the + * {@link Environment#NORMAL} slot. + * + * @author tastybento + */ +@Table(name = "IslandBlockCount") +public class IslandBlockCount implements DataObject { + + /* New env-keyed primary storage. */ + + @Expose + @JsonAdapter(EnvNamespacedKeyMapAdapter.class) + private Map> envBlockCounts = new EnumMap<>(Environment.class); + + @Expose + @JsonAdapter(EnvNamespacedKeyMapAdapter.class) + private Map> envBlockLimits = new EnumMap<>(Environment.class); + + @Expose + @JsonAdapter(EnvNamespacedKeyMapAdapter.class) + private Map> envBlockLimitsOffset = new EnumMap<>(Environment.class); + + @Expose + private Map> envEntityCounts = new EnumMap<>(Environment.class); + + @Expose + private Map> envEntityLimits = new EnumMap<>(Environment.class); + + @Expose + private Map> envEntityLimitsOffset = new EnumMap<>(Environment.class); + + @Expose + private Map> envEntityGroupLimits = new EnumMap<>(Environment.class); + + @Expose + private Map> envEntityGroupLimitsOffset = new EnumMap<>(Environment.class); + + /* Legacy fields kept to deserialize pre-env data; never written back. */ + + @Expose(serialize = false) + @JsonAdapter(NamespacedKeyMapAdapter.class) + private Map blockCounts; + + @Expose(serialize = false) + @JsonAdapter(NamespacedKeyMapAdapter.class) + private Map blockLimits; + + @Expose(serialize = false) + @JsonAdapter(NamespacedKeyMapAdapter.class) + private Map blockLimitsOffset; + + @Expose(serialize = false) + private Map entityLimits; + + @Expose(serialize = false) + private Map entityLimitsOffset; + + @Expose(serialize = false) + private Map entityGroupLimits; + + @Expose(serialize = false) + private Map entityGroupLimitsOffset; + + @Expose + private String gameMode; + + @Expose + private String uniqueId; + + private boolean changed; + private boolean migrated; + + /** + * Required by Gson. + */ + public IslandBlockCount() { + } + + /** + * @param islandId unique island ID + * @param gameMode game mode addon name + */ + public IslandBlockCount(String islandId, String gameMode) { + this.uniqueId = islandId; + this.gameMode = gameMode; + setChanged(); + } + + /* ========================================================================= + * Migration + * ========================================================================= */ + + /** + * Move any legacy single-map data into Environment.NORMAL. Idempotent. + */ + private void migrateIfNeeded() { + if (migrated) return; + migrated = true; + moveLegacy(blockCounts, envBlockCounts); + moveLegacy(blockLimits, envBlockLimits); + moveLegacy(blockLimitsOffset, envBlockLimitsOffset); + moveLegacy(entityLimits, envEntityLimits); + moveLegacy(entityLimitsOffset, envEntityLimitsOffset); + moveLegacy(entityGroupLimits, envEntityGroupLimits); + moveLegacy(entityGroupLimitsOffset, envEntityGroupLimitsOffset); + blockCounts = null; + blockLimits = null; + blockLimitsOffset = null; + entityLimits = null; + entityLimitsOffset = null; + entityGroupLimits = null; + entityGroupLimitsOffset = null; + } + + private static void moveLegacy(Map legacy, Map> target) { + if (legacy == null || legacy.isEmpty()) return; + target.computeIfAbsent(Environment.NORMAL, e -> newInner(legacy)).putAll(legacy); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static Map newInner(Map sample) { + Object first = sample.keySet().iterator().next(); + if (first instanceof EntityType) { + return (Map) (Map) new EnumMap(EntityType.class); + } + return new HashMap<>(); + } + + /* ========================================================================= + * Block counts + * ========================================================================= */ + + public Map> getAllBlockCounts() { + migrateIfNeeded(); + if (envBlockCounts == null) envBlockCounts = new EnumMap<>(Environment.class); + return envBlockCounts; + } + + public Map getBlockCounts(Environment env) { + return getAllBlockCounts().computeIfAbsent(env, e -> new HashMap<>()); + } + + public int getBlockCount(Environment env, NamespacedKey key) { + return getBlockCounts(env).getOrDefault(key, 0); + } + + public int getBlockCount(NamespacedKey key) { + int total = 0; + for (Map m : getAllBlockCounts().values()) { + total += m.getOrDefault(key, 0); + } + return total; + } + + public void add(Environment env, NamespacedKey material) { + getBlockCounts(env).merge(material, 1, Integer::sum); + setChanged(); + } + + public void remove(Environment env, NamespacedKey material) { + Map m = getBlockCounts(env); + if (m.containsKey(material)) { + m.computeIfPresent(material, (k, count) -> count - 1 > 0 ? count - 1 : null); + setChanged(); + } + } + + public void clearAllBlockCounts() { + getAllBlockCounts().values().forEach(Map::clear); + setChanged(); + } + + /* ========================================================================= + * Block limits + * ========================================================================= */ + + public Map> getAllBlockLimits() { + migrateIfNeeded(); + if (envBlockLimits == null) envBlockLimits = new EnumMap<>(Environment.class); + return envBlockLimits; + } + + public Map getBlockLimits(Environment env) { + return getAllBlockLimits().computeIfAbsent(env, e -> new HashMap<>()); + } + + /** + * @return -1 if no env has a limit, otherwise the lowest limit defined across envs. + */ + public int getBlockLimit(Environment env, NamespacedKey key) { + return getBlockLimits(env).getOrDefault(key, -1); + } + + public boolean isBlockLimited(Environment env, NamespacedKey key) { + return getBlockLimits(env).containsKey(key); + } + + public void setBlockLimit(Environment env, NamespacedKey key, int limit) { + getBlockLimits(env).put(key, limit); + setChanged(); + } + + /** + * Set the same limit for every standard environment (overworld, nether, end). + * Used by 5-segment, env-unspecified permissions and global config defaults. + */ + public void setBlockLimitAllEnvs(NamespacedKey key, int limit) { + for (Environment env : Environment.values()) { + if (env == Environment.CUSTOM) continue; + setBlockLimit(env, key, limit); + } + } + + public void clearAllBlockLimits() { + getAllBlockLimits().values().forEach(Map::clear); + setChanged(); + } + + /* ========================================================================= + * Block limit offsets (env-agnostic in storage; offsets apply per env) + * ========================================================================= */ + + public Map> getAllBlockLimitsOffset() { + migrateIfNeeded(); + if (envBlockLimitsOffset == null) envBlockLimitsOffset = new EnumMap<>(Environment.class); + return envBlockLimitsOffset; + } + + public Map getBlockLimitsOffset(Environment env) { + return getAllBlockLimitsOffset().computeIfAbsent(env, e -> new HashMap<>()); + } + + public int getBlockLimitOffset(Environment env, NamespacedKey key) { + return getBlockLimitsOffset(env).getOrDefault(key, 0); + } + + public void setBlockLimitsOffset(Environment env, NamespacedKey key, int offset) { + getBlockLimitsOffset(env).put(key, offset); + setChanged(); + } + + /** + * Apply the same offset to every standard environment. Used by /offset which is env-agnostic. + */ + public void setBlockLimitsOffsetAllEnvs(NamespacedKey key, int offset) { + for (Environment env : Environment.values()) { + if (env == Environment.CUSTOM) continue; + setBlockLimitsOffset(env, key, offset); + } + } + + /* ========================================================================= + * Entity counts (persistent) + * ========================================================================= */ + + public Map> getAllEntityCounts() { + migrateIfNeeded(); + if (envEntityCounts == null) envEntityCounts = new EnumMap<>(Environment.class); + return envEntityCounts; + } + + public Map getEntityCounts(Environment env) { + return getAllEntityCounts().computeIfAbsent(env, e -> new EnumMap<>(EntityType.class)); + } + + public int getEntityCount(Environment env, EntityType type) { + return getEntityCounts(env).getOrDefault(type, 0); + } + + public int getEntityCount(EntityType type) { + int total = 0; + for (Map m : getAllEntityCounts().values()) { + total += m.getOrDefault(type, 0); + } + return total; + } + + public void incrementEntity(Environment env, EntityType type) { + getEntityCounts(env).merge(type, 1, Integer::sum); + setChanged(); + } + + public void decrementEntity(Environment env, EntityType type) { + Map m = getEntityCounts(env); + if (m.containsKey(type)) { + m.computeIfPresent(type, (k, c) -> c - 1 > 0 ? c - 1 : null); + setChanged(); + } + } + + public void clearAllEntityCounts() { + getAllEntityCounts().values().forEach(Map::clear); + setChanged(); + } + + /* ========================================================================= + * Entity limits + * ========================================================================= */ + + public Map> getAllEntityLimits() { + migrateIfNeeded(); + if (envEntityLimits == null) envEntityLimits = new EnumMap<>(Environment.class); + return envEntityLimits; + } + + public Map getEntityLimits(Environment env) { + return getAllEntityLimits().computeIfAbsent(env, e -> new EnumMap<>(EntityType.class)); + } + + public int getEntityLimit(Environment env, EntityType type) { + return getEntityLimits(env).getOrDefault(type, -1); + } + + public void setEntityLimit(Environment env, EntityType type, int limit) { + getEntityLimits(env).put(type, limit); + setChanged(); + } + + public void setEntityLimitAllEnvs(EntityType type, int limit) { + for (Environment env : Environment.values()) { + if (env == Environment.CUSTOM) continue; + setEntityLimit(env, type, limit); + } + } + + public void clearAllEntityLimits() { + getAllEntityLimits().values().forEach(Map::clear); + setChanged(); + } + + /* ========================================================================= + * Entity limit offsets + * ========================================================================= */ + + public Map> getAllEntityLimitsOffset() { + migrateIfNeeded(); + if (envEntityLimitsOffset == null) envEntityLimitsOffset = new EnumMap<>(Environment.class); + return envEntityLimitsOffset; + } + + public Map getEntityLimitsOffset(Environment env) { + return getAllEntityLimitsOffset().computeIfAbsent(env, e -> new EnumMap<>(EntityType.class)); + } + + public int getEntityLimitOffset(Environment env, EntityType type) { + return getEntityLimitsOffset(env).getOrDefault(type, 0); + } + + public void setEntityLimitsOffset(Environment env, EntityType type, int offset) { + getEntityLimitsOffset(env).put(type, offset); + setChanged(); + } + + public void setEntityLimitsOffsetAllEnvs(EntityType type, int offset) { + for (Environment env : Environment.values()) { + if (env == Environment.CUSTOM) continue; + setEntityLimitsOffset(env, type, offset); + } + } + + /* ========================================================================= + * Entity group limits + * ========================================================================= */ + + public Map> getAllEntityGroupLimits() { + migrateIfNeeded(); + if (envEntityGroupLimits == null) envEntityGroupLimits = new EnumMap<>(Environment.class); + return envEntityGroupLimits; + } + + public Map getEntityGroupLimits(Environment env) { + return getAllEntityGroupLimits().computeIfAbsent(env, e -> new HashMap<>()); + } + + public int getEntityGroupLimit(Environment env, String name) { + return getEntityGroupLimits(env).getOrDefault(name, -1); + } + + public void setEntityGroupLimit(Environment env, String name, int limit) { + getEntityGroupLimits(env).put(name, limit); + setChanged(); + } + + public void setEntityGroupLimitAllEnvs(String name, int limit) { + for (Environment env : Environment.values()) { + if (env == Environment.CUSTOM) continue; + setEntityGroupLimit(env, name, limit); + } + } + + public void clearAllEntityGroupLimits() { + getAllEntityGroupLimits().values().forEach(Map::clear); + setChanged(); + } + + /* ========================================================================= + * Entity group limit offsets + * ========================================================================= */ + + public Map> getAllEntityGroupLimitsOffset() { + migrateIfNeeded(); + if (envEntityGroupLimitsOffset == null) envEntityGroupLimitsOffset = new EnumMap<>(Environment.class); + return envEntityGroupLimitsOffset; + } + + public Map getEntityGroupLimitsOffset(Environment env) { + return getAllEntityGroupLimitsOffset().computeIfAbsent(env, e -> new HashMap<>()); + } + + public int getEntityGroupLimitOffset(Environment env, String name) { + return getEntityGroupLimitsOffset(env).getOrDefault(name, 0); + } + + public void setEntityGroupLimitsOffset(Environment env, String name, int offset) { + getEntityGroupLimitsOffset(env).put(name, offset); + setChanged(); + } + + public void setEntityGroupLimitsOffsetAllEnvs(String name, int offset) { + for (Environment env : Environment.values()) { + if (env == Environment.CUSTOM) continue; + setEntityGroupLimitsOffset(env, name, offset); + } + } + + /* ========================================================================= + * "At limit" helpers + * ========================================================================= */ + + public boolean isAtLimit(Environment env, NamespacedKey key) { + if (!isBlockLimited(env, key)) return false; + return getBlockCount(env, key) >= getBlockLimit(env, key) + getBlockLimitOffset(env, key); + } + + public boolean isAtLimit(Environment env, NamespacedKey key, int limit) { + return getBlockCount(env, key) >= limit + getBlockLimitOffset(env, key); + } + + /* ========================================================================= + * Misc + * ========================================================================= */ + + public String getGameMode() { + return gameMode; + } + + public void setGameMode(String gameMode) { + this.gameMode = gameMode; + setChanged(); + } + + public boolean isGameMode(String gameMode) { + return getGameMode().equals(gameMode); + } + + @Override + public String getUniqueId() { + return uniqueId; + } + + @Override + public void setUniqueId(String uniqueId) { + this.uniqueId = uniqueId; + setChanged(); + } + + public boolean isChanged() { + return changed; + } + + public void setChanged() { + this.changed = true; + } + + public void setChanged(boolean changed) { + this.changed = changed; + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index e73bfa9..358ea02 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,136 +1,170 @@ -# Game Modes covered by limits -gamemodes: -- AcidIsland -- BSkyBlock -- CaveBlock -- AOneBlock - -# Ignore this island's center block. For most worlds, this is bedrock, but for AOneBlock it is -# the magic block, so ignoring it from limits makes sense. -ignore-center-block: true - -# Permissions -# Island owners can be given permissions that override all general settings -# Format is GAME-MODE-NAME.island.limit.MATERIAL.LIMIT -# example: bskyblock.island.limit.hopper.10 -# permission activates when player logs in. -# -# Cooldown for player recount command in seconds -cooldown: 120 - -# Log limits on join (to console) -log-limits-on-join: true - -# Use async checks for snowmen and iron golums. Set to false if you see problems. -async-golums: true - -# General block limiting -# Use this section to limit how many blocks can be added to an island. -# 0 means the item will be blocked from placement completely. -# These limits apply to every game mode world -# The following general terms can be used: STAIRS, DOORS, SIGNS, LOGS, SNOW, LEAVES, SHULKER_BOXES, -# BANNERS, RAILS, ICE, BUTTONS, CANDLE_CAKES, CAMPFIRES, SLABS, STONE_BRICKS, -blocklimits: - HOPPER: 10 -# This section is for world-specific limits and overrides the general limit -# Specify each world you want to limit individually (including nether and end worlds) -# If these worlds are not covered by the game modes above, nothing will happen -worlds: - AcidIsland_world: - HOPPER: 11 - -# Default entity limits within a player's island space (protected area and to island limit). -# A limit of 5 will allow up to 5 entities in over world, 5 in nether and 5 in the end. -# Affects all types of creature spawning. Also includes entities like MINECARTS. -# Note: Only the first 49 limited blocks and entities are shown in the limits GUI. -entitylimits: - ENDERMAN: 5 - CHICKEN: 10 - -# Entity Groups -# Name the group anything you like -# Select an icon, which is a Bukkit Material -# Select the limit for the total group -# List the entities in the group using Bukkit EntityTypes -entitygrouplimits: - Monsters: - icon: ROTTEN_FLESH - limit: 250 - entities: - - SKELETON - - SILVERFISH - - STRAY - - ZOMBIE_VILLAGER - - WITHER - - WARDEN - - BLAZE - - DROWNED - - BREEZE - - ZOMBIFIED_PIGLIN - - EVOKER - - PILLAGER - - HUSK - - CREEPER - - VINDICATOR - - ZOMBIE - - ENDERMAN - - ELDER_GUARDIAN - - WITHER_SKELETON - - CAVE_SPIDER - - GUARDIAN - - RAVAGER - - PIGLIN - - BOGGED - - WITCH - - ENDERMITE - - ZOGLIN - - PIGLIN_BRUTE - - ILLUSIONER - - SPIDER - - VEX - Animals: - icon: SADDLE - limit: 200 - entities: - - SHEEP - - AXOLOTL - - DONKEY - - MOOSHROOM - - TRADER_LLAMA - - BEE - - HORSE - - ZOMBIE_HORSE - - PIG - - PARROT - - CHICKEN - - RABBIT - - SNIFFER - - FROG - - GOAT - - PANDA - - CAMEL - - STRIDER - - TURTLE - - CAT - - SKELETON_HORSE - - COW - - LLAMA - - ARMADILLO - - HOGLIN - - POLAR_BEAR - - WOLF - - MULE - - OCELOT - - FOX - - DOLPHIN - - COD - - PUFFERFISH - - TADPOLE - - SQUID - - SALMON - - TROPICAL_FISH - - GLOW_SQUID - - - - +# Game Modes covered by limits +gamemodes: +- AcidIsland +- BSkyBlock +- CaveBlock +- AOneBlock + +# Ignore this island's center block. For most worlds, this is bedrock, but for AOneBlock it is +# the magic block, so ignoring it from limits makes sense. +ignore-center-block: true + +# Permissions +# Island owners can be given permissions that override all general settings. +# +# Two formats are supported: +# 1. .island.limit.. — applied to every environment. +# e.g. bskyblock.island.limit.hopper.20 +# 2. .island.limit... — applied to one environment only. +# e.g. bskyblock.island.limit.nether.hopper.5 +# is one of: overworld, nether, end +# +# Permissions activate when the island owner logs in. +# +# Cooldown for player recount command in seconds +cooldown: 120 + +# Log limits on join (to console) +log-limits-on-join: true + +# Use async checks for snowmen and iron golums. Set to false if you see problems. +async-golums: true + +# General block limiting +# Use this section to limit how many blocks can be added to an island. +# 0 means the item will be blocked from placement completely. +# These limits apply independently in every environment (overworld, nether, end). +# A HOPPER limit of 10 means the player can place up to 10 hoppers in each of overworld, +# nether, and end — 30 hoppers total across the island. +# Use blocklimits-nether / blocklimits-end below to override per environment. +# The following general terms can be used: STAIRS, DOORS, SIGNS, LOGS, SNOW, LEAVES, +# SHULKER_BOXES, BANNERS, RAILS, ICE, BUTTONS, CANDLE_CAKES, CAMPFIRES, SLABS, STONE_BRICKS +blocklimits: + HOPPER: 10 + +# Override block limits in the nether for this game mode. +# Uncomment and add entries to set nether-only limits. +#blocklimits-nether: +# HOPPER: 5 + +# Override block limits in the end for this game mode. +#blocklimits-end: +# HOPPER: 5 + +# This section is for world-named limits and overrides any environment-default limit +# above for that specific world. Specify each world you want to limit individually +# (including nether and end worlds). If these worlds are not covered by the game modes +# above, nothing will happen. +worlds: + AcidIsland_world: + HOPPER: 11 + +# Default entity limits within a player's island space (protected area and to island limit). +# Limits apply independently per environment, the same way blocklimits do. +# Affects all types of creature spawning. Also includes entities like MINECARTS. +# Note: Only the first 49 limited blocks and entities are shown in the limits GUI. +entitylimits: + ENDERMAN: 5 + CHICKEN: 10 + +# Override entity limits in the nether. +#entitylimits-nether: +# PIGLIN: 20 +# HOGLIN: 5 + +# Override entity limits in the end. +#entitylimits-end: +# ENDERMAN: 50 + +# Entity Groups +# Name the group anything you like +# Select an icon, which is a Bukkit Material +# Select the limit for the total group +# List the entities in the group using Bukkit EntityTypes +# Group limits apply independently per environment. +entitygrouplimits: + Monsters: + icon: ROTTEN_FLESH + limit: 250 + entities: + - SKELETON + - SILVERFISH + - STRAY + - ZOMBIE_VILLAGER + - WITHER + - WARDEN + - BLAZE + - DROWNED + - BREEZE + - ZOMBIFIED_PIGLIN + - EVOKER + - PILLAGER + - HUSK + - CREEPER + - VINDICATOR + - ZOMBIE + - ENDERMAN + - ELDER_GUARDIAN + - WITHER_SKELETON + - CAVE_SPIDER + - GUARDIAN + - RAVAGER + - PIGLIN + - BOGGED + - WITCH + - ENDERMITE + - ZOGLIN + - PIGLIN_BRUTE + - ILLUSIONER + - SPIDER + - VEX + Animals: + icon: SADDLE + limit: 200 + entities: + - SHEEP + - AXOLOTL + - DONKEY + - MOOSHROOM + - TRADER_LLAMA + - BEE + - HORSE + - ZOMBIE_HORSE + - PIG + - PARROT + - CHICKEN + - RABBIT + - SNIFFER + - FROG + - GOAT + - PANDA + - CAMEL + - STRIDER + - TURTLE + - CAT + - SKELETON_HORSE + - COW + - LLAMA + - ARMADILLO + - HOGLIN + - POLAR_BEAR + - WOLF + - MULE + - OCELOT + - FOX + - DOLPHIN + - COD + - PUFFERFISH + - TADPOLE + - SQUID + - SALMON + - TROPICAL_FISH + - GLOW_SQUID + +# Override an entity group's limit per environment. The group must already be defined +# in entitygrouplimits above; only the numeric limit can be overridden, not the icon +# or member set. +#entitygrouplimits-nether: +# Monsters: 100 +#entitygrouplimits-end: +# Monsters: 50 diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index 96c73a1..1d2b10b 100755 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -51,12 +51,13 @@ island: block-limit-syntax: "[number]/[limit]" no-limits: "&c No limits set in this world" panel: - title-syntax: '[title] [sort]' - entity-group-name-syntax: '[name]' + title-syntax: '[title] [env]' + entity-group-name-syntax: '[name]' entity-name-syntax: '[name]' block-name-syntax: '[name]' - A2Z: "a > z" - Z2A: "z > a" + env-overworld: "Overworld" + env-nether: "Nether" + env-end: "End" errors: no-owner: "&c That island has no owner" not-on-island: "&c This location does not have limits set." diff --git a/src/test/java/world/bentobox/limits/JoinListenerTest.java b/src/test/java/world/bentobox/limits/JoinListenerTest.java index 7c3254e..0182ff4 100644 --- a/src/test/java/world/bentobox/limits/JoinListenerTest.java +++ b/src/test/java/world/bentobox/limits/JoinListenerTest.java @@ -1,7 +1,9 @@ package world.bentobox.limits; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.contains; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -21,6 +23,7 @@ import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.OfflinePlayer; +import org.bukkit.World.Environment; import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; import org.bukkit.event.player.PlayerJoinEvent; @@ -277,7 +280,7 @@ public void testOnPlayerJoinWithPermLimitsWrongSize() { PlayerJoinEvent e = new PlayerJoinEvent(player, "welcome"); jl.onPlayerJoin(e); verify(addon).logError( - "Player tastybento has permission: 'bskyblock.island.limit.my.perm.for.game' but format must be 'bskyblock.island.limit.MATERIAL.NUMBER', 'bskyblock.island.limit.ENTITY-TYPE.NUMBER', or 'bskyblock.island.limit.ENTITY-GROUP.NUMBER' Ignoring..."); + "Player tastybento has permission: 'bskyblock.island.limit.my.perm.for.game' but format must be 'bskyblock.island.limit.[ENV.]KEY.NUMBER' where ENV is overworld|nether|end and KEY is a material, entity type, or group name. Ignoring..."); } /** @@ -349,7 +352,7 @@ public void testOnPlayerJoinWithPermLimitsSuccess() { PlayerJoinEvent e = new PlayerJoinEvent(player, "welcome"); jl.onPlayerJoin(e); verify(addon, never()).logError(anyString()); - verify(ibc).setBlockLimit(eq(Material.STONE.getKey()), eq(24)); + verify(ibc).setBlockLimit(eq(Environment.NORMAL), eq(Material.STONE.getKey()), eq(24)); } /** @@ -367,7 +370,7 @@ public void testOnPlayerJoinWithPermLimitsSuccessEntity() { PlayerJoinEvent e = new PlayerJoinEvent(player, "welcome"); jl.onPlayerJoin(e); verify(addon, never()).logError(anyString()); - verify(ibc).setEntityLimit(eq(EntityType.BAT), eq(24)); + verify(ibc).setEntityLimit(eq(Environment.NORMAL), eq(EntityType.BAT), eq(24)); } /** @@ -385,7 +388,7 @@ public void testOnPlayerJoinWithPermLimitsSuccessEntityGroup() { PlayerJoinEvent e = new PlayerJoinEvent(player, "welcome"); jl.onPlayerJoin(e); verify(addon, never()).logError(anyString()); - verify(ibc).setEntityGroupLimit(eq("friendly"), eq(24)); + verify(ibc).setEntityGroupLimit(eq(Environment.NORMAL), eq("friendly"), eq(24)); } /** @@ -424,11 +427,11 @@ public void testOnPlayerJoinWithPermLimitsMultiPerms() { PlayerJoinEvent e = new PlayerJoinEvent(player, "welcome"); jl.onPlayerJoin(e); verify(addon, never()).logError(anyString()); - verify(ibc).setBlockLimit(eq(Material.STONE.getKey()), eq(24)); - verify(ibc).setBlockLimit(eq(Material.SHORT_GRASS.getKey()), eq(14)); - verify(ibc).setBlockLimit(eq(Material.DIRT.getKey()), eq(34)); - verify(ibc).setEntityLimit(eq(EntityType.CHICKEN), eq(34)); - verify(ibc).setEntityLimit(eq(EntityType.CAVE_SPIDER), eq(4)); + verify(ibc).setBlockLimit(eq(Environment.NORMAL), eq(Material.STONE.getKey()), eq(24)); + verify(ibc).setBlockLimit(eq(Environment.NORMAL), eq(Material.SHORT_GRASS.getKey()), eq(14)); + verify(ibc).setBlockLimit(eq(Environment.NORMAL), eq(Material.DIRT.getKey()), eq(34)); + verify(ibc).setEntityLimit(eq(Environment.NORMAL), eq(EntityType.CHICKEN), eq(34)); + verify(ibc).setEntityLimit(eq(Environment.NORMAL), eq(EntityType.CAVE_SPIDER), eq(4)); } /** @@ -437,7 +440,7 @@ public void testOnPlayerJoinWithPermLimitsMultiPerms() { @Test public void testOnPlayerJoinWithPermLimitsMultiPermsSameMaterial() { // IBC - set the block limit for STONE to be 25 already - when(ibc.getBlockLimit(any())).thenReturn(25); + when(ibc.getBlockLimit(any(Environment.class), any(NamespacedKey.class))).thenReturn(25); Set perms = new HashSet<>(); PermissionAttachmentInfo permAtt = mock(PermissionAttachmentInfo.class); when(permAtt.getPermission()).thenReturn("bskyblock.island.limit.STONE.24"); @@ -456,9 +459,9 @@ public void testOnPlayerJoinWithPermLimitsMultiPermsSameMaterial() { jl.onPlayerJoin(e); verify(addon, never()).logError(anyString()); // Only the limit over 25 should be set - verify(ibc, never()).setBlockLimit(eq(Material.STONE.getKey()), eq(24)); - verify(ibc, never()).setBlockLimit(eq(Material.STONE.getKey()), eq(14)); - verify(ibc).setBlockLimit(eq(Material.STONE.getKey()), eq(34)); + verify(ibc, never()).setBlockLimit(eq(Environment.NORMAL), eq(Material.STONE.getKey()), eq(24)); + verify(ibc, never()).setBlockLimit(eq(Environment.NORMAL), eq(Material.STONE.getKey()), eq(14)); + verify(ibc).setBlockLimit(eq(Environment.NORMAL), eq(Material.STONE.getKey()), eq(34)); } /** @@ -467,7 +470,7 @@ public void testOnPlayerJoinWithPermLimitsMultiPermsSameMaterial() { @Test public void testOnPlayerJoinWithPermLimitsMultiPermsSameEntity() { // IBC - set the entity limit for BAT to be 25 already - when(ibc.getEntityLimit(any())).thenReturn(25); + when(ibc.getEntityLimit(any(Environment.class), any(EntityType.class))).thenReturn(25); Set perms = new HashSet<>(); PermissionAttachmentInfo permAtt = mock(PermissionAttachmentInfo.class); when(permAtt.getPermission()).thenReturn("bskyblock.island.limit.BAT.24"); @@ -486,9 +489,9 @@ public void testOnPlayerJoinWithPermLimitsMultiPermsSameEntity() { jl.onPlayerJoin(e); verify(addon, never()).logError(anyString()); // Only the limit over 25 should be set - verify(ibc, never()).setEntityLimit(eq(EntityType.BAT), eq(24)); - verify(ibc, never()).setEntityLimit(eq(EntityType.BAT), eq(14)); - verify(ibc).setEntityLimit(eq(EntityType.BAT), eq(34)); + verify(ibc, never()).setEntityLimit(eq(Environment.NORMAL), eq(EntityType.BAT), eq(24)); + verify(ibc, never()).setEntityLimit(eq(Environment.NORMAL), eq(EntityType.BAT), eq(14)); + verify(ibc).setEntityLimit(eq(Environment.NORMAL), eq(EntityType.BAT), eq(34)); } /** @@ -520,16 +523,16 @@ public void testOnUnregisterIslandNotInWorld() { */ @Test public void testOnUnregisterIslandInWorld() { - @SuppressWarnings("unchecked") - Map map = mock(Map.class); - when(ibc.getBlockLimits()).thenReturn(map); when(addon.inGameModeWorld(any())).thenReturn(true); + when(addon.getBlockLimitListener()).thenReturn(bll); + when(bll.getIsland(island.getUniqueId())).thenReturn(ibc); IslandEvent e = new IslandEvent(island, null, false, null, IslandEvent.Reason.UNREGISTERED); jl.onUnregisterIsland(e); verify(island).getWorld(); verify(addon).getBlockLimitListener(); - verify(map).clear(); - + verify(ibc).clearAllBlockLimits(); + verify(ibc).clearAllEntityLimits(); + verify(ibc).clearAllEntityGroupLimits(); } /** @@ -540,7 +543,7 @@ public void testOnUnregisterIslandInWorldNullIBC() { when(bll.getIsland(anyString())).thenReturn(null); @SuppressWarnings("unchecked") Map map = mock(Map.class); - when(ibc.getBlockLimits()).thenReturn(map); + when(ibc.getBlockLimits(any(Environment.class))).thenReturn(map); when(addon.inGameModeWorld(any())).thenReturn(true); IslandEvent e = new IslandEvent(island, null, false, null, IslandEvent.Reason.UNREGISTERED); jl.onUnregisterIsland(e); @@ -550,4 +553,71 @@ public void testOnUnregisterIslandInWorldNullIBC() { } + /** + * 5-segment permission applies the limit independently to every standard env. + */ + @Test + public void unprefixedPermAppliesToAllEnvs() { + Set perms = new HashSet<>(); + PermissionAttachmentInfo p = mock(PermissionAttachmentInfo.class); + when(p.getPermission()).thenReturn("bskyblock.island.limit.STONE.24"); + when(p.getValue()).thenReturn(true); + perms.add(p); + when(player.getEffectivePermissions()).thenReturn(perms); + jl.onPlayerJoin(new PlayerJoinEvent(player, "welcome")); + verify(ibc).setBlockLimit(eq(Environment.NORMAL), eq(Material.STONE.getKey()), eq(24)); + verify(ibc).setBlockLimit(eq(Environment.NETHER), eq(Material.STONE.getKey()), eq(24)); + verify(ibc).setBlockLimit(eq(Environment.THE_END), eq(Material.STONE.getKey()), eq(24)); + } + + /** + * 6-segment permission with `nether` env applies only to nether. + */ + @Test + public void netherPrefixedPermAppliesToNetherOnly() { + Set perms = new HashSet<>(); + PermissionAttachmentInfo p = mock(PermissionAttachmentInfo.class); + when(p.getPermission()).thenReturn("bskyblock.island.limit.nether.HOPPER.5"); + when(p.getValue()).thenReturn(true); + perms.add(p); + when(player.getEffectivePermissions()).thenReturn(perms); + jl.onPlayerJoin(new PlayerJoinEvent(player, "welcome")); + verify(ibc).setBlockLimit(eq(Environment.NETHER), eq(Material.HOPPER.getKey()), eq(5)); + verify(ibc, never()).setBlockLimit(eq(Environment.NORMAL), any(NamespacedKey.class), anyInt()); + verify(ibc, never()).setBlockLimit(eq(Environment.THE_END), any(NamespacedKey.class), anyInt()); + } + + /** + * `end` and `overworld` aliases also work. + */ + @Test + public void endPrefixedPermAppliesToEndOnly() { + Set perms = new HashSet<>(); + PermissionAttachmentInfo p = mock(PermissionAttachmentInfo.class); + when(p.getPermission()).thenReturn("bskyblock.island.limit.end.ENDERMAN.50"); + when(p.getValue()).thenReturn(true); + perms.add(p); + when(player.getEffectivePermissions()).thenReturn(perms); + jl.onPlayerJoin(new PlayerJoinEvent(player, "welcome")); + verify(ibc).setEntityLimit(eq(Environment.THE_END), eq(EntityType.ENDERMAN), eq(50)); + verify(ibc, never()).setEntityLimit(eq(Environment.NORMAL), any(EntityType.class), anyInt()); + verify(ibc, never()).setEntityLimit(eq(Environment.NETHER), any(EntityType.class), anyInt()); + } + + /** + * 6-segment permission with an unknown env token is rejected with a logged error. + */ + @Test + public void unknownEnvPrefixIsRejected() { + Set perms = new HashSet<>(); + PermissionAttachmentInfo p = mock(PermissionAttachmentInfo.class); + when(p.getPermission()).thenReturn("bskyblock.island.limit.aether.HOPPER.5"); + when(p.getValue()).thenReturn(true); + perms.add(p); + when(player.getEffectivePermissions()).thenReturn(perms); + jl.onPlayerJoin(new PlayerJoinEvent(player, "welcome")); + verify(addon).logError(contains("'aether' is not a recognised environment")); + verify(ibc, never()).setBlockLimit(any(Environment.class), any(NamespacedKey.class), anyInt()); + } + } diff --git a/src/test/java/world/bentobox/limits/LimitsTest.java b/src/test/java/world/bentobox/limits/LimitsTest.java index dc786a5..642c3f7 100644 --- a/src/test/java/world/bentobox/limits/LimitsTest.java +++ b/src/test/java/world/bentobox/limits/LimitsTest.java @@ -266,7 +266,7 @@ public void testGetSettings() { assertNull(addon.getSettings()); addon.onEnable(); world.bentobox.limits.Settings set = addon.getSettings(); - assertFalse(set.getLimits().isEmpty()); + assertFalse(set.getLimits(org.bukkit.World.Environment.NORMAL).isEmpty()); } /** diff --git a/src/test/java/world/bentobox/limits/SettingsTest.java b/src/test/java/world/bentobox/limits/SettingsTest.java index ec23733..39194de 100644 --- a/src/test/java/world/bentobox/limits/SettingsTest.java +++ b/src/test/java/world/bentobox/limits/SettingsTest.java @@ -14,6 +14,7 @@ import java.util.Map; import org.bukkit.Material; +import org.bukkit.World.Environment; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.EntityType; @@ -66,7 +67,7 @@ public void testGameModesNotEmpty() { @Test public void testEntityLimitsParsed() { - Map limits = settings.getLimits(); + Map limits = settings.getLimits(Environment.NORMAL); assertNotNull(limits); assertFalse(limits.isEmpty()); assertEquals(5, limits.get(EntityType.ENDERMAN)); @@ -126,16 +127,16 @@ public void testGetGeneralWithAnimalsAndMobs() throws Exception { public void testUnknownEntityTypeLogsError() throws Exception { config.set("entitylimits.UNKNOWN_ENTITY_XYZ", 99); Settings s = new Settings(addon); - verify(addon, atLeastOnce()).logError("Unknown entity type: UNKNOWN_ENTITY_XYZ - skipping..."); - assertFalse(s.getLimits().containsValue(99)); + verify(addon, atLeastOnce()).logError("Unknown entity type in entitylimits: UNKNOWN_ENTITY_XYZ - skipping..."); + assertFalse(s.getLimits(Environment.NORMAL).containsValue(99)); } @Test public void testDisallowedEntityTypeLogsError() throws Exception { config.set("entitylimits.TNT", 10); Settings s = new Settings(addon); - verify(addon).logError("Entity type: TNT is not supported - skipping..."); - assertFalse(s.getLimits().containsKey(EntityType.TNT)); + verify(addon).logError("Entity type in entitylimits not supported: TNT - skipping..."); + assertFalse(s.getLimits(Environment.NORMAL).containsKey(EntityType.TNT)); } @Test diff --git a/src/test/java/world/bentobox/limits/calculators/ResultsTest.java b/src/test/java/world/bentobox/limits/calculators/ResultsTest.java index a72b964..a604168 100644 --- a/src/test/java/world/bentobox/limits/calculators/ResultsTest.java +++ b/src/test/java/world/bentobox/limits/calculators/ResultsTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import org.bukkit.World.Environment; import org.junit.jupiter.api.Test; import world.bentobox.limits.calculators.Results.Result; @@ -23,17 +24,17 @@ public void testConstructorWithState() { } @Test - public void testGetMdCountReturnsEmptyMultiset() { + public void testGetBlockCountReturnsEmptyMultiset() { Results results = new Results(); - assertNotNull(results.getMdCount()); - assertTrue(results.getMdCount().isEmpty()); + assertNotNull(results.getBlockCount(Environment.NORMAL)); + assertTrue(results.getBlockCount(Environment.NORMAL).isEmpty()); } @Test public void testGetEntityCountReturnsEmptyMultiset() { Results results = new Results(); - assertNotNull(results.getEntityCount()); - assertTrue(results.getEntityCount().isEmpty()); + assertNotNull(results.getEntityCount(Environment.NORMAL)); + assertTrue(results.getEntityCount(Environment.NORMAL).isEmpty()); } @Test diff --git a/src/test/java/world/bentobox/limits/commands/player/LimitPanelTest.java b/src/test/java/world/bentobox/limits/commands/player/LimitPanelTest.java index ec625ae..e0d9b75 100644 --- a/src/test/java/world/bentobox/limits/commands/player/LimitPanelTest.java +++ b/src/test/java/world/bentobox/limits/commands/player/LimitPanelTest.java @@ -13,6 +13,7 @@ import org.bukkit.NamespacedKey; import org.bukkit.World; +import org.bukkit.World.Environment; import org.bukkit.entity.Player; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -105,7 +106,7 @@ void testShowLimitsNoLimits() { when(im.getIsland(world, targetUUID)).thenReturn(island); when(island.getUniqueId()).thenReturn("island-id"); when(bll.getMaterialLimits(eq(world), eq("island-id"))).thenReturn(Collections.emptyMap()); - when(settings.getLimits()).thenReturn(Collections.emptyMap()); + when(settings.getLimits(Environment.NORMAL)).thenReturn(Collections.emptyMap()); // Target player is offline (MockBukkit returns null by default when no players added) limitPanel.showLimits(gm, user, targetUUID); @@ -118,7 +119,7 @@ void testShowLimitsTargetPlayerOfflineDoesNotCallCheckPerms() { when(im.getIsland(world, targetUUID)).thenReturn(island); when(island.getUniqueId()).thenReturn("island-id"); when(bll.getMaterialLimits(eq(world), eq("island-id"))).thenReturn(Collections.emptyMap()); - when(settings.getLimits()).thenReturn(Collections.emptyMap()); + when(settings.getLimits(Environment.NORMAL)).thenReturn(Collections.emptyMap()); // Target player is offline (MockBukkit returns null by default when no players added) limitPanel.showLimits(gm, user, targetUUID); diff --git a/src/test/java/world/bentobox/limits/commands/player/LimitTabTest.java b/src/test/java/world/bentobox/limits/commands/player/LimitTabTest.java index 43c2e3f..5953f5d 100644 --- a/src/test/java/world/bentobox/limits/commands/player/LimitTabTest.java +++ b/src/test/java/world/bentobox/limits/commands/player/LimitTabTest.java @@ -1,83 +1,86 @@ package world.bentobox.limits.commands.player; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.Collections; +import java.util.List; +import java.util.Map; -import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; import org.bukkit.World; -import org.bukkit.entity.Entity; +import org.bukkit.World.Environment; import org.bukkit.entity.EntityType; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; +import org.mockbukkit.mockbukkit.MockBukkit; import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.api.panels.PanelItem; +import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.managers.IslandWorldManager; import world.bentobox.limits.Limits; import world.bentobox.limits.Settings; -import org.mockbukkit.mockbukkit.MockBukkit; import world.bentobox.limits.objects.IslandBlockCount; @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) public class LimitTabTest { - @Mock - private Limits addon; - - private LimitTab lp; - - @Mock - private Island island; - @Mock - private World world; - @Mock - private World nether; - @Mock - private World end; - @Mock - private BentoBox plugin; - @Mock - private IslandWorldManager iwm; - @Mock - private Settings settings; + @Mock private Limits addon; + @Mock private Island island; + @Mock private World world; + @Mock private BentoBox plugin; + @Mock private IslandWorldManager iwm; + @Mock private Settings settings; + @Mock private User user; @BeforeEach public void setUp() { MockBukkit.mock(); - - // Island when(island.getWorld()).thenReturn(world); - // Addon when(addon.getPlugin()).thenReturn(plugin); when(addon.getSettings()).thenReturn(settings); - when(settings.getLimits()).thenReturn(Collections.emptyMap()); when(plugin.getIWM()).thenReturn(iwm); - when(iwm.isNetherIslands(any())).thenReturn(true); - when(iwm.isEndIslands(any())).thenReturn(true); - when(iwm.getNetherWorld(eq(world))).thenReturn(nether); - when(iwm.getEndWorld(eq(world))).thenReturn(end); - // Worlds - Entity entity = mock(Entity.class); - when(entity.getType()).thenReturn(EntityType.BAT); - when(entity.getLocation()).thenReturn(mock(Location.class)); - when(world.getEntities()).thenReturn(Collections.singletonList(entity)); - when(nether.getEntities()).thenReturn(Collections.singletonList(entity)); - when(end.getEntities()).thenReturn(Collections.singletonList(entity)); - lp = new LimitTab(addon, new IslandBlockCount("", ""), Collections.emptyMap(), island, world, null, LimitTab.SORT_BY.A2Z); + when(iwm.isNetherIslands(any())).thenReturn(false); + when(iwm.isEndIslands(any())).thenReturn(false); + when(settings.getLimits(any(Environment.class))).thenReturn(Collections.emptyMap()); + when(settings.getGroupLimits(any(Environment.class))).thenReturn(Collections.emptyMap()); + when(settings.getGroupLimitDefinitions()).thenReturn(Collections.emptyList()); + // User translations: render a known set of keys to their locale templates so that + // placeholder substitution produces something we can grep in test assertions. + lenient().when(user.getTranslation(any(World.class), anyString(), any(String[].class))).thenAnswer(inv -> { + Object[] all = inv.getArguments(); + String key = (String) all[1]; + Object[] vars = new Object[all.length - 2]; + System.arraycopy(all, 2, vars, 0, vars.length); + return renderTranslation(templateFor(key), vars); + }); + lenient().when(user.getTranslation(anyString(), any(String[].class))).thenAnswer(inv -> { + Object[] all = inv.getArguments(); + String key = (String) all[0]; + Object[] vars = new Object[all.length - 1]; + System.arraycopy(all, 1, vars, 0, vars.length); + return renderTranslation(templateFor(key), vars); + }); + lenient().when(user.getTranslation(anyString())).thenAnswer(inv -> templateFor(inv.getArgument(0))); + lenient().when(user.getTranslation(any(World.class), anyString())) + .thenAnswer(inv -> templateFor(inv.getArgument(1))); } @AfterEach @@ -85,31 +88,104 @@ public void tearDown() { MockBukkit.unmock(); } + /** Resolve a translation key to its locale template text (subset matching the en-US.yml). */ + private static String templateFor(String key) { + return switch (key) { + case "island.limits.panel.title-syntax" -> "[title] [env]"; + case "island.limits.panel.entity-name-syntax", + "island.limits.panel.entity-group-name-syntax", + "island.limits.panel.block-name-syntax" -> "[name]"; + case "island.limits.panel.env-overworld" -> "Overworld"; + case "island.limits.panel.env-nether" -> "Nether"; + case "island.limits.panel.env-end" -> "End"; + case "limits.panel-title" -> "Island limits"; + case "island.limits.block-limit-syntax" -> "[number]/[limit]"; + case "island.limits.max-color" -> "&c"; + case "island.limits.regular-color" -> "&a"; + default -> key; + }; + } + + /** Replace [name]/[number]/[limit]/[env] placeholders so test assertions can read them back. */ + private static String renderTranslation(String template, Object[] vars) { + StringBuilder out = new StringBuilder(template); + if (vars != null) { + for (int i = 0; i + 1 < vars.length; i += 2) { + String var = String.valueOf(vars[i]); + String val = String.valueOf(vars[i + 1]); + int idx; + while ((idx = out.indexOf(var)) >= 0) { + out.replace(idx, idx + var.length(), val); + } + } + } + return out.toString(); + } + @Test - @Disabled - public void testShowLimits() { - fail("Not yet implemented"); + public void blockCountReadsFromEnvKeyedIbc() { + IslandBlockCount ibc = new IslandBlockCount("island", "BSkyBlock"); + NamespacedKey hopper = Material.HOPPER.getKey(); + // Place 3 hoppers in the overworld and 7 in the nether. + for (int i = 0; i < 3; i++) ibc.add(Environment.NORMAL, hopper); + for (int i = 0; i < 7; i++) ibc.add(Environment.NETHER, hopper); + + Map matLimits = Map.of(hopper, 10); + + LimitTab overworldTab = new LimitTab(addon, ibc, matLimits, island, world, user, Environment.NORMAL); + LimitTab netherTab = new LimitTab(addon, ibc, matLimits, island, world, user, Environment.NETHER); + + assertEquals("3", findCount(overworldTab, "hopper"), "3 hoppers visible in overworld tab"); + assertEquals("7", findCount(netherTab, "hopper"), "7 hoppers visible in nether tab"); } @Test - public void testGetCountInIslandSpace() { - when(island.inIslandSpace(any(Location.class))).thenReturn(true); - EntityType ent = EntityType.BAT; - assertEquals(3L, lp.getCount(island, ent)); - ent = EntityType.GHAST; - assertEquals(0L, lp.getCount(island, ent)); - when(iwm.isEndIslands(any())).thenReturn(false); - ent = EntityType.BAT; - assertEquals(2L, lp.getCount(island, ent)); - when(iwm.isNetherIslands(any())).thenReturn(false); - ent = EntityType.BAT; - assertEquals(1L, lp.getCount(island, ent)); + public void entityCountReadsFromEnvKeyedIbc() { + IslandBlockCount ibc = new IslandBlockCount("island", "BSkyBlock"); + for (int i = 0; i < 4; i++) ibc.incrementEntity(Environment.NORMAL, EntityType.CHICKEN); + for (int i = 0; i < 9; i++) ibc.incrementEntity(Environment.NETHER, EntityType.CHICKEN); + when(settings.getLimits(Environment.NORMAL)).thenReturn(Map.of(EntityType.CHICKEN, 10)); + when(settings.getLimits(Environment.NETHER)).thenReturn(Map.of(EntityType.CHICKEN, 10)); + + LimitTab overworldTab = new LimitTab(addon, ibc, Collections.emptyMap(), island, world, user, Environment.NORMAL); + LimitTab netherTab = new LimitTab(addon, ibc, Collections.emptyMap(), island, world, user, Environment.NETHER); + + assertEquals("4", findCount(overworldTab, "chicken")); + assertEquals("9", findCount(netherTab, "chicken")); } @Test - public void testGetCountNotInIslandSpace() { - EntityType ent = EntityType.BAT; - assertEquals(0L, lp.getCount(island, ent)); + public void tabIconAndTitleReflectEnvironment() { + IslandBlockCount ibc = new IslandBlockCount("island", "BSkyBlock"); + LimitTab netherTab = new LimitTab(addon, ibc, Collections.emptyMap(), island, world, user, Environment.NETHER); + assertNotNull(netherTab.getIcon()); + assertEquals(Material.NETHERRACK, netherTab.getIcon().getItem().getType()); + assertTrue(netherTab.getName().contains("Nether"), "Title should contain 'Nether'; was: " + netherTab.getName()); + assertTrue(netherTab.getName().contains("Island limits"), + "Title should contain 'Island limits'; was: " + netherTab.getName()); } + /** + * Pull the count value (the {@code [number]} placeholder) out of the first panel-item description + * containing the given material/entity key. Returns null if not found. + */ + private static String findCount(LimitTab tab, String keyFragment) { + List items = tab.getPanelItems(); + for (PanelItem item : items) { + String desc = String.valueOf(item.getDescription()); + String name = String.valueOf(item.getName()); + if (name.toLowerCase().contains(keyFragment) || desc.toLowerCase().contains(keyFragment)) { + // description format is "[number]/[limit]" and renderTranslation replaced the placeholders + // Find the first run of digits + StringBuilder digits = new StringBuilder(); + for (int i = 0; i < desc.length(); i++) { + char c = desc.charAt(i); + if (Character.isDigit(c)) digits.append(c); + else if (digits.length() > 0) break; + } + return digits.length() > 0 ? digits.toString() : null; + } + } + return null; + } } diff --git a/src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java b/src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java index 94ef373..bf0971c 100644 --- a/src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java +++ b/src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java @@ -28,6 +28,7 @@ import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.World; +import org.bukkit.World.Environment; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.block.BlockState; @@ -147,6 +148,7 @@ public void setUp() throws Exception { blockLocation = new Location(world, 100, 65, 100); centerLocation = new Location(world, 0, 65, 0); when(island.getCenter()).thenReturn(centerLocation); + when(world.getEnvironment()).thenReturn(Environment.NORMAL); when(addon.getGameModeName(world)).thenReturn("BSkyBlock"); @@ -342,7 +344,7 @@ public void testGetMaterialLimitsDefaultOnly() { public void testGetMaterialLimitsIslandOverridesDefault() { String islandId = "island-override-test"; IslandBlockCount ibc = new IslandBlockCount(islandId, "BSkyBlock"); - ibc.setBlockLimit(Material.HOPPER.getKey(), 25); + ibc.setBlockLimit(Environment.NORMAL, Material.HOPPER.getKey(), 25); listener.setIsland(islandId, ibc); Map limits = listener.getMaterialLimits(world, islandId); @@ -388,7 +390,7 @@ public void testBlockPlaceAtLimitCancelsEvent() { // Pre-populate island with 10 HOPPERs (default config limit is 10) IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); for (int i = 0; i < 10; i++) { - ibc.add(Material.HOPPER.getKey()); + ibc.add(Environment.NORMAL, Material.HOPPER.getKey()); } listener.setIsland("test-island-id", ibc); @@ -443,9 +445,9 @@ public void testBlockPlaceOutsideGameModeWorldIgnored() { public void testBlockBreakDecrementsCount() { // Pre-populate with 3 STONE IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); - ibc.add(Material.STONE.getKey()); - ibc.add(Material.STONE.getKey()); - ibc.add(Material.STONE.getKey()); + ibc.add(Environment.NORMAL, Material.STONE.getKey()); + ibc.add(Environment.NORMAL, Material.STONE.getKey()); + ibc.add(Environment.NORMAL, Material.STONE.getKey()); listener.setIsland("test-island-id", ibc); Block block = mockBlock(Material.STONE, blockLocation); @@ -474,7 +476,7 @@ public void testBlockBreakCountNeverGoesNegative() { public void testBlockBreakWithMetadataIgnoreFlagSkipped() { // Pre-populate with 1 STONE IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); - ibc.add(Material.STONE.getKey()); + ibc.add(Environment.NORMAL, Material.STONE.getKey()); listener.setIsland("test-island-id", ibc); Block block = mockBlock(Material.STONE, blockLocation); @@ -493,8 +495,8 @@ public void testBlockBreakWithMetadataIgnoreFlagSkipped() { public void testBlockMultiPlaceAtLimitCancels() { // Set limit for OAK_PLANKS to 1 via island-specific limit IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); - ibc.setBlockLimit(Material.OAK_PLANKS.getKey(), 1); - ibc.add(Material.OAK_PLANKS.getKey()); + ibc.setBlockLimit(Environment.NORMAL, Material.OAK_PLANKS.getKey(), 1); + ibc.add(Environment.NORMAL, Material.OAK_PLANKS.getKey()); listener.setIsland("test-island-id", ibc); Block block = mockBlock(Material.OAK_PLANKS, blockLocation); @@ -527,8 +529,8 @@ public void testTurtleEggNonPhysicalActionIgnored() { @Test public void testBlockBurnDecrementsCount() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); - ibc.add(Material.OAK_PLANKS.getKey()); - ibc.add(Material.OAK_PLANKS.getKey()); + ibc.add(Environment.NORMAL, Material.OAK_PLANKS.getKey()); + ibc.add(Environment.NORMAL, Material.OAK_PLANKS.getKey()); listener.setIsland("test-island-id", ibc); Block block = mockBlock(Material.OAK_PLANKS, blockLocation); @@ -542,8 +544,8 @@ public void testBlockBurnDecrementsCount() { @Test public void testBlockFadeDecrementsCount() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); - ibc.add(Material.SAND.getKey()); - ibc.add(Material.SAND.getKey()); + ibc.add(Environment.NORMAL, Material.SAND.getKey()); + ibc.add(Environment.NORMAL, Material.SAND.getKey()); listener.setIsland("test-island-id", ibc); Block block = mockBlock(Material.SAND, blockLocation); @@ -558,8 +560,8 @@ public void testBlockFadeDecrementsCount() { @Test public void testLeavesDecayDecrementsCount() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); - ibc.add(Material.OAK_LEAVES.getKey()); - ibc.add(Material.OAK_LEAVES.getKey()); + ibc.add(Environment.NORMAL, Material.OAK_LEAVES.getKey()); + ibc.add(Environment.NORMAL, Material.OAK_LEAVES.getKey()); listener.setIsland("test-island-id", ibc); Block block = mockBlock(Material.OAK_LEAVES, blockLocation); @@ -622,7 +624,7 @@ public void testBlockFormIgnoresEntityBlockFormEvent() { @Test public void testBlockFormStateTransition() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); - ibc.add(Material.COBBLESTONE.getKey()); + ibc.add(Environment.NORMAL, Material.COBBLESTONE.getKey()); listener.setIsland("test-island-id", ibc); Block block = mockBlock(Material.COBBLESTONE, blockLocation); @@ -642,9 +644,9 @@ public void testBlockFormStateTransition() { @Test public void testBlockFormAtLimitCancelsAndRestoresOld() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); - ibc.add(Material.COBBLESTONE.getKey()); - ibc.setBlockLimit(Material.STONE.getKey(), 1); - ibc.add(Material.STONE.getKey()); + ibc.add(Environment.NORMAL, Material.COBBLESTONE.getKey()); + ibc.setBlockLimit(Environment.NORMAL, Material.STONE.getKey(), 1); + ibc.add(Environment.NORMAL, Material.STONE.getKey()); listener.setIsland("test-island-id", ibc); Block block = mockBlock(Material.COBBLESTONE, blockLocation); @@ -664,7 +666,7 @@ public void testBlockFormAtLimitCancelsAndRestoresOld() { @Test public void testEntityBlockFormStateTransition() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); - ibc.add(Material.COBBLESTONE.getKey()); + ibc.add(Environment.NORMAL, Material.COBBLESTONE.getKey()); listener.setIsland("test-island-id", ibc); Block block = mockBlock(Material.COBBLESTONE, blockLocation); @@ -687,7 +689,7 @@ public void testEntityBlockFormStateTransition() { @Test public void testBlockGrowIncrementsNewAndRemovesOld() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); - ibc.add(Material.DIRT.getKey()); + ibc.add(Environment.NORMAL, Material.DIRT.getKey()); listener.setIsland("test-island-id", ibc); Block block = mockBlock(Material.DIRT, blockLocation); @@ -706,8 +708,8 @@ public void testBlockGrowIncrementsNewAndRemovesOld() { @Test public void testBlockGrowAtLimitCancelsAndRestoresBlockData() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); - ibc.add(Material.DIRT.getKey()); - ibc.setBlockLimit(Material.GRASS_BLOCK.getKey(), 0); + ibc.add(Environment.NORMAL, Material.DIRT.getKey()); + ibc.setBlockLimit(Environment.NORMAL, Material.GRASS_BLOCK.getKey(), 0); listener.setIsland("test-island-id", ibc); Block block = mockBlock(Material.DIRT, blockLocation); @@ -733,7 +735,7 @@ public void testBlockGrowAtLimitCancelsAndRestoresBlockData() { @Test public void testEntityChangeBlockToNonAirAddsNewRemovesOld() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); - ibc.add(Material.DIRT.getKey()); + ibc.add(Environment.NORMAL, Material.DIRT.getKey()); listener.setIsland("test-island-id", ibc); Block block = mockBlock(Material.DIRT, blockLocation); @@ -753,9 +755,9 @@ public void testEntityChangeBlockToNonAirAddsNewRemovesOld() { @Test public void testEntityChangeBlockToNonAirAtLimitCancels() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); - ibc.add(Material.DIRT.getKey()); - ibc.setBlockLimit(Material.COBBLESTONE.getKey(), 1); - ibc.add(Material.COBBLESTONE.getKey()); + ibc.add(Environment.NORMAL, Material.DIRT.getKey()); + ibc.setBlockLimit(Environment.NORMAL, Material.COBBLESTONE.getKey(), 1); + ibc.add(Environment.NORMAL, Material.COBBLESTONE.getKey()); listener.setIsland("test-island-id", ibc); Block block = mockBlock(Material.DIRT, blockLocation); @@ -775,8 +777,8 @@ public void testEntityChangeBlockToNonAirAtLimitCancels() { @Test public void testEntityChangeBlockFarmlandRemovesCropAbove() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); - ibc.add(Material.FARMLAND.getKey()); - ibc.add(Material.OAK_PLANKS.getKey()); + ibc.add(Environment.NORMAL, Material.FARMLAND.getKey()); + ibc.add(Environment.NORMAL, Material.OAK_PLANKS.getKey()); listener.setIsland("test-island-id", ibc); Block block = mockBlock(Material.FARMLAND, blockLocation); @@ -802,9 +804,9 @@ public void testEntityChangeBlockFarmlandRemovesCropAbove() { @Test public void testBlockBreakSugarCaneCascade() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); - ibc.add(Material.SUGAR_CANE.getKey()); - ibc.add(Material.SUGAR_CANE.getKey()); - ibc.add(Material.SUGAR_CANE.getKey()); + ibc.add(Environment.NORMAL, Material.SUGAR_CANE.getKey()); + ibc.add(Environment.NORMAL, Material.SUGAR_CANE.getKey()); + ibc.add(Environment.NORMAL, Material.SUGAR_CANE.getKey()); listener.setIsland("test-island-id", ibc); when(world.getMaxHeight()).thenReturn(320); @@ -830,9 +832,9 @@ public void testBlockBreakSugarCaneCascade() { @Test public void testBlockBreakBambooCascade() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); - ibc.add(Material.BAMBOO.getKey()); - ibc.add(Material.BAMBOO.getKey()); - ibc.add(Material.BAMBOO.getKey()); + ibc.add(Environment.NORMAL, Material.BAMBOO.getKey()); + ibc.add(Environment.NORMAL, Material.BAMBOO.getKey()); + ibc.add(Environment.NORMAL, Material.BAMBOO.getKey()); listener.setIsland("test-island-id", ibc); when(world.getMaxHeight()).thenReturn(320); @@ -856,8 +858,8 @@ public void testBlockBreakBambooCascade() { @Test public void testBlockBreakRedstoneOnTopRemoved() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); - ibc.add(Material.STONE.getKey()); - ibc.add(Material.REDSTONE_WIRE.getKey()); + ibc.add(Environment.NORMAL, Material.STONE.getKey()); + ibc.add(Environment.NORMAL, Material.REDSTONE_WIRE.getKey()); listener.setIsland("test-island-id", ibc); Block stoneBlock = mockBlock(Material.STONE, blockLocation); @@ -874,9 +876,9 @@ public void testBlockBreakRedstoneOnTopRemoved() { @Test public void testBlockBreakRedstoneWallTorchOnSideRemoved() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); - ibc.add(Material.STONE.getKey()); + ibc.add(Environment.NORMAL, Material.STONE.getKey()); // fixMaterial normalises REDSTONE_WALL_TORCH → REDSTONE_TORCH - ibc.add(Material.REDSTONE_TORCH.getKey()); + ibc.add(Environment.NORMAL, Material.REDSTONE_TORCH.getKey()); listener.setIsland("test-island-id", ibc); Block stoneBlock = mockBlock(Material.STONE, blockLocation); @@ -912,7 +914,7 @@ public void testBlockPlaceCenterBlockIgnored() { @Test public void testTurtleEggPhysicalBreakDecrementsCount() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); - ibc.add(Material.TURTLE_EGG.getKey()); + ibc.add(Environment.NORMAL, Material.TURTLE_EGG.getKey()); listener.setIsland("test-island-id", ibc); Block block = mockBlock(Material.TURTLE_EGG, blockLocation); @@ -928,9 +930,9 @@ public void testTurtleEggPhysicalBreakDecrementsCount() { @Test public void testBlockExplodeDecrementsBatch() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); - ibc.add(Material.STONE.getKey()); - ibc.add(Material.STONE.getKey()); - ibc.add(Material.STONE.getKey()); + ibc.add(Environment.NORMAL, Material.STONE.getKey()); + ibc.add(Environment.NORMAL, Material.STONE.getKey()); + ibc.add(Environment.NORMAL, Material.STONE.getKey()); listener.setIsland("test-island-id", ibc); List blocks = List.of( @@ -949,9 +951,9 @@ public void testBlockExplodeDecrementsBatch() { @Test public void testEntityExplodeDecrementsBatch() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); - ibc.add(Material.STONE.getKey()); - ibc.add(Material.STONE.getKey()); - ibc.add(Material.STONE.getKey()); + ibc.add(Environment.NORMAL, Material.STONE.getKey()); + ibc.add(Environment.NORMAL, Material.STONE.getKey()); + ibc.add(Environment.NORMAL, Material.STONE.getKey()); listener.setIsland("test-island-id", ibc); List blocks = List.of( @@ -971,7 +973,7 @@ public void testEntityExplodeDecrementsBatch() { @Test public void testEntityChangeBlockToAirDecrements() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); - ibc.add(Material.STONE.getKey()); + ibc.add(Environment.NORMAL, Material.STONE.getKey()); listener.setIsland("test-island-id", ibc); Block block = mockBlock(Material.STONE, blockLocation); @@ -990,7 +992,7 @@ public void testEntityChangeBlockToAirDecrements() { @Test public void testBlockFromToLiquidDestroysRedstone() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); - ibc.add(Material.REDSTONE_WIRE.getKey()); + ibc.add(Environment.NORMAL, Material.REDSTONE_WIRE.getKey()); listener.setIsland("test-island-id", ibc); Block sourceBlock = mockBlock(Material.WATER, blockLocation); @@ -1019,9 +1021,9 @@ public void testIslandLimitTakesPrecedenceOverWorldLimit() throws Exception { // Set island-specific limit for COBBLESTONE = 2, pre-populate with 2 IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); - ibc.setBlockLimit(Material.COBBLESTONE.getKey(), 2); - ibc.add(Material.COBBLESTONE.getKey()); - ibc.add(Material.COBBLESTONE.getKey()); + ibc.setBlockLimit(Environment.NORMAL, Material.COBBLESTONE.getKey(), 2); + ibc.add(Environment.NORMAL, Material.COBBLESTONE.getKey()); + ibc.add(Environment.NORMAL, Material.COBBLESTONE.getKey()); listener.setIsland("test-island-id", ibc); Block block = mockBlock(Material.COBBLESTONE, blockLocation); @@ -1036,12 +1038,7 @@ public void testIslandLimitTakesPrecedenceOverWorldLimit() throws Exception { @Test public void testWorldLimitTakesPrecedenceOverDefaultLimit() throws Exception { // Set default limit for COBBLESTONE = 10 - Field defaultLimitField = BlockLimitsListener.class.getDeclaredField("defaultLimitMap"); - defaultLimitField.setAccessible(true); - @SuppressWarnings("unchecked") - Map defaultLimitMap = - (Map) defaultLimitField.get(listener); - defaultLimitMap.put(Material.COBBLESTONE.getKey(), 10); + listener.getEnvDefaultLimitMap().get(Environment.NORMAL).put(Material.COBBLESTONE.getKey(), 10); // Set world limit for COBBLESTONE = 3 Field worldLimitField = BlockLimitsListener.class.getDeclaredField("worldLimitMap"); @@ -1055,9 +1052,9 @@ public void testWorldLimitTakesPrecedenceOverDefaultLimit() throws Exception { // No island-specific limit; pre-populate with 3 IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); - ibc.add(Material.COBBLESTONE.getKey()); - ibc.add(Material.COBBLESTONE.getKey()); - ibc.add(Material.COBBLESTONE.getKey()); + ibc.add(Environment.NORMAL, Material.COBBLESTONE.getKey()); + ibc.add(Environment.NORMAL, Material.COBBLESTONE.getKey()); + ibc.add(Environment.NORMAL, Material.COBBLESTONE.getKey()); listener.setIsland("test-island-id", ibc); Block block = mockBlock(Material.COBBLESTONE, blockLocation); @@ -1072,17 +1069,12 @@ public void testWorldLimitTakesPrecedenceOverDefaultLimit() throws Exception { @Test public void testDefaultLimitAppliedWhenNoIslandOrWorldLimit() throws Exception { // Set default limit for COBBLESTONE = 2 - Field defaultLimitField = BlockLimitsListener.class.getDeclaredField("defaultLimitMap"); - defaultLimitField.setAccessible(true); - @SuppressWarnings("unchecked") - Map defaultLimitMap = - (Map) defaultLimitField.get(listener); - defaultLimitMap.put(Material.COBBLESTONE.getKey(), 2); + listener.getEnvDefaultLimitMap().get(Environment.NORMAL).put(Material.COBBLESTONE.getKey(), 2); // No island or world limit; pre-populate with 2 IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); - ibc.add(Material.COBBLESTONE.getKey()); - ibc.add(Material.COBBLESTONE.getKey()); + ibc.add(Environment.NORMAL, Material.COBBLESTONE.getKey()); + ibc.add(Environment.NORMAL, Material.COBBLESTONE.getKey()); listener.setIsland("test-island-id", ibc); Block block = mockBlock(Material.COBBLESTONE, blockLocation); @@ -1097,20 +1089,15 @@ public void testDefaultLimitAppliedWhenNoIslandOrWorldLimit() throws Exception { @Test public void testIslandOffsetIncreasesEffectiveLimit() throws Exception { // Set default limit for COBBLESTONE = 2 - Field defaultLimitField = BlockLimitsListener.class.getDeclaredField("defaultLimitMap"); - defaultLimitField.setAccessible(true); - @SuppressWarnings("unchecked") - Map defaultLimitMap = - (Map) defaultLimitField.get(listener); - defaultLimitMap.put(Material.COBBLESTONE.getKey(), 2); + listener.getEnvDefaultLimitMap().get(Environment.NORMAL).put(Material.COBBLESTONE.getKey(), 2); // Set island offset = +3 (effective limit = 5); pre-populate with 4 IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); - ibc.setBlockLimitsOffset(Material.COBBLESTONE.getKey(), 3); - ibc.add(Material.COBBLESTONE.getKey()); - ibc.add(Material.COBBLESTONE.getKey()); - ibc.add(Material.COBBLESTONE.getKey()); - ibc.add(Material.COBBLESTONE.getKey()); + ibc.setBlockLimitsOffset(Environment.NORMAL, Material.COBBLESTONE.getKey(), 3); + ibc.add(Environment.NORMAL, Material.COBBLESTONE.getKey()); + ibc.add(Environment.NORMAL, Material.COBBLESTONE.getKey()); + ibc.add(Environment.NORMAL, Material.COBBLESTONE.getKey()); + ibc.add(Environment.NORMAL, Material.COBBLESTONE.getKey()); listener.setIsland("test-island-id", ibc); Block block = mockBlock(Material.COBBLESTONE, blockLocation); diff --git a/src/test/java/world/bentobox/limits/listeners/EntityLimitListenerTest.java b/src/test/java/world/bentobox/limits/listeners/EntityLimitListenerTest.java index 3816624..814d305 100644 --- a/src/test/java/world/bentobox/limits/listeners/EntityLimitListenerTest.java +++ b/src/test/java/world/bentobox/limits/listeners/EntityLimitListenerTest.java @@ -19,6 +19,7 @@ import org.bukkit.Location; import org.bukkit.World; +import org.bukkit.World.Environment; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.configuration.file.FileConfiguration; @@ -41,6 +42,7 @@ import org.bukkit.Material; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -96,7 +98,12 @@ public void setUp() throws Exception { when(island.getUniqueId()).thenReturn(UUID.randomUUID().toString()); when(island.inIslandSpace(any(Location.class))).thenReturn(true); - ibc = new IslandBlockCount("",""); + ibc = new IslandBlockCount("test-island-id","BSkyBlock"); + // Seed initial entity count for ENDERMAN to 4 (to match the 4 in collection) + ibc.incrementEntity(Environment.NORMAL, EntityType.ENDERMAN); + ibc.incrementEntity(Environment.NORMAL, EntityType.ENDERMAN); + ibc.incrementEntity(Environment.NORMAL, EntityType.ENDERMAN); + ibc.incrementEntity(Environment.NORMAL, EntityType.ENDERMAN); when(bll.getIsland(anyString())).thenReturn(ibc); when(addon.getBlockLimitListener()).thenReturn(bll); @@ -110,6 +117,7 @@ public void setUp() throws Exception { // World when(location.getWorld()).thenReturn(world); when(ent.getWorld()).thenReturn(world); + when(world.getEnvironment()).thenReturn(Environment.NORMAL); collection = new ArrayList<>(); collection.add(ent); collection.add(ent); @@ -167,7 +175,9 @@ public void testAtLimitUnderLimit() { */ @Test public void testAtLimitAtLimit() { - collection.add(ent); + // The default limit for ENDERMAN is 5 (from config), and we have 4 already + // Adding one more puts us at the limit + ibc.incrementEntity(Environment.NORMAL, EntityType.ENDERMAN); AtLimitResult result = ell.atLimit(island, ent); assertTrue(result.hit()); assertEquals(EntityType.ENDERMAN, result.getTypelimit().getKey()); @@ -180,7 +190,7 @@ public void testAtLimitAtLimit() { */ @Test public void testAtLimitUnderLimitIslandLimit() { - ibc.setEntityLimit(EntityType.ENDERMAN, 6); + ibc.setEntityLimit(Environment.NORMAL, EntityType.ENDERMAN, 6); AtLimitResult result = ell.atLimit(island, ent); assertFalse(result.hit()); } @@ -190,8 +200,9 @@ public void testAtLimitUnderLimitIslandLimit() { */ @Test public void testAtLimitAtLimitIslandLimitNotAtLimit() { - ibc.setEntityLimit(EntityType.ENDERMAN, 6); - collection.add(ent); + // Island limit of 6, we have 4 already, so 5th entity should not be at limit + ibc.setEntityLimit(Environment.NORMAL, EntityType.ENDERMAN, 6); + ibc.incrementEntity(Environment.NORMAL, EntityType.ENDERMAN); AtLimitResult result = ell.atLimit(island, ent); assertFalse(result.hit()); } @@ -201,9 +212,10 @@ public void testAtLimitAtLimitIslandLimitNotAtLimit() { */ @Test public void testAtLimitAtLimitIslandLimit() { - ibc.setEntityLimit(EntityType.ENDERMAN, 6); - collection.add(ent); - collection.add(ent); + // Island limit of 6, we have 4, add 2 more to reach exactly 6 + ibc.setEntityLimit(Environment.NORMAL, EntityType.ENDERMAN, 6); + ibc.incrementEntity(Environment.NORMAL, EntityType.ENDERMAN); + ibc.incrementEntity(Environment.NORMAL, EntityType.ENDERMAN); AtLimitResult result = ell.atLimit(island, ent); assertTrue(result.hit()); assertEquals(EntityType.ENDERMAN, result.getTypelimit().getKey()); @@ -267,13 +279,12 @@ public void testCreatureSpawnOnSpawnIslandAllowed() { @Test public void testSpawnerSpawnReasonNoPlayerNotification() { - // Put CHICKEN at limit so it gets cancelled - LivingEntity chicken = mockEntity(EntityType.CHICKEN, location); - List chickens = new ArrayList<>(); + // Put CHICKEN at limit (10 from config) so it gets cancelled + // Pre-fill with 10 chickens for (int i = 0; i < 10; i++) { - chickens.add(mockEntity(EntityType.CHICKEN, location)); + ibc.incrementEntity(Environment.NORMAL, EntityType.CHICKEN); } - when(world.getNearbyEntities(any())).thenReturn(chickens); + LivingEntity chicken = mockEntity(EntityType.CHICKEN, location); CreatureSpawnEvent event = new CreatureSpawnEvent(chicken, SpawnReason.SPAWNER); @@ -328,7 +339,7 @@ public void testBreedingOpPlayerBypasses() { when(mother.getUniqueId()).thenReturn(UUID.randomUUID()); // Set CHICKEN limit to 0 so any entity would be over limit - ibc.setEntityLimit(EntityType.CHICKEN, 0); + ibc.setEntityLimit(Environment.NORMAL, EntityType.CHICKEN, 0); EntityBreedEvent event = new EntityBreedEvent(child, mother, father, opPlayer, null, 0); @@ -344,11 +355,9 @@ public void testBreedingOpPlayerBypasses() { @Test public void testCreatureSpawnAtLimitCancels() { // Set island-specific CHICKEN limit to 1, pre-fill with 1 - ibc.setEntityLimit(EntityType.CHICKEN, 1); + ibc.setEntityLimit(Environment.NORMAL, EntityType.CHICKEN, 1); + ibc.incrementEntity(Environment.NORMAL, EntityType.CHICKEN); LivingEntity chicken = mockEntity(EntityType.CHICKEN, location); - List chickens = new ArrayList<>(); - chickens.add(mockEntity(EntityType.CHICKEN, location)); - when(world.getNearbyEntities(any())).thenReturn(chickens); CreatureSpawnEvent event = new CreatureSpawnEvent(chicken, SpawnReason.NATURAL); @@ -360,15 +369,13 @@ public void testCreatureSpawnAtLimitCancels() { @Test public void testCreatureSpawnBreedingVillagerProcessed() { // Set island-specific VILLAGER limit to 1, pre-fill with 1 - ibc.setEntityLimit(EntityType.VILLAGER, 1); - LivingEntity villager = mock(Villager.class); + ibc.setEntityLimit(Environment.NORMAL, EntityType.VILLAGER, 1); + ibc.incrementEntity(Environment.NORMAL, EntityType.VILLAGER); + Villager villager = mock(Villager.class); when(villager.getType()).thenReturn(EntityType.VILLAGER); when(villager.getLocation()).thenReturn(location); when(villager.getWorld()).thenReturn(world); when(villager.getUniqueId()).thenReturn(UUID.randomUUID()); - List villagers = new ArrayList<>(); - villagers.add(villager); - when(world.getNearbyEntities(any())).thenReturn(villagers); CreatureSpawnEvent event = new CreatureSpawnEvent(villager, SpawnReason.BREEDING); @@ -386,11 +393,9 @@ public void testCreatureSpawnDebounceSkipsSecond() throws Exception { List justSpawned = (List) justSpawnedField.get(ell); // Set CHICKEN at limit - ibc.setEntityLimit(EntityType.CHICKEN, 1); + ibc.setEntityLimit(Environment.NORMAL, EntityType.CHICKEN, 1); + ibc.incrementEntity(Environment.NORMAL, EntityType.CHICKEN); LivingEntity chicken = mockEntity(EntityType.CHICKEN, location); - List chickens = new ArrayList<>(); - chickens.add(mockEntity(EntityType.CHICKEN, location)); - when(world.getNearbyEntities(any())).thenReturn(chickens); // Add entity UUID to justSpawned (debounce) justSpawned.add(chicken.getUniqueId()); @@ -407,17 +412,14 @@ public void testCreatureSpawnDebounceSkipsSecond() throws Exception { @Test public void testVehicleCreateAtLimitCancels() { // Set island-specific MINECART limit to 1, pre-fill with 1 - ibc.setEntityLimit(EntityType.MINECART, 1); + ibc.setEntityLimit(Environment.NORMAL, EntityType.MINECART, 1); + ibc.incrementEntity(Environment.NORMAL, EntityType.MINECART); Minecart minecart = mock(Minecart.class); when(minecart.getType()).thenReturn(EntityType.MINECART); when(minecart.getLocation()).thenReturn(location); when(minecart.getWorld()).thenReturn(world); when(minecart.getUniqueId()).thenReturn(UUID.randomUUID()); - List minecarts = new ArrayList<>(); - minecarts.add(minecart); - when(world.getNearbyEntities(any())).thenReturn(minecarts); - VehicleCreateEvent event = new VehicleCreateEvent(minecart); ell.onMinecart(event); @@ -432,7 +434,7 @@ public void testVehicleCreateDebounceSkipsSecond() throws Exception { @SuppressWarnings("unchecked") List justSpawned = (List) justSpawnedField.get(ell); - ibc.setEntityLimit(EntityType.MINECART, 1); + ibc.setEntityLimit(Environment.NORMAL, EntityType.MINECART, 1); Minecart minecart = mock(Minecart.class); when(minecart.getType()).thenReturn(EntityType.MINECART); when(minecart.getLocation()).thenReturn(location); @@ -459,17 +461,14 @@ public void testVehicleCreateDebounceSkipsSecond() throws Exception { @Test public void testHangingPlaceAtLimitCancels() { // Set island-specific PAINTING limit to 1, pre-fill with 1 - ibc.setEntityLimit(EntityType.PAINTING, 1); + ibc.setEntityLimit(Environment.NORMAL, EntityType.PAINTING, 1); + ibc.incrementEntity(Environment.NORMAL, EntityType.PAINTING); Painting painting = mock(Painting.class); when(painting.getType()).thenReturn(EntityType.PAINTING); when(painting.getLocation()).thenReturn(location); when(painting.getWorld()).thenReturn(world); when(painting.getUniqueId()).thenReturn(UUID.randomUUID()); - List paintings = new ArrayList<>(); - paintings.add(painting); - when(world.getNearbyEntities(any())).thenReturn(paintings); - Player player = mock(Player.class); when(player.isOp()).thenReturn(false); when(player.hasPermission(anyString())).thenReturn(false); @@ -486,7 +485,7 @@ public void testHangingPlaceAtLimitCancels() { @Test public void testHangingPlaceOpPlayerBypasses() { // Set island-specific PAINTING limit to 1, pre-fill with 1 - ibc.setEntityLimit(EntityType.PAINTING, 1); + ibc.setEntityLimit(Environment.NORMAL, EntityType.PAINTING, 1); Painting painting = mock(Painting.class); when(painting.getType()).thenReturn(EntityType.PAINTING); when(painting.getLocation()).thenReturn(location); @@ -516,16 +515,25 @@ public void testEntityGroupLimitBlocksSpawnWhenGroupFull() { // Create a custom group "testanimals" covering CHICKEN and COW with limit 2 EntityGroup testGroup = new EntityGroup("testanimals", Set.of(EntityType.CHICKEN, EntityType.COW), 2, Material.BARRIER); Settings settings = addon.getSettings(); + // Add to entity type → group lookup settings.getGroupLimits().put(EntityType.CHICKEN, new ArrayList<>(List.of(testGroup))); settings.getGroupLimits().put(EntityType.COW, new ArrayList<>(List.of(testGroup))); + // Also add the per-environment limit for the group (via reflection since we can't directly access the map) + try { + java.lang.reflect.Field envGroupLimitsField = Settings.class.getDeclaredField("envGroupLimits"); + envGroupLimitsField.setAccessible(true); + @SuppressWarnings("unchecked") + java.util.Map> envGroupLimits = + (java.util.Map>) envGroupLimitsField.get(settings); + envGroupLimits.computeIfAbsent(Environment.NORMAL, k -> new java.util.HashMap<>()) + .put("testanimals", 2); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } // Pre-fill with 2 entities in the group (1 CHICKEN + 1 COW) - LivingEntity chickenEntity = mockEntity(EntityType.CHICKEN, location); - LivingEntity cowEntity = mockEntity(EntityType.COW, location); - List entities = new ArrayList<>(); - entities.add(chickenEntity); - entities.add(cowEntity); - when(world.getNearbyEntities(any())).thenReturn(entities); + ibc.incrementEntity(Environment.NORMAL, EntityType.CHICKEN); + ibc.incrementEntity(Environment.NORMAL, EntityType.COW); // Try to spawn another CHICKEN LivingEntity newChicken = mockEntity(EntityType.CHICKEN, location); diff --git a/src/test/java/world/bentobox/limits/objects/IslandBlockCountTest.java b/src/test/java/world/bentobox/limits/objects/IslandBlockCountTest.java index 2047665..81f2737 100644 --- a/src/test/java/world/bentobox/limits/objects/IslandBlockCountTest.java +++ b/src/test/java/world/bentobox/limits/objects/IslandBlockCountTest.java @@ -6,6 +6,7 @@ import org.bukkit.Material; import org.bukkit.NamespacedKey; +import org.bukkit.World.Environment; import org.bukkit.entity.EntityType; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -42,25 +43,25 @@ public void testConstructorSetsFields() { @Test public void testAddIncrementsCount() { - ibc.add(stoneKey); + ibc.add(Environment.NORMAL, stoneKey); assertEquals(1, ibc.getBlockCount(stoneKey)); - ibc.add(stoneKey); + ibc.add(Environment.NORMAL, stoneKey); assertEquals(2, ibc.getBlockCount(stoneKey)); } @Test public void testRemoveDecrementsAndRemovesAtZero() { - ibc.add(stoneKey); - ibc.add(stoneKey); + ibc.add(Environment.NORMAL, stoneKey); + ibc.add(Environment.NORMAL, stoneKey); assertEquals(2, ibc.getBlockCount(stoneKey)); - ibc.remove(stoneKey); + ibc.remove(Environment.NORMAL, stoneKey); assertEquals(1, ibc.getBlockCount(stoneKey)); - ibc.remove(stoneKey); + ibc.remove(Environment.NORMAL, stoneKey); // At 0 the entry should be removed entirely assertEquals(0, ibc.getBlockCount(stoneKey)); - assertFalse(ibc.getBlockCounts().containsKey(stoneKey)); + assertFalse(ibc.getBlockCounts(Environment.NORMAL).containsKey(stoneKey)); } @Test @@ -70,102 +71,102 @@ public void testGetBlockCountUnknownMaterial() { @Test public void testGetBlockLimitUnknownMaterial() { - assertEquals(-1, ibc.getBlockLimit(stoneKey)); + assertEquals(-1, ibc.getBlockLimit(Environment.NORMAL, stoneKey)); } @Test public void testSetBlockLimitThenGet() { - ibc.setBlockLimit(stoneKey, 50); - assertEquals(50, ibc.getBlockLimit(stoneKey)); + ibc.setBlockLimit(Environment.NORMAL, stoneKey, 50); + assertEquals(50, ibc.getBlockLimit(Environment.NORMAL, stoneKey)); } @Test public void testIsAtLimitNoLimitSet() { - assertFalse(ibc.isAtLimit(stoneKey)); + assertFalse(ibc.isAtLimit(Environment.NORMAL, stoneKey)); } @Test public void testIsAtLimitCountBelowLimit() { - ibc.setBlockLimit(stoneKey, 10); - ibc.add(stoneKey); - assertFalse(ibc.isAtLimit(stoneKey)); + ibc.setBlockLimit(Environment.NORMAL, stoneKey, 10); + ibc.add(Environment.NORMAL, stoneKey); + assertFalse(ibc.isAtLimit(Environment.NORMAL, stoneKey)); } @Test public void testIsAtLimitCountAtLimit() { - ibc.setBlockLimit(stoneKey, 2); - ibc.add(stoneKey); - ibc.add(stoneKey); - assertTrue(ibc.isAtLimit(stoneKey)); + ibc.setBlockLimit(Environment.NORMAL, stoneKey, 2); + ibc.add(Environment.NORMAL, stoneKey); + ibc.add(Environment.NORMAL, stoneKey); + assertTrue(ibc.isAtLimit(Environment.NORMAL, stoneKey)); } @Test public void testIsAtLimitWithOffset() { // count=10, limit=10, offset=5 → effective limit is 15, so NOT at limit - ibc.setBlockLimit(stoneKey, 10); - ibc.setBlockLimitsOffset(stoneKey, 5); + ibc.setBlockLimit(Environment.NORMAL, stoneKey, 10); + ibc.setBlockLimitsOffset(Environment.NORMAL, stoneKey, 5); for (int i = 0; i < 10; i++) { - ibc.add(stoneKey); + ibc.add(Environment.NORMAL, stoneKey); } - assertFalse(ibc.isAtLimit(stoneKey)); + assertFalse(ibc.isAtLimit(Environment.NORMAL, stoneKey)); } @Test public void testIsAtLimitOverloadWithOffset() { - ibc.setBlockLimitsOffset(stoneKey, 5); + ibc.setBlockLimitsOffset(Environment.NORMAL, stoneKey, 5); for (int i = 0; i < 10; i++) { - ibc.add(stoneKey); + ibc.add(Environment.NORMAL, stoneKey); } // isAtLimit(material, limit) → count(10) >= limit(10) + offset(5) = 15 → false - assertFalse(ibc.isAtLimit(stoneKey, 10)); + assertFalse(ibc.isAtLimit(Environment.NORMAL, stoneKey, 10)); // count(10) >= limit(5) + offset(5) = 10 → true - assertTrue(ibc.isAtLimit(stoneKey, 5)); + assertTrue(ibc.isAtLimit(Environment.NORMAL, stoneKey, 5)); } @Test public void testIsBlockLimited() { - assertFalse(ibc.isBlockLimited(stoneKey)); - ibc.setBlockLimit(stoneKey, 10); - assertTrue(ibc.isBlockLimited(stoneKey)); + assertFalse(ibc.isBlockLimited(Environment.NORMAL, stoneKey)); + ibc.setBlockLimit(Environment.NORMAL, stoneKey, 10); + assertTrue(ibc.isBlockLimited(Environment.NORMAL, stoneKey)); } @Test public void testEntityLimits() { - assertEquals(-1, ibc.getEntityLimit(EntityType.ZOMBIE)); - ibc.setEntityLimit(EntityType.ZOMBIE, 25); - assertEquals(25, ibc.getEntityLimit(EntityType.ZOMBIE)); - ibc.clearEntityLimits(); - assertEquals(-1, ibc.getEntityLimit(EntityType.ZOMBIE)); + assertEquals(-1, ibc.getEntityLimit(Environment.NORMAL, EntityType.ZOMBIE)); + ibc.setEntityLimit(Environment.NORMAL, EntityType.ZOMBIE, 25); + assertEquals(25, ibc.getEntityLimit(Environment.NORMAL, EntityType.ZOMBIE)); + ibc.clearAllEntityLimits(); + assertEquals(-1, ibc.getEntityLimit(Environment.NORMAL, EntityType.ZOMBIE)); } @Test public void testEntityGroupLimits() { - assertEquals(-1, ibc.getEntityGroupLimit("monsters")); - ibc.setEntityGroupLimit("monsters", 30); - assertEquals(30, ibc.getEntityGroupLimit("monsters")); - ibc.clearEntityGroupLimits(); - assertEquals(-1, ibc.getEntityGroupLimit("monsters")); + assertEquals(-1, ibc.getEntityGroupLimit(Environment.NORMAL, "monsters")); + ibc.setEntityGroupLimit(Environment.NORMAL, "monsters", 30); + assertEquals(30, ibc.getEntityGroupLimit(Environment.NORMAL, "monsters")); + ibc.clearAllEntityGroupLimits(); + assertEquals(-1, ibc.getEntityGroupLimit(Environment.NORMAL, "monsters")); } @Test public void testBlockLimitsOffset() { - assertEquals(0, ibc.getBlockLimitOffset(stoneKey)); - ibc.setBlockLimitsOffset(stoneKey, 7); - assertEquals(7, ibc.getBlockLimitOffset(stoneKey)); + assertEquals(0, ibc.getBlockLimitOffset(Environment.NORMAL, stoneKey)); + ibc.setBlockLimitsOffset(Environment.NORMAL, stoneKey, 7); + assertEquals(7, ibc.getBlockLimitOffset(Environment.NORMAL, stoneKey)); } @Test public void testEntityLimitsOffset() { - assertEquals(0, ibc.getEntityLimitOffset(EntityType.ZOMBIE)); - ibc.setEntityLimitsOffset(EntityType.ZOMBIE, 3); - assertEquals(3, ibc.getEntityLimitOffset(EntityType.ZOMBIE)); + assertEquals(0, ibc.getEntityLimitOffset(Environment.NORMAL, EntityType.ZOMBIE)); + ibc.setEntityLimitsOffset(Environment.NORMAL, EntityType.ZOMBIE, 3); + assertEquals(3, ibc.getEntityLimitOffset(Environment.NORMAL, EntityType.ZOMBIE)); } @Test public void testEntityGroupLimitsOffset() { - assertEquals(0, ibc.getEntityGroupLimitOffset("monsters")); - ibc.setEntityGroupLimitsOffset("monsters", 4); - assertEquals(4, ibc.getEntityGroupLimitOffset("monsters")); + assertEquals(0, ibc.getEntityGroupLimitOffset(Environment.NORMAL, "monsters")); + ibc.setEntityGroupLimitsOffset(Environment.NORMAL, "monsters", 4); + assertEquals(4, ibc.getEntityGroupLimitOffset(Environment.NORMAL, "monsters")); } @Test @@ -224,6 +225,86 @@ public void testReadLegacyJsonWithBareMaterialNames() { assertEquals(8, loaded.getBlockCount(NamespacedKey.minecraft("oak_log"))); } + @Test + public void legacyJsonMigratesIntoOverworldEnv() { + // Legacy data with no envBlockCounts must end up scoped to Environment.NORMAL. + String legacy = """ + { + "uniqueId": "isle", + "gameMode": "BSkyBlock", + "blockCounts": { "minecraft:hopper": 7 }, + "blockLimits": { "minecraft:hopper": 20 }, + "blockLimitsOffset": { "minecraft:hopper": 3 }, + "entityLimits": { "CHICKEN": 12 }, + "entityLimitsOffset": { "CHICKEN": 2 }, + "entityGroupLimits": { "Monsters": 50 }, + "entityGroupLimitsOffset": { "Monsters": 10 } + } + """; + IslandBlockCount loaded = buildBentoboxGson().fromJson(legacy, IslandBlockCount.class); + NamespacedKey hopper = NamespacedKey.minecraft("hopper"); + + // Touching any getter triggers migration, after which only the NORMAL env should hold data. + assertEquals(7, loaded.getBlockCount(Environment.NORMAL, hopper)); + assertEquals(0, loaded.getBlockCount(Environment.NETHER, hopper)); + assertEquals(0, loaded.getBlockCount(Environment.THE_END, hopper)); + + assertEquals(20, loaded.getBlockLimit(Environment.NORMAL, hopper)); + assertEquals(-1, loaded.getBlockLimit(Environment.NETHER, hopper)); + + assertEquals(3, loaded.getBlockLimitOffset(Environment.NORMAL, hopper)); + assertEquals(0, loaded.getBlockLimitOffset(Environment.NETHER, hopper)); + + assertEquals(12, loaded.getEntityLimit(Environment.NORMAL, EntityType.CHICKEN)); + assertEquals(-1, loaded.getEntityLimit(Environment.NETHER, EntityType.CHICKEN)); + + assertEquals(2, loaded.getEntityLimitOffset(Environment.NORMAL, EntityType.CHICKEN)); + assertEquals(50, loaded.getEntityGroupLimit(Environment.NORMAL, "Monsters")); + assertEquals(10, loaded.getEntityGroupLimitOffset(Environment.NORMAL, "Monsters")); + } + + @Test + public void allEnvsSetterAppliesUniformly() { + ibc.setBlockLimitAllEnvs(stoneKey, 50); + assertEquals(50, ibc.getBlockLimit(Environment.NORMAL, stoneKey)); + assertEquals(50, ibc.getBlockLimit(Environment.NETHER, stoneKey)); + assertEquals(50, ibc.getBlockLimit(Environment.THE_END, stoneKey)); + + ibc.setEntityLimitAllEnvs(EntityType.CHICKEN, 30); + assertEquals(30, ibc.getEntityLimit(Environment.NORMAL, EntityType.CHICKEN)); + assertEquals(30, ibc.getEntityLimit(Environment.NETHER, EntityType.CHICKEN)); + assertEquals(30, ibc.getEntityLimit(Environment.THE_END, EntityType.CHICKEN)); + } + + @Test + public void countsAreEnvIndependent() { + ibc.add(Environment.NORMAL, stoneKey); + ibc.add(Environment.NORMAL, stoneKey); + ibc.add(Environment.NETHER, stoneKey); + + assertEquals(2, ibc.getBlockCount(Environment.NORMAL, stoneKey)); + assertEquals(1, ibc.getBlockCount(Environment.NETHER, stoneKey)); + assertEquals(0, ibc.getBlockCount(Environment.THE_END, stoneKey)); + // Sum across envs + assertEquals(3, ibc.getBlockCount(stoneKey)); + } + + @Test + public void entityCountIncrementDecrement() { + ibc.incrementEntity(Environment.NORMAL, EntityType.PIG); + ibc.incrementEntity(Environment.NORMAL, EntityType.PIG); + ibc.incrementEntity(Environment.NETHER, EntityType.PIG); + assertEquals(2, ibc.getEntityCount(Environment.NORMAL, EntityType.PIG)); + assertEquals(1, ibc.getEntityCount(Environment.NETHER, EntityType.PIG)); + assertEquals(3, ibc.getEntityCount(EntityType.PIG)); + + ibc.decrementEntity(Environment.NORMAL, EntityType.PIG); + assertEquals(1, ibc.getEntityCount(Environment.NORMAL, EntityType.PIG)); + // Decrementing past zero is a no-op. + ibc.decrementEntity(Environment.THE_END, EntityType.PIG); + assertEquals(0, ibc.getEntityCount(Environment.THE_END, EntityType.PIG)); + } + @Test public void testReadJsonWithNamespacedStringKeys() { // This is the format the new adapter writes; it must round-trip. @@ -244,17 +325,17 @@ public void testReadJsonWithNamespacedStringKeys() { @Test public void testWriteRoundTripWithNamespacedKeys() { - ibc.add(stoneKey); - ibc.add(stoneKey); - ibc.setBlockLimit(NamespacedKey.minecraft("hopper"), 20); - ibc.setBlockLimitsOffset(NamespacedKey.minecraft("hopper"), 5); + ibc.add(Environment.NORMAL, stoneKey); + ibc.add(Environment.NORMAL, stoneKey); + ibc.setBlockLimit(Environment.NORMAL, NamespacedKey.minecraft("hopper"), 20); + ibc.setBlockLimitsOffset(Environment.NORMAL, NamespacedKey.minecraft("hopper"), 5); Gson gson = buildBentoboxGson(); String json = gson.toJson(ibc); IslandBlockCount loaded = gson.fromJson(json, IslandBlockCount.class); assertEquals(2, loaded.getBlockCount(stoneKey)); - assertEquals(20, loaded.getBlockLimit(NamespacedKey.minecraft("hopper"))); - assertEquals(5, loaded.getBlockLimitOffset(NamespacedKey.minecraft("hopper"))); + assertEquals(20, loaded.getBlockLimit(Environment.NORMAL, NamespacedKey.minecraft("hopper"))); + assertEquals(5, loaded.getBlockLimitOffset(Environment.NORMAL, NamespacedKey.minecraft("hopper"))); } } From 5709b006a84fa612d22397981fb7ef6ba6cfa634 Mon Sep 17 00:00:00 2001 From: tastybento Date: Wed, 6 May 2026 08:55:24 -0700 Subject: [PATCH 02/24] Clean up test code per SonarCloud findings - Drop redundant eq() matchers in JoinListenerTest verify() calls (S6068) - Replace public modifier with package-private on JUnit 5 tests (S5786) - Use non-deprecated PlayerJoinEvent(Player, Component) constructor (S5738) - Rename `var` local in LimitTabTest to avoid restricted identifier (S6213) - Remove unused imports (eq, mock, Disabled) (S1128) - Use StringBuilder.isEmpty() over length() > 0 (S7158) - Drop "throws Exception" from BlockLimitsListenerTest methods that don't throw (S1130) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../bentobox/limits/JoinListenerTest.java | 55 ++++++++++--------- .../limits/calculators/ResultsTest.java | 2 +- .../limits/commands/player/LimitTabTest.java | 18 +++--- .../listeners/BlockLimitsListenerTest.java | 4 +- .../listeners/EntityLimitListenerTest.java | 1 - .../limits/objects/IslandBlockCountTest.java | 8 +-- 6 files changed, 43 insertions(+), 45 deletions(-) diff --git a/src/test/java/world/bentobox/limits/JoinListenerTest.java b/src/test/java/world/bentobox/limits/JoinListenerTest.java index 0182ff4..c1b5898 100644 --- a/src/test/java/world/bentobox/limits/JoinListenerTest.java +++ b/src/test/java/world/bentobox/limits/JoinListenerTest.java @@ -19,6 +19,7 @@ import java.util.Set; import java.util.UUID; +import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.NamespacedKey; @@ -352,7 +353,7 @@ public void testOnPlayerJoinWithPermLimitsSuccess() { PlayerJoinEvent e = new PlayerJoinEvent(player, "welcome"); jl.onPlayerJoin(e); verify(addon, never()).logError(anyString()); - verify(ibc).setBlockLimit(eq(Environment.NORMAL), eq(Material.STONE.getKey()), eq(24)); + verify(ibc).setBlockLimit(Environment.NORMAL, Material.STONE.getKey(), 24); } /** @@ -370,7 +371,7 @@ public void testOnPlayerJoinWithPermLimitsSuccessEntity() { PlayerJoinEvent e = new PlayerJoinEvent(player, "welcome"); jl.onPlayerJoin(e); verify(addon, never()).logError(anyString()); - verify(ibc).setEntityLimit(eq(Environment.NORMAL), eq(EntityType.BAT), eq(24)); + verify(ibc).setEntityLimit(Environment.NORMAL, EntityType.BAT, 24); } /** @@ -388,7 +389,7 @@ public void testOnPlayerJoinWithPermLimitsSuccessEntityGroup() { PlayerJoinEvent e = new PlayerJoinEvent(player, "welcome"); jl.onPlayerJoin(e); verify(addon, never()).logError(anyString()); - verify(ibc).setEntityGroupLimit(eq(Environment.NORMAL), eq("friendly"), eq(24)); + verify(ibc).setEntityGroupLimit(Environment.NORMAL, "friendly", 24); } /** @@ -427,11 +428,11 @@ public void testOnPlayerJoinWithPermLimitsMultiPerms() { PlayerJoinEvent e = new PlayerJoinEvent(player, "welcome"); jl.onPlayerJoin(e); verify(addon, never()).logError(anyString()); - verify(ibc).setBlockLimit(eq(Environment.NORMAL), eq(Material.STONE.getKey()), eq(24)); - verify(ibc).setBlockLimit(eq(Environment.NORMAL), eq(Material.SHORT_GRASS.getKey()), eq(14)); - verify(ibc).setBlockLimit(eq(Environment.NORMAL), eq(Material.DIRT.getKey()), eq(34)); - verify(ibc).setEntityLimit(eq(Environment.NORMAL), eq(EntityType.CHICKEN), eq(34)); - verify(ibc).setEntityLimit(eq(Environment.NORMAL), eq(EntityType.CAVE_SPIDER), eq(4)); + verify(ibc).setBlockLimit(Environment.NORMAL, Material.STONE.getKey(), 24); + verify(ibc).setBlockLimit(Environment.NORMAL, Material.SHORT_GRASS.getKey(), 14); + verify(ibc).setBlockLimit(Environment.NORMAL, Material.DIRT.getKey(), 34); + verify(ibc).setEntityLimit(Environment.NORMAL, EntityType.CHICKEN, 34); + verify(ibc).setEntityLimit(Environment.NORMAL, EntityType.CAVE_SPIDER, 4); } /** @@ -459,9 +460,9 @@ public void testOnPlayerJoinWithPermLimitsMultiPermsSameMaterial() { jl.onPlayerJoin(e); verify(addon, never()).logError(anyString()); // Only the limit over 25 should be set - verify(ibc, never()).setBlockLimit(eq(Environment.NORMAL), eq(Material.STONE.getKey()), eq(24)); - verify(ibc, never()).setBlockLimit(eq(Environment.NORMAL), eq(Material.STONE.getKey()), eq(14)); - verify(ibc).setBlockLimit(eq(Environment.NORMAL), eq(Material.STONE.getKey()), eq(34)); + verify(ibc, never()).setBlockLimit(Environment.NORMAL, Material.STONE.getKey(), 24); + verify(ibc, never()).setBlockLimit(Environment.NORMAL, Material.STONE.getKey(), 14); + verify(ibc).setBlockLimit(Environment.NORMAL, Material.STONE.getKey(), 34); } /** @@ -489,9 +490,9 @@ public void testOnPlayerJoinWithPermLimitsMultiPermsSameEntity() { jl.onPlayerJoin(e); verify(addon, never()).logError(anyString()); // Only the limit over 25 should be set - verify(ibc, never()).setEntityLimit(eq(Environment.NORMAL), eq(EntityType.BAT), eq(24)); - verify(ibc, never()).setEntityLimit(eq(Environment.NORMAL), eq(EntityType.BAT), eq(14)); - verify(ibc).setEntityLimit(eq(Environment.NORMAL), eq(EntityType.BAT), eq(34)); + verify(ibc, never()).setEntityLimit(Environment.NORMAL, EntityType.BAT, 24); + verify(ibc, never()).setEntityLimit(Environment.NORMAL, EntityType.BAT, 14); + verify(ibc).setEntityLimit(Environment.NORMAL, EntityType.BAT, 34); } /** @@ -557,32 +558,32 @@ public void testOnUnregisterIslandInWorldNullIBC() { * 5-segment permission applies the limit independently to every standard env. */ @Test - public void unprefixedPermAppliesToAllEnvs() { + void unprefixedPermAppliesToAllEnvs() { Set perms = new HashSet<>(); PermissionAttachmentInfo p = mock(PermissionAttachmentInfo.class); when(p.getPermission()).thenReturn("bskyblock.island.limit.STONE.24"); when(p.getValue()).thenReturn(true); perms.add(p); when(player.getEffectivePermissions()).thenReturn(perms); - jl.onPlayerJoin(new PlayerJoinEvent(player, "welcome")); - verify(ibc).setBlockLimit(eq(Environment.NORMAL), eq(Material.STONE.getKey()), eq(24)); - verify(ibc).setBlockLimit(eq(Environment.NETHER), eq(Material.STONE.getKey()), eq(24)); - verify(ibc).setBlockLimit(eq(Environment.THE_END), eq(Material.STONE.getKey()), eq(24)); + jl.onPlayerJoin(new PlayerJoinEvent(player, Component.text("welcome"))); + verify(ibc).setBlockLimit(Environment.NORMAL, Material.STONE.getKey(), 24); + verify(ibc).setBlockLimit(Environment.NETHER, Material.STONE.getKey(), 24); + verify(ibc).setBlockLimit(Environment.THE_END, Material.STONE.getKey(), 24); } /** * 6-segment permission with `nether` env applies only to nether. */ @Test - public void netherPrefixedPermAppliesToNetherOnly() { + void netherPrefixedPermAppliesToNetherOnly() { Set perms = new HashSet<>(); PermissionAttachmentInfo p = mock(PermissionAttachmentInfo.class); when(p.getPermission()).thenReturn("bskyblock.island.limit.nether.HOPPER.5"); when(p.getValue()).thenReturn(true); perms.add(p); when(player.getEffectivePermissions()).thenReturn(perms); - jl.onPlayerJoin(new PlayerJoinEvent(player, "welcome")); - verify(ibc).setBlockLimit(eq(Environment.NETHER), eq(Material.HOPPER.getKey()), eq(5)); + jl.onPlayerJoin(new PlayerJoinEvent(player, Component.text("welcome"))); + verify(ibc).setBlockLimit(Environment.NETHER, Material.HOPPER.getKey(), 5); verify(ibc, never()).setBlockLimit(eq(Environment.NORMAL), any(NamespacedKey.class), anyInt()); verify(ibc, never()).setBlockLimit(eq(Environment.THE_END), any(NamespacedKey.class), anyInt()); } @@ -591,15 +592,15 @@ public void netherPrefixedPermAppliesToNetherOnly() { * `end` and `overworld` aliases also work. */ @Test - public void endPrefixedPermAppliesToEndOnly() { + void endPrefixedPermAppliesToEndOnly() { Set perms = new HashSet<>(); PermissionAttachmentInfo p = mock(PermissionAttachmentInfo.class); when(p.getPermission()).thenReturn("bskyblock.island.limit.end.ENDERMAN.50"); when(p.getValue()).thenReturn(true); perms.add(p); when(player.getEffectivePermissions()).thenReturn(perms); - jl.onPlayerJoin(new PlayerJoinEvent(player, "welcome")); - verify(ibc).setEntityLimit(eq(Environment.THE_END), eq(EntityType.ENDERMAN), eq(50)); + jl.onPlayerJoin(new PlayerJoinEvent(player, Component.text("welcome"))); + verify(ibc).setEntityLimit(Environment.THE_END, EntityType.ENDERMAN, 50); verify(ibc, never()).setEntityLimit(eq(Environment.NORMAL), any(EntityType.class), anyInt()); verify(ibc, never()).setEntityLimit(eq(Environment.NETHER), any(EntityType.class), anyInt()); } @@ -608,14 +609,14 @@ public void endPrefixedPermAppliesToEndOnly() { * 6-segment permission with an unknown env token is rejected with a logged error. */ @Test - public void unknownEnvPrefixIsRejected() { + void unknownEnvPrefixIsRejected() { Set perms = new HashSet<>(); PermissionAttachmentInfo p = mock(PermissionAttachmentInfo.class); when(p.getPermission()).thenReturn("bskyblock.island.limit.aether.HOPPER.5"); when(p.getValue()).thenReturn(true); perms.add(p); when(player.getEffectivePermissions()).thenReturn(perms); - jl.onPlayerJoin(new PlayerJoinEvent(player, "welcome")); + jl.onPlayerJoin(new PlayerJoinEvent(player, Component.text("welcome"))); verify(addon).logError(contains("'aether' is not a recognised environment")); verify(ibc, never()).setBlockLimit(any(Environment.class), any(NamespacedKey.class), anyInt()); } diff --git a/src/test/java/world/bentobox/limits/calculators/ResultsTest.java b/src/test/java/world/bentobox/limits/calculators/ResultsTest.java index a604168..6d32941 100644 --- a/src/test/java/world/bentobox/limits/calculators/ResultsTest.java +++ b/src/test/java/world/bentobox/limits/calculators/ResultsTest.java @@ -24,7 +24,7 @@ public void testConstructorWithState() { } @Test - public void testGetBlockCountReturnsEmptyMultiset() { + void testGetBlockCountReturnsEmptyMultiset() { Results results = new Results(); assertNotNull(results.getBlockCount(Environment.NORMAL)); assertTrue(results.getBlockCount(Environment.NORMAL).isEmpty()); diff --git a/src/test/java/world/bentobox/limits/commands/player/LimitTabTest.java b/src/test/java/world/bentobox/limits/commands/player/LimitTabTest.java index 5953f5d..547fa06 100644 --- a/src/test/java/world/bentobox/limits/commands/player/LimitTabTest.java +++ b/src/test/java/world/bentobox/limits/commands/player/LimitTabTest.java @@ -5,9 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.lenient; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.Collections; @@ -111,11 +109,11 @@ private static String renderTranslation(String template, Object[] vars) { StringBuilder out = new StringBuilder(template); if (vars != null) { for (int i = 0; i + 1 < vars.length; i += 2) { - String var = String.valueOf(vars[i]); + String placeholder = String.valueOf(vars[i]); String val = String.valueOf(vars[i + 1]); int idx; - while ((idx = out.indexOf(var)) >= 0) { - out.replace(idx, idx + var.length(), val); + while ((idx = out.indexOf(placeholder)) >= 0) { + out.replace(idx, idx + placeholder.length(), val); } } } @@ -123,7 +121,7 @@ private static String renderTranslation(String template, Object[] vars) { } @Test - public void blockCountReadsFromEnvKeyedIbc() { + void blockCountReadsFromEnvKeyedIbc() { IslandBlockCount ibc = new IslandBlockCount("island", "BSkyBlock"); NamespacedKey hopper = Material.HOPPER.getKey(); // Place 3 hoppers in the overworld and 7 in the nether. @@ -140,7 +138,7 @@ public void blockCountReadsFromEnvKeyedIbc() { } @Test - public void entityCountReadsFromEnvKeyedIbc() { + void entityCountReadsFromEnvKeyedIbc() { IslandBlockCount ibc = new IslandBlockCount("island", "BSkyBlock"); for (int i = 0; i < 4; i++) ibc.incrementEntity(Environment.NORMAL, EntityType.CHICKEN); for (int i = 0; i < 9; i++) ibc.incrementEntity(Environment.NETHER, EntityType.CHICKEN); @@ -155,7 +153,7 @@ public void entityCountReadsFromEnvKeyedIbc() { } @Test - public void tabIconAndTitleReflectEnvironment() { + void tabIconAndTitleReflectEnvironment() { IslandBlockCount ibc = new IslandBlockCount("island", "BSkyBlock"); LimitTab netherTab = new LimitTab(addon, ibc, Collections.emptyMap(), island, world, user, Environment.NETHER); assertNotNull(netherTab.getIcon()); @@ -181,9 +179,9 @@ private static String findCount(LimitTab tab, String keyFragment) { for (int i = 0; i < desc.length(); i++) { char c = desc.charAt(i); if (Character.isDigit(c)) digits.append(c); - else if (digits.length() > 0) break; + else if (!digits.isEmpty()) break; } - return digits.length() > 0 ? digits.toString() : null; + return !digits.isEmpty() ? digits.toString() : null; } } return null; diff --git a/src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java b/src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java index bf0971c..7549d65 100644 --- a/src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java +++ b/src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java @@ -1067,7 +1067,7 @@ public void testWorldLimitTakesPrecedenceOverDefaultLimit() throws Exception { } @Test - public void testDefaultLimitAppliedWhenNoIslandOrWorldLimit() throws Exception { + public void testDefaultLimitAppliedWhenNoIslandOrWorldLimit() { // Set default limit for COBBLESTONE = 2 listener.getEnvDefaultLimitMap().get(Environment.NORMAL).put(Material.COBBLESTONE.getKey(), 2); @@ -1087,7 +1087,7 @@ public void testDefaultLimitAppliedWhenNoIslandOrWorldLimit() throws Exception { } @Test - public void testIslandOffsetIncreasesEffectiveLimit() throws Exception { + public void testIslandOffsetIncreasesEffectiveLimit() { // Set default limit for COBBLESTONE = 2 listener.getEnvDefaultLimitMap().get(Environment.NORMAL).put(Material.COBBLESTONE.getKey(), 2); diff --git a/src/test/java/world/bentobox/limits/listeners/EntityLimitListenerTest.java b/src/test/java/world/bentobox/limits/listeners/EntityLimitListenerTest.java index 814d305..9182be9 100644 --- a/src/test/java/world/bentobox/limits/listeners/EntityLimitListenerTest.java +++ b/src/test/java/world/bentobox/limits/listeners/EntityLimitListenerTest.java @@ -42,7 +42,6 @@ import org.bukkit.Material; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; diff --git a/src/test/java/world/bentobox/limits/objects/IslandBlockCountTest.java b/src/test/java/world/bentobox/limits/objects/IslandBlockCountTest.java index 81f2737..2930e19 100644 --- a/src/test/java/world/bentobox/limits/objects/IslandBlockCountTest.java +++ b/src/test/java/world/bentobox/limits/objects/IslandBlockCountTest.java @@ -226,7 +226,7 @@ public void testReadLegacyJsonWithBareMaterialNames() { } @Test - public void legacyJsonMigratesIntoOverworldEnv() { + void legacyJsonMigratesIntoOverworldEnv() { // Legacy data with no envBlockCounts must end up scoped to Environment.NORMAL. String legacy = """ { @@ -264,7 +264,7 @@ public void legacyJsonMigratesIntoOverworldEnv() { } @Test - public void allEnvsSetterAppliesUniformly() { + void allEnvsSetterAppliesUniformly() { ibc.setBlockLimitAllEnvs(stoneKey, 50); assertEquals(50, ibc.getBlockLimit(Environment.NORMAL, stoneKey)); assertEquals(50, ibc.getBlockLimit(Environment.NETHER, stoneKey)); @@ -277,7 +277,7 @@ public void allEnvsSetterAppliesUniformly() { } @Test - public void countsAreEnvIndependent() { + void countsAreEnvIndependent() { ibc.add(Environment.NORMAL, stoneKey); ibc.add(Environment.NORMAL, stoneKey); ibc.add(Environment.NETHER, stoneKey); @@ -290,7 +290,7 @@ public void countsAreEnvIndependent() { } @Test - public void entityCountIncrementDecrement() { + void entityCountIncrementDecrement() { ibc.incrementEntity(Environment.NORMAL, EntityType.PIG); ibc.incrementEntity(Environment.NORMAL, EntityType.PIG); ibc.incrementEntity(Environment.NETHER, EntityType.PIG); From 842a3c4bda27d34a1a6cadf170c1690581f38e7e Mon Sep 17 00:00:00 2001 From: tastybento Date: Wed, 6 May 2026 08:59:13 -0700 Subject: [PATCH 03/24] Remove unused field and redundant cast (Sonar S1068, S1905) - Drop the unused islandWorldManager field from Limits - Remove unnecessary intermediate raw Map cast in IslandBlockCount.newInner Co-Authored-By: Claude Opus 4.7 (1M context) --- src/main/java/world/bentobox/limits/Limits.java | 3 --- .../java/world/bentobox/limits/objects/IslandBlockCount.java | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/world/bentobox/limits/Limits.java b/src/main/java/world/bentobox/limits/Limits.java index a56052e..988c74c 100644 --- a/src/main/java/world/bentobox/limits/Limits.java +++ b/src/main/java/world/bentobox/limits/Limits.java @@ -19,7 +19,6 @@ import world.bentobox.bentobox.api.addons.GameModeAddon; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.objects.Island; -import world.bentobox.bentobox.managers.IslandWorldManager; import world.bentobox.limits.commands.admin.AdminCommand; import world.bentobox.limits.commands.player.PlayerCommand; import world.bentobox.limits.listeners.BlockLimitsListener; @@ -41,7 +40,6 @@ public class Limits extends Addon { private List gameModes = new ArrayList<>(); private BlockLimitsListener blockLimitListener; private JoinListener joinListener; - private IslandWorldManager islandWorldManager; @Override public void onDisable() { @@ -53,7 +51,6 @@ public void onDisable() { @Override public void onEnable() { saveDefaultConfig(); - this.islandWorldManager = getPlugin().getIWM(); settings = new Settings(this); gameModes = getPlugin().getAddonsManager().getGameModeAddons().stream() .filter(gm -> settings.getGameModes().contains(gm.getDescription().getName())) diff --git a/src/main/java/world/bentobox/limits/objects/IslandBlockCount.java b/src/main/java/world/bentobox/limits/objects/IslandBlockCount.java index 4dd103b..b1df9cb 100644 --- a/src/main/java/world/bentobox/limits/objects/IslandBlockCount.java +++ b/src/main/java/world/bentobox/limits/objects/IslandBlockCount.java @@ -142,7 +142,7 @@ private static void moveLegacy(Map legacy, Map Map newInner(Map sample) { Object first = sample.keySet().iterator().next(); if (first instanceof EntityType) { - return (Map) (Map) new EnumMap(EntityType.class); + return (Map) new EnumMap(EntityType.class); } return new HashMap<>(); } From fe70bacfd248773444cc7b307992bf57fd9404e7 Mon Sep 17 00:00:00 2001 From: tastybento Date: Wed, 6 May 2026 08:59:19 -0700 Subject: [PATCH 04/24] Refactor Settings entity-limit loading (Sonar S3776, S1192, S135) - Extract entity-key handling from loadEntityLimits into applyEntityKey to cut cognitive complexity from 17 to under the threshold - Extract group-override handling into helpers so loadGroupLimitOverrides has a single break/continue path - Replace duplicated " - skipping..." and ".limit" string literals with SKIPPING and LIMIT_SUFFIX constants Co-Authored-By: Claude Opus 4.7 (1M context) --- .../java/world/bentobox/limits/Settings.java | 77 +++++++++++-------- 1 file changed, 47 insertions(+), 30 deletions(-) diff --git a/src/main/java/world/bentobox/limits/Settings.java b/src/main/java/world/bentobox/limits/Settings.java index 05b7b22..7c6cb79 100644 --- a/src/main/java/world/bentobox/limits/Settings.java +++ b/src/main/java/world/bentobox/limits/Settings.java @@ -36,6 +36,9 @@ enum GeneralGroup { Environment.NETHER, "-nether", Environment.THE_END, "-end"); + private static final String SKIPPING = " - skipping..."; + private static final String LIMIT_SUFFIX = ".limit"; + private final Map general = new EnumMap<>(GeneralGroup.class); /** Per-env entity type limits (env defaults from config). */ private final Map> envLimits = new EnumMap<>(Environment.class); @@ -126,24 +129,31 @@ private void loadEntityLimits(Limits addon, String section, List ta general.put(GeneralGroup.MOBS, el.getInt(key, 0)); } } else { - EntityType type = getType(key); - if (type == null) { - addon.logError("Unknown entity type in " + section + ": " + key + " - skipping..."); - } else if (DISALLOWED.contains(type)) { - addon.logError("Entity type in " + section + " not supported: " + key + " - skipping..."); - } else { - int value = el.getInt(key, 0); - targetEnvs.forEach(env -> envLimits.get(env).put(type, value)); - } + applyEntityKey(addon, section, el, key, targetEnvs); } } } + private void applyEntityKey(Limits addon, String section, ConfigurationSection el, String key, + List targetEnvs) { + EntityType type = getType(key); + if (type == null) { + addon.logError("Unknown entity type in " + section + ": " + key + SKIPPING); + return; + } + if (DISALLOWED.contains(type)) { + addon.logError("Entity type in " + section + " not supported: " + key + SKIPPING); + return; + } + int value = el.getInt(key, 0); + targetEnvs.forEach(env -> envLimits.get(env).put(type, value)); + } + private void loadGroupDefinitions(Limits addon) { ConfigurationSection el = addon.getConfig().getConfigurationSection("entitygrouplimits"); if (el == null) return; for (String name : el.getKeys(false)) { - int limit = el.getInt(name + ".limit"); + int limit = el.getInt(name + LIMIT_SUFFIX); String iconName = el.getString(name + ".icon", "BARRIER"); Material icon; try { @@ -155,11 +165,11 @@ private void loadGroupDefinitions(Limits addon) { Set entities = el.getStringList(name + ".entities").stream().map(s -> { EntityType type = getType(s); if (type == null) { - addon.logError("Unknown entity type: " + s + " - skipping..."); + addon.logError("Unknown entity type: " + s + SKIPPING); return null; } if (DISALLOWED.contains(type)) { - addon.logError("Entity type: " + s + " is not supported - skipping..."); + addon.logError("Entity type: " + s + " is not supported" + SKIPPING); return null; } return type; @@ -176,25 +186,32 @@ private void loadGroupLimitOverrides(Limits addon, String section, Environment e ConfigurationSection el = addon.getConfig().getConfigurationSection(section); if (el == null) return; for (String name : el.getKeys(false)) { - // Accept either a flat int (Monsters: 100) or a nested .limit (Monsters.limit: 100) - int limit; - if (el.isInt(name)) { - limit = el.getInt(name); - } else if (el.isInt(name + ".limit")) { - limit = el.getInt(name + ".limit"); - } else { - addon.logError("Group override " + section + "." + name + " missing limit - skipping."); - continue; - } - // Group must already be defined in the base entitygrouplimits section - boolean exists = getGroupLimitDefinitions().stream().anyMatch(g -> g.getName().equals(name)); - if (!exists) { - addon.logError("Group override " + section + "." + name - + " refers to an undefined group - define it under entitygrouplimits first."); - continue; - } - envGroupLimits.get(env).put(name, limit); + applyGroupOverride(addon, el, section, name, env); + } + } + + private void applyGroupOverride(Limits addon, ConfigurationSection el, String section, String name, + Environment env) { + // Accept either a flat int (Monsters: 100) or a nested .limit (Monsters.limit: 100) + Integer limit = resolveGroupLimit(el, name); + if (limit == null) { + addon.logError("Group override " + section + "." + name + " missing limit - skipping."); + return; + } + // Group must already be defined in the base entitygrouplimits section + boolean exists = getGroupLimitDefinitions().stream().anyMatch(g -> g.getName().equals(name)); + if (!exists) { + addon.logError("Group override " + section + "." + name + + " refers to an undefined group - define it under entitygrouplimits first."); + return; } + envGroupLimits.get(env).put(name, limit); + } + + private static Integer resolveGroupLimit(ConfigurationSection el, String name) { + if (el.isInt(name)) return el.getInt(name); + if (el.isInt(name + LIMIT_SUFFIX)) return el.getInt(name + LIMIT_SUFFIX); + return null; } private EntityType getType(String key) { From ab9727bb95cd2531df3b990c1d8b2f687b07f451 Mon Sep 17 00:00:00 2001 From: tastybento Date: Wed, 6 May 2026 08:59:27 -0700 Subject: [PATCH 05/24] Tidy limit-panel command classes (Sonar S107, S1172, S1192, S1640) - Drop the unused GameModeAddon parameter from LimitPanel.addEnvTab (was 8 params, now 7) and the unused Island parameter from LimitTab.addEntityLimits/addEntityGroupLimits - Hoist the duplicated translation keys (max-color, regular-color, block-limit-syntax) and "[limit]" placeholder into constants - Use EnumMap for the EntityType limit map in addEntityLimits Co-Authored-By: Claude Opus 4.7 (1M context) --- .../limits/commands/player/LimitPanel.java | 8 ++-- .../limits/commands/player/LimitTab.java | 41 +++++++++++-------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/main/java/world/bentobox/limits/commands/player/LimitPanel.java b/src/main/java/world/bentobox/limits/commands/player/LimitPanel.java index f00ca70..4ce12c9 100644 --- a/src/main/java/world/bentobox/limits/commands/player/LimitPanel.java +++ b/src/main/java/world/bentobox/limits/commands/player/LimitPanel.java @@ -53,14 +53,14 @@ public void showLimits(GameModeAddon gm, User user, UUID target) { // Always show overworld; show nether/end only if the gamemode generates and provides them. int slot = 0; - slot = addEnvTab(tpb, gm, overWorld, ibc, island, user, Environment.NORMAL, slot); + slot = addEnvTab(tpb, overWorld, ibc, island, user, Environment.NORMAL, slot); World netherWorld = gm.getNetherWorld(); if (netherWorld != null && addon.getPlugin().getIWM().isNetherIslands(overWorld)) { - slot = addEnvTab(tpb, gm, netherWorld, ibc, island, user, Environment.NETHER, slot); + slot = addEnvTab(tpb, netherWorld, ibc, island, user, Environment.NETHER, slot); } World endWorld = gm.getEndWorld(); if (endWorld != null && addon.getPlugin().getIWM().isEndIslands(overWorld)) { - slot = addEnvTab(tpb, gm, endWorld, ibc, island, user, Environment.THE_END, slot); + slot = addEnvTab(tpb, endWorld, ibc, island, user, Environment.THE_END, slot); } Map overworldLimits = addon.getBlockLimitListener().getMaterialLimits(overWorld, @@ -73,7 +73,7 @@ public void showLimits(GameModeAddon gm, User user, UUID target) { tpb.build().openPanel(); } - private int addEnvTab(TabbedPanelBuilder tpb, GameModeAddon gm, World envWorld, IslandBlockCount ibc, Island island, + private int addEnvTab(TabbedPanelBuilder tpb, World envWorld, IslandBlockCount ibc, Island island, User user, Environment env, int slot) { Map matLimits = addon.getBlockLimitListener().getMaterialLimits(envWorld, island.getUniqueId()); diff --git a/src/main/java/world/bentobox/limits/commands/player/LimitTab.java b/src/main/java/world/bentobox/limits/commands/player/LimitTab.java index 31bc151..d420fb6 100644 --- a/src/main/java/world/bentobox/limits/commands/player/LimitTab.java +++ b/src/main/java/world/bentobox/limits/commands/player/LimitTab.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.Comparator; +import java.util.EnumMap; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -37,6 +38,11 @@ */ public class LimitTab implements Tab { + private static final String MAX_COLOR_KEY = "island.limits.max-color"; + private static final String REGULAR_COLOR_KEY = "island.limits.regular-color"; + private static final String BLOCK_LIMIT_SYNTAX_KEY = "island.limits.block-limit-syntax"; + private static final String LIMIT_PLACEHOLDER = "[limit]"; + /** This maps the entity types to the icon that should be shown in the panel */ private static final Map E2M = ImmutableMap.builder() .put(EntityType.MOOSHROOM, Material.MOOSHROOM_SPAWN_EGG).put(EntityType.SNOW_GOLEM, Material.SNOW_BLOCK) @@ -81,12 +87,12 @@ public LimitTab(Limits addon, IslandBlockCount ibc, Map this.env = env; result = new ArrayList<>(); addMaterialIcons(ibc, matLimits); - addEntityLimits(ibc, island); - addEntityGroupLimits(ibc, island); + addEntityLimits(ibc); + addEntityGroupLimits(ibc); result.sort(Comparator.comparing(PanelItem::getName)); } - private void addEntityGroupLimits(IslandBlockCount ibc, Island island) { + private void addEntityGroupLimits(IslandBlockCount ibc) { Map envGroupLimits = new HashMap<>(addon.getSettings().getGroupLimits(env)); Map groupMap = addon.getSettings().getGroupLimitDefinitions().stream() .filter(g -> envGroupLimits.containsKey(g.getName())) @@ -110,11 +116,11 @@ private void addEntityGroupLimits(IslandBlockCount ibc, Island island) { String description = "(" + prettyNames(g) + ")\n"; pib.icon(g.getIcon()); int count = ibc == null ? 0 : sumGroupCount(ibc, g); - String color = count >= limit ? user.getTranslation("island.limits.max-color") - : user.getTranslation("island.limits.regular-color"); - description += color + user.getTranslation("island.limits.block-limit-syntax", + String color = count >= limit ? user.getTranslation(MAX_COLOR_KEY) + : user.getTranslation(REGULAR_COLOR_KEY); + description += color + user.getTranslation(BLOCK_LIMIT_SYNTAX_KEY, TextVariables.NUMBER, String.valueOf(count), - "[limit]", String.valueOf(limit)); + LIMIT_PLACEHOLDER, String.valueOf(limit)); pib.description(description); result.add(pib.build()); }); @@ -129,8 +135,9 @@ private int sumGroupCount(IslandBlockCount ibc, EntityGroup group) { return total; } - private void addEntityLimits(IslandBlockCount ibc, Island island) { - Map map = new HashMap<>(addon.getSettings().getLimits(env)); + private void addEntityLimits(IslandBlockCount ibc) { + Map map = new EnumMap<>(EntityType.class); + map.putAll(addon.getSettings().getLimits(env)); if (ibc != null) { map.putAll(ibc.getEntityLimits(env)); ibc.getEntityLimitsOffset(env).forEach((k, v) -> map.put(k, map.getOrDefault(k, 0) + v)); @@ -153,11 +160,11 @@ private void addEntityLimits(IslandBlockCount ibc, Island island) { } pib.icon(m); int count = ibc == null ? 0 : ibc.getEntityCount(env, k); - String color = count >= v ? user.getTranslation("island.limits.max-color") - : user.getTranslation("island.limits.regular-color"); - pib.description(color + user.getTranslation("island.limits.block-limit-syntax", + String color = count >= v ? user.getTranslation(MAX_COLOR_KEY) + : user.getTranslation(REGULAR_COLOR_KEY); + pib.description(color + user.getTranslation(BLOCK_LIMIT_SYNTAX_KEY, TextVariables.NUMBER, String.valueOf(count), - "[limit]", String.valueOf(v))); + LIMIT_PLACEHOLDER, String.valueOf(v))); result.add(pib.build()); }); } @@ -172,11 +179,11 @@ private void addMaterialIcons(IslandBlockCount ibc, Map int count = ibc == null ? 0 : ibc.getBlockCount(env, en.getKey()); int value = en.getValue(); - String color = count >= value ? user.getTranslation("island.limits.max-color") - : user.getTranslation("island.limits.regular-color"); - pib.description(color + user.getTranslation("island.limits.block-limit-syntax", + String color = count >= value ? user.getTranslation(MAX_COLOR_KEY) + : user.getTranslation(REGULAR_COLOR_KEY); + pib.description(color + user.getTranslation(BLOCK_LIMIT_SYNTAX_KEY, TextVariables.NUMBER, String.valueOf(count), - "[limit]", String.valueOf(value))); + LIMIT_PLACEHOLDER, String.valueOf(value))); result.add(pib.build()); } } From bda31d0634f8641b321d5879551916556b8b1bd5 Mon Sep 17 00:00:00 2001 From: tastybento Date: Wed, 6 May 2026 09:01:34 -0700 Subject: [PATCH 06/24] Reduce cognitive complexity in scan/limit hot paths (Sonar S3776) - Split RecountCalculator.scanAsync's nested chunk/column logic into a scanColumn helper and lift loop bounds out of the per-block check - Break EntityLimitListener.atLimit into resolveTypeLimit, resolveGroupLimits, checkTypeLimit, and checkGroupLimits so each method has a single responsibility - Extract JoinListener.checkPerms's per-permission body into applyOnePerm so the loop becomes a thin filter-and-apply pass Co-Authored-By: Claude Opus 4.7 (1M context) --- .../limits/calculators/RecountCalculator.java | 44 +++++++------ .../limits/listeners/EntityLimitListener.java | 55 ++++++++++------ .../limits/listeners/JoinListener.java | 63 ++++++++++--------- 3 files changed, 92 insertions(+), 70 deletions(-) diff --git a/src/main/java/world/bentobox/limits/calculators/RecountCalculator.java b/src/main/java/world/bentobox/limits/calculators/RecountCalculator.java index 6ec1551..117ac07 100644 --- a/src/main/java/world/bentobox/limits/calculators/RecountCalculator.java +++ b/src/main/java/world/bentobox/limits/calculators/RecountCalculator.java @@ -143,29 +143,33 @@ private void loadChunks(CompletableFuture> r2, World world, Queue= island.getMinProtectedX() - + island.getProtectionRange() * 2) { - continue; - } + int absX = chunkBaseX + x; + if (absX < minX || absX >= maxX) continue; for (int z = 0; z < 16; z++) { - if (chunkSnapshot.getZ() * 16 + z < island.getMinProtectedZ() - || chunkSnapshot.getZ() * 16 + z >= island.getMinProtectedZ() - + island.getProtectionRange() * 2) { - continue; - } - for (int y = chunk.getWorld().getMinHeight(); y < chunk.getWorld().getMaxHeight(); y++) { - BlockData blockData = chunkSnapshot.getBlockData(x, y, z); - if (Tag.SLABS.isTagged(blockData.getMaterial())) { - Slab slab = (Slab) blockData; - if (slab.getType().equals(Slab.Type.DOUBLE)) { - checkBlock(env, blockData); - } - } - checkBlock(env, blockData); - } + int absZ = chunkBaseZ + z; + if (absZ < minZ || absZ >= maxZ) continue; + scanColumn(env, chunkSnapshot, x, z, minY, maxY); + } + } + } + + private void scanColumn(Environment env, ChunkSnapshot chunkSnapshot, int x, int z, int minY, int maxY) { + for (int y = minY; y < maxY; y++) { + BlockData blockData = chunkSnapshot.getBlockData(x, y, z); + if (Tag.SLABS.isTagged(blockData.getMaterial()) + && ((Slab) blockData).getType().equals(Slab.Type.DOUBLE)) { + checkBlock(env, blockData); } + checkBlock(env, blockData); } } diff --git a/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java b/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java index d8452f9..62919e1 100644 --- a/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java +++ b/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java @@ -442,44 +442,59 @@ AtLimitResult atLimit(Island island, Entity entity) { @Nullable IslandBlockCount ibc = addon.getBlockLimitListener().getIsland(island.getUniqueId()); - // 1. Resolve effective per-type limit - int limitAmount = -1; - if (ibc != null) { - limitAmount = ibc.getEntityLimit(env, type); + int limitAmount = resolveTypeLimit(ibc, env, type); + Map groupsLimits = resolveGroupLimits(ibc, env, type); + + if (limitAmount < 0 && groupsLimits.isEmpty()) { + return new AtLimitResult(); } - if (limitAmount < 0) { + + AtLimitResult typeResult = checkTypeLimit(ibc, env, type, limitAmount); + if (typeResult.hit()) return typeResult; + + return checkGroupLimits(ibc, env, groupsLimits); + } + + private int resolveTypeLimit(@Nullable IslandBlockCount ibc, Environment env, EntityType type) { + int limit = ibc == null ? -1 : ibc.getEntityLimit(env, type); + if (limit < 0) { Integer cfg = addon.getSettings().getLimits(env).get(type); - if (cfg != null) limitAmount = cfg; + if (cfg != null) limit = cfg; } + return limit; + } - // 2. Build effective group-limits for this env, applying island and config overrides + private Map resolveGroupLimits(@Nullable IslandBlockCount ibc, Environment env, + EntityType type) { Map groupsLimits = new HashMap<>(); List groupdefs = addon.getSettings().getGroupLimits().getOrDefault(type, Collections.emptyList()); Map envGroupCfg = addon.getSettings().getGroupLimits(env); - Map islandGroupOverrides = ibc == null ? Collections.emptyMap() : ibc.getEntityGroupLimits(env); + Map islandGroupOverrides = ibc == null ? Collections.emptyMap() + : ibc.getEntityGroupLimits(env); for (EntityGroup def : groupdefs) { int limit = islandGroupOverrides.getOrDefault(def.getName(), envGroupCfg.getOrDefault(def.getName(), -1)); if (limit >= 0) { groupsLimits.put(def, limit); } } + return groupsLimits; + } - if (limitAmount < 0 && groupsLimits.isEmpty()) { - return new AtLimitResult(); - } + private AtLimitResult checkTypeLimit(@Nullable IslandBlockCount ibc, Environment env, EntityType type, + int limitAmount) { + if (limitAmount < 0) return new AtLimitResult(); + int count = ibc == null ? 0 : ibc.getEntityCount(env, type); + int max = limitAmount + (ibc == null ? 0 : ibc.getEntityLimitOffset(env, type)); + return count >= max ? new AtLimitResult(type, max) : new AtLimitResult(); + } - // 3. Read counts from the IBC; use 0 if no IBC has been created yet - if (limitAmount >= 0) { - int count = ibc == null ? 0 : ibc.getEntityCount(env, type); - int max = limitAmount + (ibc == null ? 0 : ibc.getEntityLimitOffset(env, type)); - if (count >= max) { - return new AtLimitResult(type, max); - } - } + private AtLimitResult checkGroupLimits(@Nullable IslandBlockCount ibc, Environment env, + Map groupsLimits) { for (Map.Entry group : groupsLimits.entrySet()) { if (group.getValue() < 0) continue; int count = sumGroupCount(ibc, env, group.getKey()); - int max = group.getValue() + (ibc == null ? 0 : ibc.getEntityGroupLimitOffset(env, group.getKey().getName())); + int max = group.getValue() + + (ibc == null ? 0 : ibc.getEntityGroupLimitOffset(env, group.getKey().getName())); if (count >= max) { return new AtLimitResult(group.getKey(), max); } diff --git a/src/main/java/world/bentobox/limits/listeners/JoinListener.java b/src/main/java/world/bentobox/limits/listeners/JoinListener.java index 948e837..7079613 100644 --- a/src/main/java/world/bentobox/limits/listeners/JoinListener.java +++ b/src/main/java/world/bentobox/limits/listeners/JoinListener.java @@ -68,41 +68,44 @@ public void checkPerms(Player player, String permissionPrefix, String islandId, if (!permissionInfo.getValue() || !permissionInfo.getPermission().startsWith(permissionPrefix)) { continue; } - ParsedPerm parsed = parsePerm(permissionInfo, player.getName(), permissionPrefix); - if (parsed == null) continue; - - // Try to match the key part to an EntityType, Material, or EntityGroup - EntityType entityType = matchEntityType(parsed.key); - Material material = matchMaterial(parsed.key); - EntityGroup entityGroup = matchEntityGroup(parsed.key); + islandBlockCount = applyOnePerm(player, permissionInfo, permissionPrefix, islandId, gameMode, + islandBlockCount); + } + if (islandBlockCount != null) { + addon.getBlockLimitListener().setIsland(islandId, islandBlockCount); + } + } - if (entityGroup == null && entityType == null && material == null) { - logError(player.getName(), permissionInfo.getPermission(), - parsed.key.toUpperCase(Locale.ENGLISH) + " is not a valid material or entity type/group."); - continue; - } + private IslandBlockCount applyOnePerm(Player player, PermissionAttachmentInfo permissionInfo, + String permissionPrefix, String islandId, String gameMode, IslandBlockCount current) { + ParsedPerm parsed = parsePerm(permissionInfo, player.getName(), permissionPrefix); + if (parsed == null) return current; - if (islandBlockCount == null) { - islandBlockCount = new IslandBlockCount(islandId, gameMode); - } - logIfEnabled("Setting login limit via perm for " + player.getName() + "..."); + // Try to match the key part to an EntityType, Material, or EntityGroup + EntityType entityType = matchEntityType(parsed.key); + Material material = matchMaterial(parsed.key); + EntityGroup entityGroup = matchEntityGroup(parsed.key); - LimitsPermCheckEvent limitsPermCheckEvent = new LimitsPermCheckEvent(player, islandId, islandBlockCount, - entityGroup, entityType, material, parsed.value); - Bukkit.getPluginManager().callEvent(limitsPermCheckEvent); - if (limitsPermCheckEvent.isCancelled()) { - addon.log("Permissions not set because another addon/plugin canceled setting."); - continue; - } - islandBlockCount = limitsPermCheckEvent.getIbc(); - if (islandBlockCount == null) { - islandBlockCount = new IslandBlockCount(islandId, gameMode); - } - applyLimit(islandBlockCount, parsed.envs, limitsPermCheckEvent); + if (entityGroup == null && entityType == null && material == null) { + logError(player.getName(), permissionInfo.getPermission(), + parsed.key.toUpperCase(Locale.ENGLISH) + " is not a valid material or entity type/group."); + return current; } - if (islandBlockCount != null) { - addon.getBlockLimitListener().setIsland(islandId, islandBlockCount); + + IslandBlockCount ibc = current != null ? current : new IslandBlockCount(islandId, gameMode); + logIfEnabled("Setting login limit via perm for " + player.getName() + "..."); + + LimitsPermCheckEvent limitsPermCheckEvent = new LimitsPermCheckEvent(player, islandId, ibc, entityGroup, + entityType, material, parsed.value); + Bukkit.getPluginManager().callEvent(limitsPermCheckEvent); + if (limitsPermCheckEvent.isCancelled()) { + addon.log("Permissions not set because another addon/plugin canceled setting."); + return ibc; } + IslandBlockCount finalIbc = limitsPermCheckEvent.getIbc() != null ? limitsPermCheckEvent.getIbc() + : new IslandBlockCount(islandId, gameMode); + applyLimit(finalIbc, parsed.envs, limitsPermCheckEvent); + return finalIbc; } /** From 0fbcc4f215a3e0bace6f412291622bd8e63d503f Mon Sep 17 00:00:00 2001 From: tastybento Date: Wed, 6 May 2026 12:45:02 -0700 Subject: [PATCH 07/24] Drop unused Island parameter from LimitTab constructor (Sonar S1172) Now that addEntityLimits/addEntityGroupLimits no longer use the island, the constructor's island parameter is dead too. Remove it and update the LimitPanel and LimitTabTest call sites. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../bentobox/limits/commands/player/LimitPanel.java | 2 +- .../bentobox/limits/commands/player/LimitTab.java | 3 +-- .../bentobox/limits/commands/player/LimitTabTest.java | 10 +++++----- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main/java/world/bentobox/limits/commands/player/LimitPanel.java b/src/main/java/world/bentobox/limits/commands/player/LimitPanel.java index 4ce12c9..81681b6 100644 --- a/src/main/java/world/bentobox/limits/commands/player/LimitPanel.java +++ b/src/main/java/world/bentobox/limits/commands/player/LimitPanel.java @@ -82,7 +82,7 @@ private int addEnvTab(TabbedPanelBuilder tpb, World envWorld, IslandBlockCount i && addon.getSettings().getGroupLimits(env).isEmpty()) { return slot; } - tpb.tab(slot, new LimitTab(addon, ibc, matLimits, island, envWorld, user, env)); + tpb.tab(slot, new LimitTab(addon, ibc, matLimits, envWorld, user, env)); return slot + 1; } } diff --git a/src/main/java/world/bentobox/limits/commands/player/LimitTab.java b/src/main/java/world/bentobox/limits/commands/player/LimitTab.java index d420fb6..e5acaa8 100644 --- a/src/main/java/world/bentobox/limits/commands/player/LimitTab.java +++ b/src/main/java/world/bentobox/limits/commands/player/LimitTab.java @@ -25,7 +25,6 @@ import world.bentobox.bentobox.api.panels.Tab; import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; import world.bentobox.bentobox.api.user.User; -import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.util.Util; import world.bentobox.limits.EntityGroup; import world.bentobox.limits.Limits; @@ -79,7 +78,7 @@ public class LimitTab implements Tab { private final Environment env; private final List<@Nullable PanelItem> result; - public LimitTab(Limits addon, IslandBlockCount ibc, Map matLimits, Island island, + public LimitTab(Limits addon, IslandBlockCount ibc, Map matLimits, World world, User user, Environment env) { this.addon = addon; this.world = world; diff --git a/src/test/java/world/bentobox/limits/commands/player/LimitTabTest.java b/src/test/java/world/bentobox/limits/commands/player/LimitTabTest.java index 547fa06..2c29ed2 100644 --- a/src/test/java/world/bentobox/limits/commands/player/LimitTabTest.java +++ b/src/test/java/world/bentobox/limits/commands/player/LimitTabTest.java @@ -130,8 +130,8 @@ void blockCountReadsFromEnvKeyedIbc() { Map matLimits = Map.of(hopper, 10); - LimitTab overworldTab = new LimitTab(addon, ibc, matLimits, island, world, user, Environment.NORMAL); - LimitTab netherTab = new LimitTab(addon, ibc, matLimits, island, world, user, Environment.NETHER); + LimitTab overworldTab = new LimitTab(addon, ibc, matLimits, world, user, Environment.NORMAL); + LimitTab netherTab = new LimitTab(addon, ibc, matLimits, world, user, Environment.NETHER); assertEquals("3", findCount(overworldTab, "hopper"), "3 hoppers visible in overworld tab"); assertEquals("7", findCount(netherTab, "hopper"), "7 hoppers visible in nether tab"); @@ -145,8 +145,8 @@ void entityCountReadsFromEnvKeyedIbc() { when(settings.getLimits(Environment.NORMAL)).thenReturn(Map.of(EntityType.CHICKEN, 10)); when(settings.getLimits(Environment.NETHER)).thenReturn(Map.of(EntityType.CHICKEN, 10)); - LimitTab overworldTab = new LimitTab(addon, ibc, Collections.emptyMap(), island, world, user, Environment.NORMAL); - LimitTab netherTab = new LimitTab(addon, ibc, Collections.emptyMap(), island, world, user, Environment.NETHER); + LimitTab overworldTab = new LimitTab(addon, ibc, Collections.emptyMap(), world, user, Environment.NORMAL); + LimitTab netherTab = new LimitTab(addon, ibc, Collections.emptyMap(), world, user, Environment.NETHER); assertEquals("4", findCount(overworldTab, "chicken")); assertEquals("9", findCount(netherTab, "chicken")); @@ -155,7 +155,7 @@ void entityCountReadsFromEnvKeyedIbc() { @Test void tabIconAndTitleReflectEnvironment() { IslandBlockCount ibc = new IslandBlockCount("island", "BSkyBlock"); - LimitTab netherTab = new LimitTab(addon, ibc, Collections.emptyMap(), island, world, user, Environment.NETHER); + LimitTab netherTab = new LimitTab(addon, ibc, Collections.emptyMap(), world, user, Environment.NETHER); assertNotNull(netherTab.getIcon()); assertEquals(Material.NETHERRACK, netherTab.getIcon().getItem().getType()); assertTrue(netherTab.getName().contains("Nether"), "Title should contain 'Nether'; was: " + netherTab.getName()); From 208fc84b12ab830aaa96bb0340a00126e2457ddc Mon Sep 17 00:00:00 2001 From: tastybento Date: Wed, 6 May 2026 12:45:08 -0700 Subject: [PATCH 08/24] Address null-flow and diamond-operator findings (Sonar S2637, S2293) - JoinListener.applyOnePerm: pull the @Nullable event-IBC into a local before the null check so the analyzer sees the guard before the applyLimit call - IslandBlockCount.newInner: use diamond operator on the EnumMap via a typed local, dodging the unchecked-cast inference issue Co-Authored-By: Claude Opus 4.7 (1M context) --- .../java/world/bentobox/limits/listeners/JoinListener.java | 4 ++-- .../java/world/bentobox/limits/objects/IslandBlockCount.java | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/world/bentobox/limits/listeners/JoinListener.java b/src/main/java/world/bentobox/limits/listeners/JoinListener.java index 7079613..f1e90c7 100644 --- a/src/main/java/world/bentobox/limits/listeners/JoinListener.java +++ b/src/main/java/world/bentobox/limits/listeners/JoinListener.java @@ -102,8 +102,8 @@ private IslandBlockCount applyOnePerm(Player player, PermissionAttachmentInfo pe addon.log("Permissions not set because another addon/plugin canceled setting."); return ibc; } - IslandBlockCount finalIbc = limitsPermCheckEvent.getIbc() != null ? limitsPermCheckEvent.getIbc() - : new IslandBlockCount(islandId, gameMode); + IslandBlockCount eventIbc = limitsPermCheckEvent.getIbc(); + IslandBlockCount finalIbc = eventIbc != null ? eventIbc : new IslandBlockCount(islandId, gameMode); applyLimit(finalIbc, parsed.envs, limitsPermCheckEvent); return finalIbc; } diff --git a/src/main/java/world/bentobox/limits/objects/IslandBlockCount.java b/src/main/java/world/bentobox/limits/objects/IslandBlockCount.java index b1df9cb..cd4657e 100644 --- a/src/main/java/world/bentobox/limits/objects/IslandBlockCount.java +++ b/src/main/java/world/bentobox/limits/objects/IslandBlockCount.java @@ -142,7 +142,8 @@ private static void moveLegacy(Map legacy, Map Map newInner(Map sample) { Object first = sample.keySet().iterator().next(); if (first instanceof EntityType) { - return (Map) new EnumMap(EntityType.class); + Map enumMap = new EnumMap<>(EntityType.class); + return (Map) enumMap; } return new HashMap<>(); } From e40f45f47518aa83078e7e3e639472bf6078baeb Mon Sep 17 00:00:00 2001 From: tastybento Date: Wed, 6 May 2026 12:45:12 -0700 Subject: [PATCH 09/24] Drop public modifiers on JUnit 5 tests (Sonar S5786) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../world/bentobox/limits/objects/IslandBlockCountTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/world/bentobox/limits/objects/IslandBlockCountTest.java b/src/test/java/world/bentobox/limits/objects/IslandBlockCountTest.java index 2930e19..7043c51 100644 --- a/src/test/java/world/bentobox/limits/objects/IslandBlockCountTest.java +++ b/src/test/java/world/bentobox/limits/objects/IslandBlockCountTest.java @@ -200,7 +200,7 @@ private static Gson buildBentoboxGson() { } @Test - public void testReadLegacyJsonWithBareMaterialNames() { + void testReadLegacyJsonWithBareMaterialNames() { // This is the on-disk shape produced by versions of Limits where // blockCounts was Map. Pre-1d0a3b7 databases look // exactly like this and must still load. @@ -306,7 +306,7 @@ void entityCountIncrementDecrement() { } @Test - public void testReadJsonWithNamespacedStringKeys() { + void testReadJsonWithNamespacedStringKeys() { // This is the format the new adapter writes; it must round-trip. String json = """ { @@ -324,7 +324,7 @@ public void testReadJsonWithNamespacedStringKeys() { } @Test - public void testWriteRoundTripWithNamespacedKeys() { + void testWriteRoundTripWithNamespacedKeys() { ibc.add(Environment.NORMAL, stoneKey); ibc.add(Environment.NORMAL, stoneKey); ibc.setBlockLimit(Environment.NORMAL, NamespacedKey.minecraft("hopper"), 20); From 83b371a08dc03c833116b662265fcab4d1e28ec2 Mon Sep 17 00:00:00 2001 From: tastybento Date: Wed, 6 May 2026 12:46:54 -0700 Subject: [PATCH 10/24] Bump version to 1.28.2 Co-Authored-By: Claude Opus 4.7 (1M context) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index dde6857..8e3e5e8 100644 --- a/pom.xml +++ b/pom.xml @@ -62,7 +62,7 @@ -LOCAL - 1.28.1 + 1.28.2 BentoBoxWorld_Limits bentobox-world https://sonarcloud.io From 245d18d0618a6a7431ae4f328567f52575045c5c Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 29 May 2026 17:26:02 -0700 Subject: [PATCH 11/24] Pin MockBukkit to Maven Central 4.110.0 instead of jitpack snapshot The floating jitpack snapshot v1.21-SNAPSHOT resolves to an ephemeral git-described build whose jar/POM get evicted, breaking CI even on unchanged commits. Switch to the equivalent stable Maven Central coordinates (org.mockbukkit.mockbukkit:mockbukkit-v1.21:4.110.0). Co-Authored-By: Claude Opus 4.8 --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 8e3e5e8..4b401b0 100644 --- a/pom.xml +++ b/pom.xml @@ -135,9 +135,9 @@ - com.github.MockBukkit - MockBukkit - v1.21-SNAPSHOT + org.mockbukkit.mockbukkit + mockbukkit-v1.21 + 4.110.0 test From c00fb4f10b9bd067b64bdae1de6e210ee50150f2 Mon Sep 17 00:00:00 2001 From: tastybento Date: Wed, 3 Jun 2026 08:07:50 -0700 Subject: [PATCH 12/24] Update Modrinth game-versions to current MC range Brings the supported-versions list in line with the other addons (1.21.5-1.21.11, 26.1, 26.1.1, 26.1.2). Co-Authored-By: Claude Opus 4.8 --- .github/workflows/modrinth-publish.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/modrinth-publish.yml b/.github/workflows/modrinth-publish.yml index 350c3c8..2173242 100644 --- a/.github/workflows/modrinth-publish.yml +++ b/.github/workflows/modrinth-publish.yml @@ -87,6 +87,9 @@ jobs: 1.21.9 1.21.10 1.21.11 + 26.1 + 26.1.1 + 26.1.2 # Path to the built JAR — Maven produces Limits-.jar in target/ # Note: glob patterns are not supported; use the release tag directly. From 7f78a60073edfcf6f3cd5a8d6d9421aba72a6884 Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 12 Jun 2026 07:38:26 -0700 Subject: [PATCH 13/24] Add project key to SonarQube analysis command --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 06898a5..daf47b5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,4 +35,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar \ No newline at end of file + run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=BentoBoxWorld_Limits From c1a3e2b0394bd24f4fd28270df9b0fb8659c7fa7 Mon Sep 17 00:00:00 2001 From: daniel-skopek Date: Wed, 10 Jun 2026 22:41:14 +0200 Subject: [PATCH 14/24] Fix block count leaks and parallelize recount chunk loading - BlockSpreadEvent: decrement old block before adding new (was leaking old counts) - BlockGrowEvent: revert count increment when event is cancelled due to limit hit - RecountCalculator: load chunks in parallel instead of sequentially (fixes calc timeout on large islands) - RecountCalculator: use @NonNull getIsland(Island) to prevent permission limit loss on null ibc --- .../limits/calculators/RecountCalculator.java | 58 +++++++++---------- .../limits/listeners/BlockLimitsListener.java | 7 ++- .../listeners/BlockLimitsListenerTest.java | 3 + 3 files changed, 36 insertions(+), 32 deletions(-) diff --git a/src/main/java/world/bentobox/limits/calculators/RecountCalculator.java b/src/main/java/world/bentobox/limits/calculators/RecountCalculator.java index 117ac07..e094a63 100644 --- a/src/main/java/world/bentobox/limits/calculators/RecountCalculator.java +++ b/src/main/java/world/bentobox/limits/calculators/RecountCalculator.java @@ -11,6 +11,7 @@ import java.util.concurrent.ConcurrentLinkedQueue; import java.util.function.BooleanSupplier; import java.util.function.LongSupplier; +import java.util.stream.Collectors; import org.bukkit.Bukkit; import org.bukkit.Chunk; @@ -65,7 +66,7 @@ public RecountCalculator(Limits addon, Island island, CompletableFuture this.addon = addon; this.bll = addon.getBlockLimitListener(); this.island = island; - this.ibc = bll.getIsland(Objects.requireNonNull(island).getUniqueId()); + this.ibc = bll.getIsland(Objects.requireNonNull(island)); this.r = r; results = new Results(); chunksToCheck = getChunksToScan(island); @@ -119,28 +120,25 @@ public Results getResults() { private CompletableFuture> getWorldChunk(Environment env, Queue> pairList) { if (worlds.containsKey(env)) { - CompletableFuture> r2 = new CompletableFuture<>(); - List chunkList = new ArrayList<>(); World world = worlds.get(env); - loadChunks(r2, world, pairList, chunkList); - return r2; + boolean isNether = world.getEnvironment().equals(Environment.NETHER); + List> futures = new ArrayList<>(); + while (!pairList.isEmpty()) { + Pair p = pairList.poll(); + futures.add(Util.getChunkAtAsync(world, p.x, p.z, isNether)); + } + if (futures.isEmpty()) { + return CompletableFuture.completedFuture(Collections.emptyList()); + } + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .thenApply(v -> futures.stream() + .map(CompletableFuture::join) + .filter(Objects::nonNull) + .collect(Collectors.toList())); } return CompletableFuture.completedFuture(Collections.emptyList()); } - private void loadChunks(CompletableFuture> r2, World world, Queue> pairList, - List chunkList) { - if (pairList.isEmpty()) { - r2.complete(chunkList); - return; - } - Pair p = pairList.poll(); - Util.getChunkAtAsync(world, p.x, p.z, world.getEnvironment().equals(Environment.NETHER)).thenAccept(chunk -> { - if (chunk != null) chunkList.add(chunk); - loadChunks(r2, world, pairList, chunkList); - }); - } - private void scanAsync(Environment env, Chunk chunk) { ChunkSnapshot chunkSnapshot = chunk.getChunkSnapshot(); int minX = island.getMinProtectedX(); @@ -197,15 +195,16 @@ public CompletableFuture scanNextChunk() { } Queue> endPairList = new ConcurrentLinkedQueue<>(pairList); Queue> netherPairList = new ConcurrentLinkedQueue<>(pairList); - CompletableFuture result = new CompletableFuture<>(); - getWorldChunk(Environment.THE_END, endPairList).thenAccept(endChunks -> - scanChunk(Environment.THE_END, endChunks).thenAccept(b -> - getWorldChunk(Environment.NETHER, netherPairList).thenAccept(netherChunks -> - scanChunk(Environment.NETHER, netherChunks).thenAccept(b2 -> - getWorldChunk(Environment.NORMAL, pairList).thenAccept(normalChunks -> - scanChunk(Environment.NORMAL, normalChunks).thenAccept(b3 -> - result.complete(!chunksToCheck.isEmpty()))))))); - return result; + + CompletableFuture> endFuture = getWorldChunk(Environment.THE_END, endPairList); + CompletableFuture> netherFuture = getWorldChunk(Environment.NETHER, netherPairList); + CompletableFuture> normalFuture = getWorldChunk(Environment.NORMAL, pairList); + + return CompletableFuture.allOf(endFuture, netherFuture, normalFuture) + .thenCompose(v -> scanChunk(Environment.THE_END, endFuture.join()) + .thenCompose(b -> scanChunk(Environment.NETHER, netherFuture.join())) + .thenCompose(b2 -> scanChunk(Environment.NORMAL, normalFuture.join())) + .thenApply(b3 -> !chunksToCheck.isEmpty())); } private void scanEntities() { @@ -225,10 +224,7 @@ private void scanEntities() { } public void tidyUp() { - if (ibc == null) { - ibc = new IslandBlockCount(island.getUniqueId(), - addon.getPlugin().getIWM().getAddon(world).map(a -> a.getDescription().getName()).orElse("default")); - } + ibc = bll.getIsland(island); // Reset and write per-env block counts ibc.clearAllBlockCounts(); results.getEnvBlockCount().forEach((env, multiset) -> multiset.forEach(key -> ibc.add(env, key))); diff --git a/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java b/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java index 6fc12cc..45e6df0 100644 --- a/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java +++ b/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java @@ -290,7 +290,11 @@ public void onBlock(BlockFormEvent e) { @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onBlock(BlockSpreadEvent e) { - process(e.getBlock(), true); + process(e.getBlock(), false); + if (process(e.getBlock(), e.getNewState().getBlockData(), true) > -1) { + e.setCancelled(true); + process(e.getBlock(), true); + } } @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) @@ -306,6 +310,7 @@ public void onBlock(EntityBlockFormEvent e) { public void onBlock(BlockGrowEvent e) { if (process(e.getNewState().getBlock(), true) > -1) { e.setCancelled(true); + process(e.getNewState().getBlock(), false); e.getBlock().getWorld().getBlockAt(e.getBlock().getLocation()).setBlockData(e.getBlock().getBlockData()); } else { process(e.getBlock(), false); diff --git a/src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java b/src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java index 7549d65..9009fb2 100644 --- a/src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java +++ b/src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java @@ -579,6 +579,9 @@ public void testBlockSpreadIncrementsCount() { Block block = mockBlock(Material.GRASS_BLOCK, blockLocation); Block source = mockBlock(Material.GRASS_BLOCK, new Location(world, 101, 65, 100)); BlockState newState = mock(BlockState.class); + BlockData newBlockData = mock(BlockData.class); + when(newBlockData.getMaterial()).thenReturn(Material.GRASS_BLOCK); + when(newState.getBlockData()).thenReturn(newBlockData); BlockSpreadEvent event = new BlockSpreadEvent(block, source, newState); listener.onBlock(event); From 66988242fba2d7dd148633ec6a6df325505298ed Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 12 Jun 2026 07:41:40 -0700 Subject: [PATCH 15/24] Revert unbalanced BlockGrowEvent decrement; add spread/grow tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The BlockGrowEvent handler decremented the new block's count after a cancelled (over-limit) grow. But process(block, true) returns before incrementing when the limit is hit, so that decrement removed a count that was never added — driving a real, physically-present block's count below its true value and letting the next grow slip past the limit. Drop the spurious decrement (the cancel branch was already balanced) and add tests covering: - BlockSpreadEvent decrementing the old block and adding the new (the leak this PR fixes) - BlockSpreadEvent reverting cleanly when the new material is at limit - BlockGrowEvent at a non-zero limit leaving the count untouched on a blocked grow (regression guard for the reverted line) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../limits/listeners/BlockLimitsListener.java | 1 - .../listeners/BlockLimitsListenerTest.java | 72 +++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java b/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java index 45e6df0..ff71cf6 100644 --- a/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java +++ b/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java @@ -310,7 +310,6 @@ public void onBlock(EntityBlockFormEvent e) { public void onBlock(BlockGrowEvent e) { if (process(e.getNewState().getBlock(), true) > -1) { e.setCancelled(true); - process(e.getNewState().getBlock(), false); e.getBlock().getWorld().getBlockAt(e.getBlock().getLocation()).setBlockData(e.getBlock().getBlockData()); } else { process(e.getBlock(), false); diff --git a/src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java b/src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java index 9009fb2..eedc0dd 100644 --- a/src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java +++ b/src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java @@ -591,6 +591,52 @@ public void testBlockSpreadIncrementsCount() { assertEquals(1, ibc.getBlockCount(Material.GRASS_BLOCK.getKey())); } + @Test + public void testBlockSpreadDecrementsOldAddsNew() { + // Grass spreading onto dirt: DIRT count should drop, GRASS_BLOCK count should rise. + IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); + ibc.add(Environment.NORMAL, Material.DIRT.getKey()); + listener.setIsland("test-island-id", ibc); + + Block block = mockBlock(Material.DIRT, blockLocation); + Block source = mockBlock(Material.GRASS_BLOCK, new Location(world, 101, 65, 100)); + BlockState newState = mock(BlockState.class); + BlockData newBlockData = mock(BlockData.class); + when(newBlockData.getMaterial()).thenReturn(Material.GRASS_BLOCK); + when(newState.getBlockData()).thenReturn(newBlockData); + BlockSpreadEvent event = new BlockSpreadEvent(block, source, newState); + + listener.onBlock(event); + + assertFalse(event.isCancelled()); + assertEquals(0, ibc.getBlockCount(Material.DIRT.getKey())); + assertEquals(1, ibc.getBlockCount(Material.GRASS_BLOCK.getKey())); + } + + @Test + public void testBlockSpreadAtLimitCancelsAndRestoresOld() { + // GRASS_BLOCK is at limit; spread onto DIRT must be cancelled and the DIRT count preserved. + IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); + ibc.add(Environment.NORMAL, Material.DIRT.getKey()); + ibc.setBlockLimit(Environment.NORMAL, Material.GRASS_BLOCK.getKey(), 1); + ibc.add(Environment.NORMAL, Material.GRASS_BLOCK.getKey()); + listener.setIsland("test-island-id", ibc); + + Block block = mockBlock(Material.DIRT, blockLocation); + Block source = mockBlock(Material.GRASS_BLOCK, new Location(world, 101, 65, 100)); + BlockState newState = mock(BlockState.class); + BlockData newBlockData = mock(BlockData.class); + when(newBlockData.getMaterial()).thenReturn(Material.GRASS_BLOCK); + when(newState.getBlockData()).thenReturn(newBlockData); + BlockSpreadEvent event = new BlockSpreadEvent(block, source, newState); + + listener.onBlock(event); + + assertTrue(event.isCancelled()); + assertEquals(1, ibc.getBlockCount(Material.DIRT.getKey())); + assertEquals(1, ibc.getBlockCount(Material.GRASS_BLOCK.getKey())); + } + // --- BlockFromToEvent tests --- @Test @@ -733,6 +779,32 @@ public void testBlockGrowAtLimitCancelsAndRestoresBlockData() { verify(worldBlock).setBlockData(block.getBlockData()); } + @Test + public void testBlockGrowAtNonZeroLimitDoesNotDecrement() { + // GRASS_BLOCK is at a non-zero limit (5 of 5). A blocked grow must NOT change the count. + IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); + ibc.setBlockLimit(Environment.NORMAL, Material.GRASS_BLOCK.getKey(), 5); + for (int i = 0; i < 5; i++) { + ibc.add(Environment.NORMAL, Material.GRASS_BLOCK.getKey()); + } + listener.setIsland("test-island-id", ibc); + + Block block = mockBlock(Material.DIRT, blockLocation); + Block newBlock = mockBlock(Material.GRASS_BLOCK, blockLocation); + BlockState newState = mock(BlockState.class); + when(newState.getBlock()).thenReturn(newBlock); + + Block worldBlock = mock(Block.class); + when(world.getBlockAt(blockLocation)).thenReturn(worldBlock); + + BlockGrowEvent event = new BlockGrowEvent(block, newState); + listener.onBlock(event); + + assertTrue(event.isCancelled()); + // The physical world still has 5 grass blocks, so the count must remain 5. + assertEquals(5, ibc.getBlockCount(Material.GRASS_BLOCK.getKey())); + } + // --- EntityChangeBlockEvent state transition tests --- @Test From e222088655b39387d9e93b03a21de54fabeb2d42 Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 12 Jun 2026 07:48:13 -0700 Subject: [PATCH 16/24] Document block state-transition counting invariant and project layout in CLAUDE.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a "Block State Transitions" section capturing the count-balance rule for form/spread/grow handlers (decrement old, add new with limit check, revert on cancel) and the gotcha that process(block, true) does not increment when the limit is hit — so no compensating decrement belongs in the cancel branch. Also brings in the pending Dependency Source Lookup, Project Layout, and Key Dependencies sections. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 5a00e33..fbf4940 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -90,6 +90,20 @@ Two formats: `BlockLimitsListener.fixMaterial()` normalizes variant materials to their canonical form (e.g., `CHIPPED_ANVIL` → `ANVIL`, `REDSTONE_WALL_TORCH` → `REDSTONE_TORCH`, `PISTON_HEAD` → `PISTON`/`STICKY_PISTON`). Block config keys can use Bukkit Material names or Minecraft tag names (e.g., `minecraft:logs`). +### Block State Transitions + +When one block becomes another in place — `BlockFormEvent`, `EntityBlockFormEvent`, `BlockSpreadEvent` (grass/mycelium/fire spread), `BlockGrowEvent` (crops, sugar cane, etc.) — the handlers must keep the count balanced: **decrement the replaced block, then add the new one with a limit check, and revert on cancel.** The canonical shape (see `onBlock(BlockFormEvent)`): + +```java +process(e.getBlock(), false); // remove old +if (process(e.getBlock(), e.getNewState().getBlockData(), true) > -1) { + e.setCancelled(true); + process(e.getBlock(), true); // limit hit → restore old +} +``` + +Key invariant when editing these handlers: `process(block, true)` returns the limit **before** incrementing when the limit is hit, so it only ever adds to the count on the success path (`-1` return). Do **not** add a compensating `process(..., false)` in the cancel branch — nothing was added, so it would decrement a real block's count below its true value and let the next transition slip past the limit. `BlockLimitsListenerTest` guards this with the `*StateTransition`, `*AtLimitCancelsAndRestoresOld`, and `testBlockGrowAtNonZeroLimitDoesNotDecrement` cases. + ### Events API Two cancellable events are fired so other plugins can intercept limit application: @@ -166,3 +180,92 @@ worldLimitField.setAccessible(true); #### Testing Debounce (`justSpawned`) `EntityLimitListener`'s private `justSpawned` list (deduplicates entity spawns) can be accessed via reflection to test debounce behavior. + +## Dependency Source Lookup + +When you need to inspect source code for a dependency (e.g., BentoBox, addons): + +1. **Check local Maven repo first**: `~/.m2/repository/` — sources jars are named `*-sources.jar` +2. **Check the workspace**: Look for sibling directories or Git submodules that may contain the dependency as a local project (e.g., `../bentoBox`, `../addon-*`) +3. **Check Maven local cache for already-extracted sources** before downloading anything +4. Only download a jar or fetch from the internet if the above steps yield nothing useful + +Prefer reading `.java` source files directly from a local Git clone over decompiling or extracting a jar. + +In general, the latest version of BentoBox should be targeted. + +## Project Layout + +Related projects are checked out as siblings under `~/git/`: + +**Core:** +- `bentobox/` — core BentoBox framework + +**Game modes:** +- `addon-acidisland/` — AcidIsland game mode +- `addon-bskyblock/` — BSkyBlock game mode +- `Boxed/` — Boxed game mode (expandable box area) +- `CaveBlock/` — CaveBlock game mode +- `OneBlock/` — AOneBlock game mode +- `SkyGrid/` — SkyGrid game mode +- `RaftMode/` — Raft survival game mode +- `StrangerRealms/` — StrangerRealms game mode +- `Brix/` — plot game mode +- `parkour/` — Parkour game mode +- `poseidon/` — Poseidon game mode +- `gg/` — gg game mode + +**Addons:** +- `addon-level/` — island level calculation +- `addon-challenges/` — challenges system +- `addon-welcomewarpsigns/` — warp signs +- `addon-limits/` — block/entity limits +- `addon-invSwitcher/` / `invSwitcher/` — inventory switcher +- `addon-biomes/` / `Biomes/` — biomes management +- `Bank/` — island bank +- `Border/` — world border for islands +- `Chat/` — island chat +- `CheckMeOut/` — island submission/voting +- `ControlPanel/` — game mode control panel +- `Converter/` — ASkyBlock to BSkyBlock converter +- `DimensionalTrees/` — dimension-specific trees +- `discordwebhook/` — Discord integration +- `Downloads/` — BentoBox downloads site +- `DragonFights/` — per-island ender dragon fights +- `ExtraMobs/` — additional mob spawning rules +- `FarmersDance/` — twerking crop growth +- `GravityFlux/` — gravity addon +- `Greenhouses-addon/` — greenhouse biomes +- `IslandFly/` — island flight permission +- `IslandRankup/` — island rankup system +- `Likes/` — island likes/dislikes +- `Limits/` — block/entity limits +- `lost-sheep/` — lost sheep adventure +- `MagicCobblestoneGenerator/` — custom cobblestone generator +- `PortalStart/` — portal-based island start +- `pp/` — pp addon +- `Regionerator/` — region management +- `Residence/` — residence addon +- `TopBlock/` — top ten for OneBlock +- `TwerkingForTrees/` — twerking tree growth +- `Upgrades/` — island upgrades (Vault) +- `Visit/` — island visiting +- `weblink/` — web link addon +- `CrowdBound/` — CrowdBound addon + +**Data packs:** +- `BoxedDataPack/` — advancement datapack for Boxed + +**Documentation & tools:** +- `docs/` — main documentation site +- `docs-chinese/` — Chinese documentation +- `docs-french/` — French documentation +- `BentoBoxWorld.github.io/` — GitHub Pages site +- `website/` — website +- `translation-tool/` — translation tool + +Check these for source before any network fetch. + +## Key Dependencies (source locations) + +- `world.bentobox:bentobox` → `~/git/bentobox/src/` From 580b66e3f02352482927d80f6a580f4c598142c3 Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 12 Jun 2026 08:01:12 -0700 Subject: [PATCH 17/24] Count multi-block placements (beds, doors) once, not twice (#86) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BlockMultiPlaceEvent extends BlockPlaceEvent and does not declare its own HandlerList, so a single multi-place dispatch is delivered to BOTH onBlock(BlockPlaceEvent) and onBlock(BlockMultiPlaceEvent) — each calling process(block, true). A bed (or door, double-tall plant) was therefore counted twice on placement, while breaking it fired a single BlockBreakEvent and decremented once, permanently leaking +1 per place/break cycle until the island hit a phantom limit. Skip BlockMultiPlaceEvent instances in the BlockPlaceEvent handler and let the dedicated handler count them once — the same subtype-guard pattern onBlock(BlockFormEvent) already uses for its subclasses. Tests dispatch through the real plugin manager (so the double delivery actually occurs) and assert a bed counts once and returns to zero after a break. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../limits/listeners/BlockLimitsListener.java | 5 +++ .../listeners/BlockLimitsListenerTest.java | 44 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java b/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java index ff71cf6..890500b 100644 --- a/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java +++ b/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java @@ -193,6 +193,11 @@ private Environment envOf(World w) { // Player-related events @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onBlock(BlockPlaceEvent e) { + // BlockMultiPlaceEvent is a subclass of BlockPlaceEvent and shares its HandlerList, + // so it is delivered here too. Let the dedicated handler count it once (see #86). + if (e instanceof BlockMultiPlaceEvent) { + return; + } notify(e, User.getInstance(e.getPlayer()), process(e.getBlock(), true), e.getBlock().getType()); } diff --git a/src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java b/src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java index eedc0dd..bdf2c7c 100644 --- a/src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java +++ b/src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java @@ -511,6 +511,50 @@ public void testBlockMultiPlaceAtLimitCancels() { assertTrue(event.isCancelled()); } + @Test + public void testBedPlacedViaMultiPlaceCountsOnce() { + // Regression for #86. A bed places two blocks and fires a BlockMultiPlaceEvent. + // BlockMultiPlaceEvent shares BlockPlaceEvent's HandlerList, so a single dispatch + // reaches BOTH onBlock(BlockPlaceEvent) and onBlock(BlockMultiPlaceEvent). The bed + // must be counted once, not twice. Dispatch through the real plugin manager so the + // double-delivery actually happens (a direct handler call would mask it). + org.mockbukkit.mockbukkit.plugin.PluginMock mockPlugin = MockBukkit.createMockPlugin(); + org.bukkit.Bukkit.getPluginManager().registerEvents(listener, mockPlugin); + + Block foot = mockBlock(Material.RED_BED, blockLocation); + Block head = mockBlock(Material.RED_BED, new Location(world, 100, 65, 101)); + BlockState headState = mock(BlockState.class); + when(headState.getBlock()).thenReturn(head); + BlockMultiPlaceEvent event = new BlockMultiPlaceEvent(List.of(headState), foot, + new ItemStack(Material.RED_BED), player, true); + + org.bukkit.Bukkit.getPluginManager().callEvent(event); + + assertEquals(1, listener.getIsland("test-island-id").getBlockCount(Material.RED_BED.getKey())); + } + + @Test + public void testBedPlaceThenBreakLeavesZero() { + // Regression for #86: placing then breaking a bed must return the count to 0, + // not leave a phantom bed behind. + org.mockbukkit.mockbukkit.plugin.PluginMock mockPlugin = MockBukkit.createMockPlugin(); + org.bukkit.Bukkit.getPluginManager().registerEvents(listener, mockPlugin); + + Block foot = mockBlock(Material.RED_BED, blockLocation); + Block head = mockBlock(Material.RED_BED, new Location(world, 100, 65, 101)); + BlockState headState = mock(BlockState.class); + when(headState.getBlock()).thenReturn(head); + BlockMultiPlaceEvent place = new BlockMultiPlaceEvent(List.of(headState), foot, + new ItemStack(Material.RED_BED), player, true); + org.bukkit.Bukkit.getPluginManager().callEvent(place); + + // Player breaks one half of the bed (vanilla removes the other half with no break event). + BlockBreakEvent breakEvent = new BlockBreakEvent(foot, player); + org.bukkit.Bukkit.getPluginManager().callEvent(breakEvent); + + assertEquals(0, listener.getIsland("test-island-id").getBlockCount(Material.RED_BED.getKey())); + } + // --- PlayerInteractEvent tests --- @Test From 30c5c0e6a202549595595c0f903ca5afe082c066 Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 12 Jun 2026 08:36:14 -0700 Subject: [PATCH 18/24] Remove 'public' modifier from test methods --- .../listeners/BlockLimitsListenerTest.java | 140 +++++++++--------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java b/src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java index bdf2c7c..dc117dd 100644 --- a/src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java +++ b/src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java @@ -185,49 +185,49 @@ public void tearDown() { // --- fixMaterial tests --- @Test - public void testFixMaterialChippedAnvil() { + void testFixMaterialChippedAnvil() { BlockData blockData = mock(BlockData.class); when(blockData.getMaterial()).thenReturn(Material.CHIPPED_ANVIL); assertEquals(Material.ANVIL.getKey(), listener.fixMaterial(blockData)); } @Test - public void testFixMaterialDamagedAnvil() { + void testFixMaterialDamagedAnvil() { BlockData blockData = mock(BlockData.class); when(blockData.getMaterial()).thenReturn(Material.DAMAGED_ANVIL); assertEquals(Material.ANVIL.getKey(), listener.fixMaterial(blockData)); } @Test - public void testFixMaterialRedstoneWallTorch() { + void testFixMaterialRedstoneWallTorch() { BlockData blockData = mock(BlockData.class); when(blockData.getMaterial()).thenReturn(Material.REDSTONE_WALL_TORCH); assertEquals(Material.REDSTONE_TORCH.getKey(), listener.fixMaterial(blockData)); } @Test - public void testFixMaterialWallTorch() { + void testFixMaterialWallTorch() { BlockData blockData = mock(BlockData.class); when(blockData.getMaterial()).thenReturn(Material.WALL_TORCH); assertEquals(Material.TORCH.getKey(), listener.fixMaterial(blockData)); } @Test - public void testFixMaterialZombieWallHead() { + void testFixMaterialZombieWallHead() { BlockData blockData = mock(BlockData.class); when(blockData.getMaterial()).thenReturn(Material.ZOMBIE_WALL_HEAD); assertEquals(Material.ZOMBIE_HEAD.getKey(), listener.fixMaterial(blockData)); } @Test - public void testFixMaterialBambooSapling() { + void testFixMaterialBambooSapling() { BlockData blockData = mock(BlockData.class); when(blockData.getMaterial()).thenReturn(Material.BAMBOO_SAPLING); assertEquals(Material.BAMBOO.getKey(), listener.fixMaterial(blockData)); } @Test - public void testFixMaterialPistonHeadNormal() { + void testFixMaterialPistonHeadNormal() { TechnicalPiston tp = mock(TechnicalPiston.class); when(tp.getMaterial()).thenReturn(Material.PISTON_HEAD); when(tp.getType()).thenReturn(TechnicalPiston.Type.NORMAL); @@ -235,7 +235,7 @@ public void testFixMaterialPistonHeadNormal() { } @Test - public void testFixMaterialPistonHeadSticky() { + void testFixMaterialPistonHeadSticky() { TechnicalPiston tp = mock(TechnicalPiston.class); when(tp.getMaterial()).thenReturn(Material.PISTON_HEAD); when(tp.getType()).thenReturn(TechnicalPiston.Type.STICKY); @@ -243,70 +243,70 @@ public void testFixMaterialPistonHeadSticky() { } @Test - public void testFixMaterialRegularStone() { + void testFixMaterialRegularStone() { BlockData blockData = mock(BlockData.class); when(blockData.getMaterial()).thenReturn(Material.STONE); assertEquals(Material.STONE.getKey(), listener.fixMaterial(blockData)); } @Test - public void testFixMaterialCopperWallTorch() { + void testFixMaterialCopperWallTorch() { BlockData blockData = mock(BlockData.class); when(blockData.getMaterial()).thenReturn(Material.COPPER_WALL_TORCH); assertEquals(Material.COPPER_TORCH.getKey(), listener.fixMaterial(blockData)); } @Test - public void testFixMaterialExposedCopperChest() { + void testFixMaterialExposedCopperChest() { BlockData blockData = mock(BlockData.class); when(blockData.getMaterial()).thenReturn(Material.EXPOSED_COPPER_CHEST); assertEquals(Material.COPPER_CHEST.getKey(), listener.fixMaterial(blockData)); } @Test - public void testFixMaterialWeatheredCopperChest() { + void testFixMaterialWeatheredCopperChest() { BlockData blockData = mock(BlockData.class); when(blockData.getMaterial()).thenReturn(Material.WEATHERED_COPPER_CHEST); assertEquals(Material.COPPER_CHEST.getKey(), listener.fixMaterial(blockData)); } @Test - public void testFixMaterialOxidizedCopperChest() { + void testFixMaterialOxidizedCopperChest() { BlockData blockData = mock(BlockData.class); when(blockData.getMaterial()).thenReturn(Material.OXIDIZED_COPPER_CHEST); assertEquals(Material.COPPER_CHEST.getKey(), listener.fixMaterial(blockData)); } @Test - public void testFixMaterialWaxedCopperChest() { + void testFixMaterialWaxedCopperChest() { BlockData blockData = mock(BlockData.class); when(blockData.getMaterial()).thenReturn(Material.WAXED_COPPER_CHEST); assertEquals(Material.COPPER_CHEST.getKey(), listener.fixMaterial(blockData)); } @Test - public void testFixMaterialWaxedExposedCopperChest() { + void testFixMaterialWaxedExposedCopperChest() { BlockData blockData = mock(BlockData.class); when(blockData.getMaterial()).thenReturn(Material.WAXED_EXPOSED_COPPER_CHEST); assertEquals(Material.COPPER_CHEST.getKey(), listener.fixMaterial(blockData)); } @Test - public void testFixMaterialWaxedWeatheredCopperChest() { + void testFixMaterialWaxedWeatheredCopperChest() { BlockData blockData = mock(BlockData.class); when(blockData.getMaterial()).thenReturn(Material.WAXED_WEATHERED_COPPER_CHEST); assertEquals(Material.COPPER_CHEST.getKey(), listener.fixMaterial(blockData)); } @Test - public void testFixMaterialWaxedOxidizedCopperChest() { + void testFixMaterialWaxedOxidizedCopperChest() { BlockData blockData = mock(BlockData.class); when(blockData.getMaterial()).thenReturn(Material.WAXED_OXIDIZED_COPPER_CHEST); assertEquals(Material.COPPER_CHEST.getKey(), listener.fixMaterial(blockData)); } @Test - public void testFixMaterialCopperChestUnchanged() { + void testFixMaterialCopperChestUnchanged() { BlockData blockData = mock(BlockData.class); when(blockData.getMaterial()).thenReturn(Material.COPPER_CHEST); assertEquals(Material.COPPER_CHEST.getKey(), listener.fixMaterial(blockData)); @@ -315,7 +315,7 @@ public void testFixMaterialCopperChestUnchanged() { // --- setIsland / getIsland tests --- @Test - public void testSetAndGetIsland() { + void testSetAndGetIsland() { String islandId = "test-island-123"; IslandBlockCount ibc = new IslandBlockCount(islandId, "BSkyBlock"); listener.setIsland(islandId, ibc); @@ -325,14 +325,14 @@ public void testSetAndGetIsland() { } @Test - public void testGetIslandUnknownIdReturnsNull() { + void testGetIslandUnknownIdReturnsNull() { assertNull(listener.getIsland("nonexistent-island-id")); } // --- getMaterialLimits tests --- @Test - public void testGetMaterialLimitsDefaultOnly() { + void testGetMaterialLimitsDefaultOnly() { Map limits = listener.getMaterialLimits(world, "some-island"); assertNotNull(limits); // The default config has HOPPER: 10 in blocklimits @@ -341,7 +341,7 @@ public void testGetMaterialLimitsDefaultOnly() { } @Test - public void testGetMaterialLimitsIslandOverridesDefault() { + void testGetMaterialLimitsIslandOverridesDefault() { String islandId = "island-override-test"; IslandBlockCount ibc = new IslandBlockCount(islandId, "BSkyBlock"); ibc.setBlockLimit(Environment.NORMAL, Material.HOPPER.getKey(), 25); @@ -373,7 +373,7 @@ private Block mockBlock(Material material, Location location) { // --- BlockPlaceEvent tests --- @Test - public void testBlockPlaceIncrementsCount() { + void testBlockPlaceIncrementsCount() { Block block = mockBlock(Material.STONE, blockLocation); BlockState replacedState = mock(BlockState.class); BlockPlaceEvent event = new BlockPlaceEvent(block, replacedState, block, new ItemStack(Material.STONE), player, true, EquipmentSlot.HAND); @@ -386,7 +386,7 @@ public void testBlockPlaceIncrementsCount() { } @Test - public void testBlockPlaceAtLimitCancelsEvent() { + void testBlockPlaceAtLimitCancelsEvent() { // Pre-populate island with 10 HOPPERs (default config limit is 10) IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); for (int i = 0; i < 10; i++) { @@ -404,7 +404,7 @@ public void testBlockPlaceAtLimitCancelsEvent() { } @Test - public void testBlockPlaceUnlimitedMaterialAllowed() { + void testBlockPlaceUnlimitedMaterialAllowed() { Block block = mockBlock(Material.DIRT, blockLocation); BlockState replacedState = mock(BlockState.class); BlockPlaceEvent event = new BlockPlaceEvent(block, replacedState, block, new ItemStack(Material.DIRT), player, true, EquipmentSlot.HAND); @@ -415,7 +415,7 @@ public void testBlockPlaceUnlimitedMaterialAllowed() { } @Test - public void testBlockPlaceDoNotCountWaterBlock() { + void testBlockPlaceDoNotCountWaterBlock() { Block block = mockBlock(Material.WATER, blockLocation); BlockState replacedState = mock(BlockState.class); BlockPlaceEvent event = new BlockPlaceEvent(block, replacedState, block, new ItemStack(Material.WATER_BUCKET), player, true, EquipmentSlot.HAND); @@ -427,7 +427,7 @@ public void testBlockPlaceDoNotCountWaterBlock() { } @Test - public void testBlockPlaceOutsideGameModeWorldIgnored() { + void testBlockPlaceOutsideGameModeWorldIgnored() { when(addon.inGameModeWorld(world)).thenReturn(false); Block block = mockBlock(Material.STONE, blockLocation); @@ -442,7 +442,7 @@ public void testBlockPlaceOutsideGameModeWorldIgnored() { // --- BlockBreakEvent tests --- @Test - public void testBlockBreakDecrementsCount() { + void testBlockBreakDecrementsCount() { // Pre-populate with 3 STONE IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); ibc.add(Environment.NORMAL, Material.STONE.getKey()); @@ -459,7 +459,7 @@ public void testBlockBreakDecrementsCount() { } @Test - public void testBlockBreakCountNeverGoesNegative() { + void testBlockBreakCountNeverGoesNegative() { // Start with 0 STONE (no pre-population) IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); listener.setIsland("test-island-id", ibc); @@ -473,7 +473,7 @@ public void testBlockBreakCountNeverGoesNegative() { } @Test - public void testBlockBreakWithMetadataIgnoreFlagSkipped() { + void testBlockBreakWithMetadataIgnoreFlagSkipped() { // Pre-populate with 1 STONE IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); ibc.add(Environment.NORMAL, Material.STONE.getKey()); @@ -492,7 +492,7 @@ public void testBlockBreakWithMetadataIgnoreFlagSkipped() { // --- BlockMultiPlaceEvent tests --- @Test - public void testBlockMultiPlaceAtLimitCancels() { + void testBlockMultiPlaceAtLimitCancels() { // Set limit for OAK_PLANKS to 1 via island-specific limit IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); ibc.setBlockLimit(Environment.NORMAL, Material.OAK_PLANKS.getKey(), 1); @@ -512,7 +512,7 @@ public void testBlockMultiPlaceAtLimitCancels() { } @Test - public void testBedPlacedViaMultiPlaceCountsOnce() { + void testBedPlacedViaMultiPlaceCountsOnce() { // Regression for #86. A bed places two blocks and fires a BlockMultiPlaceEvent. // BlockMultiPlaceEvent shares BlockPlaceEvent's HandlerList, so a single dispatch // reaches BOTH onBlock(BlockPlaceEvent) and onBlock(BlockMultiPlaceEvent). The bed @@ -534,7 +534,7 @@ public void testBedPlacedViaMultiPlaceCountsOnce() { } @Test - public void testBedPlaceThenBreakLeavesZero() { + void testBedPlaceThenBreakLeavesZero() { // Regression for #86: placing then breaking a bed must return the count to 0, // not leave a phantom bed behind. org.mockbukkit.mockbukkit.plugin.PluginMock mockPlugin = MockBukkit.createMockPlugin(); @@ -558,7 +558,7 @@ public void testBedPlaceThenBreakLeavesZero() { // --- PlayerInteractEvent tests --- @Test - public void testTurtleEggNonPhysicalActionIgnored() { + void testTurtleEggNonPhysicalActionIgnored() { Block block = mockBlock(Material.TURTLE_EGG, blockLocation); PlayerInteractEvent event = new PlayerInteractEvent(player, Action.LEFT_CLICK_BLOCK, null, block, BlockFace.UP); @@ -571,7 +571,7 @@ public void testTurtleEggNonPhysicalActionIgnored() { // --- BlockBurnEvent / BlockFadeEvent / LeavesDecayEvent tests --- @Test - public void testBlockBurnDecrementsCount() { + void testBlockBurnDecrementsCount() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); ibc.add(Environment.NORMAL, Material.OAK_PLANKS.getKey()); ibc.add(Environment.NORMAL, Material.OAK_PLANKS.getKey()); @@ -586,7 +586,7 @@ public void testBlockBurnDecrementsCount() { } @Test - public void testBlockFadeDecrementsCount() { + void testBlockFadeDecrementsCount() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); ibc.add(Environment.NORMAL, Material.SAND.getKey()); ibc.add(Environment.NORMAL, Material.SAND.getKey()); @@ -602,7 +602,7 @@ public void testBlockFadeDecrementsCount() { } @Test - public void testLeavesDecayDecrementsCount() { + void testLeavesDecayDecrementsCount() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); ibc.add(Environment.NORMAL, Material.OAK_LEAVES.getKey()); ibc.add(Environment.NORMAL, Material.OAK_LEAVES.getKey()); @@ -619,7 +619,7 @@ public void testLeavesDecayDecrementsCount() { // --- BlockSpreadEvent tests --- @Test - public void testBlockSpreadIncrementsCount() { + void testBlockSpreadIncrementsCount() { Block block = mockBlock(Material.GRASS_BLOCK, blockLocation); Block source = mockBlock(Material.GRASS_BLOCK, new Location(world, 101, 65, 100)); BlockState newState = mock(BlockState.class); @@ -636,7 +636,7 @@ public void testBlockSpreadIncrementsCount() { } @Test - public void testBlockSpreadDecrementsOldAddsNew() { + void testBlockSpreadDecrementsOldAddsNew() { // Grass spreading onto dirt: DIRT count should drop, GRASS_BLOCK count should rise. IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); ibc.add(Environment.NORMAL, Material.DIRT.getKey()); @@ -658,7 +658,7 @@ public void testBlockSpreadDecrementsOldAddsNew() { } @Test - public void testBlockSpreadAtLimitCancelsAndRestoresOld() { + void testBlockSpreadAtLimitCancelsAndRestoresOld() { // GRASS_BLOCK is at limit; spread onto DIRT must be cancelled and the DIRT count preserved. IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); ibc.add(Environment.NORMAL, Material.DIRT.getKey()); @@ -684,7 +684,7 @@ public void testBlockSpreadAtLimitCancelsAndRestoresOld() { // --- BlockFromToEvent tests --- @Test - public void testBlockFromToNonLiquidIgnored() { + void testBlockFromToNonLiquidIgnored() { Block sourceBlock = mockBlock(Material.STONE, blockLocation); when(sourceBlock.isLiquid()).thenReturn(false); Block toBlock = mockBlock(Material.REDSTONE_WIRE, new Location(world, 101, 65, 100)); @@ -699,7 +699,7 @@ public void testBlockFromToNonLiquidIgnored() { // --- BlockFormEvent / EntityBlockFormEvent tests --- @Test - public void testBlockFormIgnoresEntityBlockFormEvent() { + void testBlockFormIgnoresEntityBlockFormEvent() { Block block = mockBlock(Material.STONE, blockLocation); BlockState newState = mock(BlockState.class); Entity entity = mock(Entity.class); @@ -715,7 +715,7 @@ public void testBlockFormIgnoresEntityBlockFormEvent() { // --- Block state transition tests --- @Test - public void testBlockFormStateTransition() { + void testBlockFormStateTransition() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); ibc.add(Environment.NORMAL, Material.COBBLESTONE.getKey()); listener.setIsland("test-island-id", ibc); @@ -735,7 +735,7 @@ public void testBlockFormStateTransition() { } @Test - public void testBlockFormAtLimitCancelsAndRestoresOld() { + void testBlockFormAtLimitCancelsAndRestoresOld() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); ibc.add(Environment.NORMAL, Material.COBBLESTONE.getKey()); ibc.setBlockLimit(Environment.NORMAL, Material.STONE.getKey(), 1); @@ -757,7 +757,7 @@ public void testBlockFormAtLimitCancelsAndRestoresOld() { } @Test - public void testEntityBlockFormStateTransition() { + void testEntityBlockFormStateTransition() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); ibc.add(Environment.NORMAL, Material.COBBLESTONE.getKey()); listener.setIsland("test-island-id", ibc); @@ -780,7 +780,7 @@ public void testEntityBlockFormStateTransition() { // --- BlockGrowEvent tests --- @Test - public void testBlockGrowIncrementsNewAndRemovesOld() { + void testBlockGrowIncrementsNewAndRemovesOld() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); ibc.add(Environment.NORMAL, Material.DIRT.getKey()); listener.setIsland("test-island-id", ibc); @@ -799,7 +799,7 @@ public void testBlockGrowIncrementsNewAndRemovesOld() { } @Test - public void testBlockGrowAtLimitCancelsAndRestoresBlockData() { + void testBlockGrowAtLimitCancelsAndRestoresBlockData() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); ibc.add(Environment.NORMAL, Material.DIRT.getKey()); ibc.setBlockLimit(Environment.NORMAL, Material.GRASS_BLOCK.getKey(), 0); @@ -824,7 +824,7 @@ public void testBlockGrowAtLimitCancelsAndRestoresBlockData() { } @Test - public void testBlockGrowAtNonZeroLimitDoesNotDecrement() { + void testBlockGrowAtNonZeroLimitDoesNotDecrement() { // GRASS_BLOCK is at a non-zero limit (5 of 5). A blocked grow must NOT change the count. IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); ibc.setBlockLimit(Environment.NORMAL, Material.GRASS_BLOCK.getKey(), 5); @@ -852,7 +852,7 @@ public void testBlockGrowAtNonZeroLimitDoesNotDecrement() { // --- EntityChangeBlockEvent state transition tests --- @Test - public void testEntityChangeBlockToNonAirAddsNewRemovesOld() { + void testEntityChangeBlockToNonAirAddsNewRemovesOld() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); ibc.add(Environment.NORMAL, Material.DIRT.getKey()); listener.setIsland("test-island-id", ibc); @@ -872,7 +872,7 @@ public void testEntityChangeBlockToNonAirAddsNewRemovesOld() { } @Test - public void testEntityChangeBlockToNonAirAtLimitCancels() { + void testEntityChangeBlockToNonAirAtLimitCancels() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); ibc.add(Environment.NORMAL, Material.DIRT.getKey()); ibc.setBlockLimit(Environment.NORMAL, Material.COBBLESTONE.getKey(), 1); @@ -894,7 +894,7 @@ public void testEntityChangeBlockToNonAirAtLimitCancels() { } @Test - public void testEntityChangeBlockFarmlandRemovesCropAbove() { + void testEntityChangeBlockFarmlandRemovesCropAbove() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); ibc.add(Environment.NORMAL, Material.FARMLAND.getKey()); ibc.add(Environment.NORMAL, Material.OAK_PLANKS.getKey()); @@ -921,7 +921,7 @@ public void testEntityChangeBlockFarmlandRemovesCropAbove() { // --- Block cascade tests --- @Test - public void testBlockBreakSugarCaneCascade() { + void testBlockBreakSugarCaneCascade() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); ibc.add(Environment.NORMAL, Material.SUGAR_CANE.getKey()); ibc.add(Environment.NORMAL, Material.SUGAR_CANE.getKey()); @@ -949,7 +949,7 @@ public void testBlockBreakSugarCaneCascade() { } @Test - public void testBlockBreakBambooCascade() { + void testBlockBreakBambooCascade() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); ibc.add(Environment.NORMAL, Material.BAMBOO.getKey()); ibc.add(Environment.NORMAL, Material.BAMBOO.getKey()); @@ -975,7 +975,7 @@ public void testBlockBreakBambooCascade() { } @Test - public void testBlockBreakRedstoneOnTopRemoved() { + void testBlockBreakRedstoneOnTopRemoved() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); ibc.add(Environment.NORMAL, Material.STONE.getKey()); ibc.add(Environment.NORMAL, Material.REDSTONE_WIRE.getKey()); @@ -993,7 +993,7 @@ public void testBlockBreakRedstoneOnTopRemoved() { } @Test - public void testBlockBreakRedstoneWallTorchOnSideRemoved() { + void testBlockBreakRedstoneWallTorchOnSideRemoved() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); ibc.add(Environment.NORMAL, Material.STONE.getKey()); // fixMaterial normalises REDSTONE_WALL_TORCH → REDSTONE_TORCH @@ -1014,7 +1014,7 @@ public void testBlockBreakRedstoneWallTorchOnSideRemoved() { // --- Center block test --- @Test - public void testBlockPlaceCenterBlockIgnored() { + void testBlockPlaceCenterBlockIgnored() { // Make the block location equal to the island center when(island.getCenter()).thenReturn(blockLocation); @@ -1031,7 +1031,7 @@ public void testBlockPlaceCenterBlockIgnored() { // --- Turtle egg physical interaction test --- @Test - public void testTurtleEggPhysicalBreakDecrementsCount() { + void testTurtleEggPhysicalBreakDecrementsCount() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); ibc.add(Environment.NORMAL, Material.TURTLE_EGG.getKey()); listener.setIsland("test-island-id", ibc); @@ -1047,7 +1047,7 @@ public void testTurtleEggPhysicalBreakDecrementsCount() { // --- Explosion tests --- @Test - public void testBlockExplodeDecrementsBatch() { + void testBlockExplodeDecrementsBatch() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); ibc.add(Environment.NORMAL, Material.STONE.getKey()); ibc.add(Environment.NORMAL, Material.STONE.getKey()); @@ -1068,7 +1068,7 @@ public void testBlockExplodeDecrementsBatch() { } @Test - public void testEntityExplodeDecrementsBatch() { + void testEntityExplodeDecrementsBatch() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); ibc.add(Environment.NORMAL, Material.STONE.getKey()); ibc.add(Environment.NORMAL, Material.STONE.getKey()); @@ -1090,7 +1090,7 @@ public void testEntityExplodeDecrementsBatch() { // --- EntityChangeBlockEvent tests --- @Test - public void testEntityChangeBlockToAirDecrements() { + void testEntityChangeBlockToAirDecrements() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); ibc.add(Environment.NORMAL, Material.STONE.getKey()); listener.setIsland("test-island-id", ibc); @@ -1109,7 +1109,7 @@ public void testEntityChangeBlockToAirDecrements() { // --- BlockFromToEvent liquid destroys redstone test --- @Test - public void testBlockFromToLiquidDestroysRedstone() { + void testBlockFromToLiquidDestroysRedstone() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); ibc.add(Environment.NORMAL, Material.REDSTONE_WIRE.getKey()); listener.setIsland("test-island-id", ibc); @@ -1127,7 +1127,7 @@ public void testBlockFromToLiquidDestroysRedstone() { // --- Limit hierarchy tests --- @Test - public void testIslandLimitTakesPrecedenceOverWorldLimit() throws Exception { + void testIslandLimitTakesPrecedenceOverWorldLimit() throws Exception { // Set world limit for COBBLESTONE = 5 Field worldLimitField = BlockLimitsListener.class.getDeclaredField("worldLimitMap"); worldLimitField.setAccessible(true); @@ -1155,7 +1155,7 @@ public void testIslandLimitTakesPrecedenceOverWorldLimit() throws Exception { } @Test - public void testWorldLimitTakesPrecedenceOverDefaultLimit() throws Exception { + void testWorldLimitTakesPrecedenceOverDefaultLimit() throws Exception { // Set default limit for COBBLESTONE = 10 listener.getEnvDefaultLimitMap().get(Environment.NORMAL).put(Material.COBBLESTONE.getKey(), 10); @@ -1186,7 +1186,7 @@ public void testWorldLimitTakesPrecedenceOverDefaultLimit() throws Exception { } @Test - public void testDefaultLimitAppliedWhenNoIslandOrWorldLimit() { + void testDefaultLimitAppliedWhenNoIslandOrWorldLimit() { // Set default limit for COBBLESTONE = 2 listener.getEnvDefaultLimitMap().get(Environment.NORMAL).put(Material.COBBLESTONE.getKey(), 2); @@ -1206,7 +1206,7 @@ public void testDefaultLimitAppliedWhenNoIslandOrWorldLimit() { } @Test - public void testIslandOffsetIncreasesEffectiveLimit() { + void testIslandOffsetIncreasesEffectiveLimit() { // Set default limit for COBBLESTONE = 2 listener.getEnvDefaultLimitMap().get(Environment.NORMAL).put(Material.COBBLESTONE.getKey(), 2); @@ -1231,7 +1231,7 @@ public void testIslandOffsetIncreasesEffectiveLimit() { // --- Batch save tests --- @Test - public void testBatchSaveTriggersAfterThreshold() { + void testBatchSaveTriggersAfterThreshold() { // CHANGE_LIMIT = 9, so the 10th change triggers a save for (int i = 0; i < 10; i++) { Block block = mockBlock(Material.STONE, blockLocation); @@ -1245,7 +1245,7 @@ public void testBatchSaveTriggersAfterThreshold() { } @Test - public void testNoSaveBeforeThresholdReached() { + void testNoSaveBeforeThresholdReached() { // Fire 9 events (CHANGE_LIMIT = 9, save triggers when > 9, i.e. on 10th) for (int i = 0; i < 9; i++) { Block block = mockBlock(Material.STONE, blockLocation); @@ -1261,7 +1261,7 @@ public void testNoSaveBeforeThresholdReached() { // --- IslandDeleteEvent tests --- @Test - public void testIslandDeleteRemovesFromMaps() { + void testIslandDeleteRemovesFromMaps() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); listener.setIsland("test-island-id", ibc); assertNotNull(listener.getIsland("test-island-id")); @@ -1276,7 +1276,7 @@ public void testIslandDeleteRemovesFromMaps() { } @Test - public void testIslandDeleteCallsDatabaseDelete() { + void testIslandDeleteCallsDatabaseDelete() { IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock"); listener.setIsland("test-island-id", ibc); @@ -1295,7 +1295,7 @@ public void testIslandDeleteCallsDatabaseDelete() { // --- save tests --- @Test - public void testSaveSavesChangedIslands() { + void testSaveSavesChangedIslands() { String islandId = "save-test-island"; IslandBlockCount ibc = new IslandBlockCount(islandId, "BSkyBlock"); ibc.setChanged(true); From def4397d97d6ddc6ad27694251c2db388222411d Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 12 Jun 2026 08:39:03 -0700 Subject: [PATCH 19/24] Anchor golem/snowman block removal on the pumpkin, not the spawn block (#127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a player built an iron golem, detectIronGolem() assumed the CreatureSpawnEvent location was the BASE iron block and only searched upward. The built-mob spawn location is not guaranteed to be a specific block of the structure — when it is the body block, the old search matched nothing and removed only the spawn block, leaving the base, both arms and the pumpkin behind. Vanilla consumes those blocks, so Limits left a half-built golem's worth of blocks in the world (and uncounted). Anchor detection on the carved pumpkin / jack o'lantern instead, which is unambiguous: scan the spawn block and the two above it for the head, then verify and erase the body, base and arms relative to it. This works whether the spawn anchors on the base, body or head, and — unlike the old code, which unconditionally aired the spawn block — erases nothing unless a full valid pattern matches. Snowman detection is anchored the same way. Tests build a coordinate-keyed mock block grid and verify every structure block is removed for both spawn anchors, that a snowman is cleared, and that a lone iron block is left untouched. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../limits/listeners/EntityLimitListener.java | 101 ++++++++++-------- .../listeners/EntityLimitListenerTest.java | 101 ++++++++++++++++++ 2 files changed, 157 insertions(+), 45 deletions(-) diff --git a/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java b/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java index 62919e1..f870d5f 100644 --- a/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java +++ b/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java @@ -48,6 +48,8 @@ public class EntityLimitListener implements Listener { /** Cardinal directions used for block structure detection. */ private static final List CARDINALS = List.of(BlockFace.UP, BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST, BlockFace.DOWN); + /** One block face per horizontal axis, used to find golem arms regardless of facing. */ + private static final List HORIZONTAL = List.of(BlockFace.NORTH, BlockFace.EAST); public EntityLimitListener(Limits addon) { this.addon = addon; @@ -289,61 +291,70 @@ private void preSpawn(EntityType entityType, SpawnReason spawnReason, Location l } private void detectIronGolem(Location location) { - Block legs = location.getBlock(); - addon.getBlockLimitListener().removeBlock(legs); - legs.setType(Material.AIR); - for (BlockFace bf : CARDINALS) { - Block body = legs.getRelative(bf); - if (body.getType().equals(Material.IRON_BLOCK)) { - Block head = body.getRelative(bf); - if (isGolemHead(head) && eraseGolemIfArmsMatch(body, head, bf)) { - return; - } + // Anchor on the carved pumpkin: body and base are the two iron blocks below it, + // arms are two opposite iron blocks beside the body on a horizontal axis. + Block head = findPumpkinHead(location, Material.IRON_BLOCK); + if (head == null) { + return; + } + Block body = head.getRelative(BlockFace.DOWN); + Block base = body.getRelative(BlockFace.DOWN); + if (base.getType() != Material.IRON_BLOCK) { + return; + } + for (BlockFace bf : HORIZONTAL) { + Block arm1 = body.getRelative(bf); + Block arm2 = body.getRelative(bf.getOppositeFace()); + if (arm1.getType() == Material.IRON_BLOCK && arm2.getType() == Material.IRON_BLOCK) { + eraseBlocks(head, body, base, arm1, arm2); + return; } } } - private boolean isGolemHead(Block block) { - return block.getType() == Material.CARVED_PUMPKIN || block.getType() == Material.JACK_O_LANTERN; + private void detectSnowman(Location location) { + // Anchor on the carved pumpkin: the two snow blocks below it are the body and base. + Block head = findPumpkinHead(location, Material.SNOW_BLOCK); + if (head == null) { + return; + } + Block body = head.getRelative(BlockFace.DOWN); + Block base = body.getRelative(BlockFace.DOWN); + if (base.getType() != Material.SNOW_BLOCK) { + return; + } + eraseBlocks(head, body, base); } - private boolean eraseGolemIfArmsMatch(Block body, Block head, BlockFace bf) { - for (BlockFace bf2 : CARDINALS) { - Block arm1 = body.getRelative(bf2); - Block arm2 = body.getRelative(bf2.getOppositeFace()); - if (arm1.getType() == Material.IRON_BLOCK && arm2.getType() == Material.IRON_BLOCK - && arm1.getRelative(bf.getOppositeFace()).isEmpty() - && arm2.getRelative(bf.getOppositeFace()).isEmpty()) { - addon.getBlockLimitListener().removeBlock(body); - addon.getBlockLimitListener().removeBlock(arm1); - addon.getBlockLimitListener().removeBlock(arm2); - addon.getBlockLimitListener().removeBlock(head); - body.setType(Material.AIR); - arm1.setType(Material.AIR); - arm2.setType(Material.AIR); - head.setType(Material.AIR); - return true; + /** + * Locate the carved pumpkin / jack o'lantern that tops a freshly built golem or snowman. + * + *

The {@link CreatureSpawnEvent} location for a built mob is not guaranteed to be a + * particular block of the structure, so we anchor on the unambiguous pumpkin rather than + * assuming the spawn block is the base. Scan the spawn block and the two blocks above it + * for a head that sits directly on top of the expected body material. + * + * @return the pumpkin block, or {@code null} if none is found + */ + private Block findPumpkinHead(Location location, Material bodyMaterial) { + Block b = location.getBlock(); + for (int i = 0; i < 3; i++) { + if (isGolemHead(b) && b.getRelative(BlockFace.DOWN).getType() == bodyMaterial) { + return b; } + b = b.getRelative(BlockFace.UP); } - return false; + return null; } - private void detectSnowman(Location location) { - Block legs = location.getBlock(); - addon.getBlockLimitListener().removeBlock(legs); - legs.setType(Material.AIR); - for (BlockFace bf : CARDINALS) { - Block body = legs.getRelative(bf); - if (body.getType().equals(Material.SNOW_BLOCK)) { - Block head = body.getRelative(bf); - if (head.getType() == Material.CARVED_PUMPKIN || head.getType() == Material.JACK_O_LANTERN) { - addon.getBlockLimitListener().removeBlock(body); - addon.getBlockLimitListener().removeBlock(head); - body.setType(Material.AIR); - head.setType(Material.AIR); - return; - } - } + private boolean isGolemHead(Block block) { + return block.getType() == Material.CARVED_PUMPKIN || block.getType() == Material.JACK_O_LANTERN; + } + + private void eraseBlocks(Block... blocks) { + for (Block b : blocks) { + addon.getBlockLimitListener().removeBlock(b); + b.setType(Material.AIR); } } diff --git a/src/test/java/world/bentobox/limits/listeners/EntityLimitListenerTest.java b/src/test/java/world/bentobox/limits/listeners/EntityLimitListenerTest.java index 9182be9..4e51a7d 100644 --- a/src/test/java/world/bentobox/limits/listeners/EntityLimitListenerTest.java +++ b/src/test/java/world/bentobox/limits/listeners/EntityLimitListenerTest.java @@ -11,8 +11,11 @@ import static org.mockito.Mockito.when; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -579,4 +582,102 @@ private LivingEntity mockEntity(EntityType type, Location location) { return entity; } + // --- Golem / snowman block-removal tests (#127) --- + + @Test + public void testDetectIronGolemRemovesAllBlocksWhenSpawnedAtBody() throws Exception { + grid = new HashMap<>(); + Block base = setBlock(0, 64, 0, Material.IRON_BLOCK); + Block body = setBlock(0, 65, 0, Material.IRON_BLOCK); + Block head = setBlock(0, 66, 0, Material.CARVED_PUMPKIN); + Block arm1 = setBlock(0, 65, -1, Material.IRON_BLOCK); // NORTH + Block arm2 = setBlock(0, 65, 1, Material.IRON_BLOCK); // SOUTH + + // Vanilla can spawn the golem at the BODY block; the old base-anchored search missed + // everything except the spawn block, leaving the base, arms and pumpkin behind (#127). + invokeDetect("detectIronGolem", spawnAt(body)); + + for (Block b : List.of(base, body, head, arm1, arm2)) { + verify(bll).removeBlock(b); + verify(b).setType(Material.AIR); + } + } + + @Test + public void testDetectIronGolemRemovesAllBlocksWhenSpawnedAtBase() throws Exception { + grid = new HashMap<>(); + Block base = setBlock(0, 64, 0, Material.IRON_BLOCK); + Block body = setBlock(0, 65, 0, Material.IRON_BLOCK); + Block head = setBlock(0, 66, 0, Material.CARVED_PUMPKIN); + Block arm1 = setBlock(1, 65, 0, Material.IRON_BLOCK); // EAST + Block arm2 = setBlock(-1, 65, 0, Material.IRON_BLOCK); // WEST + + invokeDetect("detectIronGolem", spawnAt(base)); + + for (Block b : List.of(base, body, head, arm1, arm2)) { + verify(bll).removeBlock(b); + verify(b).setType(Material.AIR); + } + } + + @Test + public void testDetectSnowmanRemovesAllBlocks() throws Exception { + grid = new HashMap<>(); + Block base = setBlock(0, 64, 0, Material.SNOW_BLOCK); + Block body = setBlock(0, 65, 0, Material.SNOW_BLOCK); + Block head = setBlock(0, 66, 0, Material.JACK_O_LANTERN); + + invokeDetect("detectSnowman", spawnAt(body)); + + for (Block b : List.of(base, body, head)) { + verify(bll).removeBlock(b); + verify(b).setType(Material.AIR); + } + } + + @Test + public void testDetectIronGolemLeavesUnrelatedBlocksAlone() throws Exception { + grid = new HashMap<>(); + // A lone iron block with no pumpkin is not a golem — nothing may be erased. + Block stray = setBlock(0, 65, 0, Material.IRON_BLOCK); + + invokeDetect("detectIronGolem", spawnAt(stray)); + + verify(stray, never()).setType(Material.AIR); + verify(bll, never()).removeBlock(any(Block.class)); + } + + /** Coordinate-keyed mock blocks whose getRelative() walks the shared grid. */ + private Map, Block> grid; + + private Block gridBlock(int x, int y, int z) { + return grid.computeIfAbsent(List.of(x, y, z), k -> { + Block b = mock(Block.class); + when(b.getType()).thenReturn(Material.AIR); + when(b.getRelative(any(BlockFace.class))).thenAnswer(inv -> { + BlockFace f = inv.getArgument(0); + return gridBlock(x + f.getModX(), y + f.getModY(), z + f.getModZ()); + }); + return b; + }); + } + + private Block setBlock(int x, int y, int z, Material type) { + Block b = gridBlock(x, y, z); + when(b.getType()).thenReturn(type); + return b; + } + + private Location spawnAt(Block block) { + Location loc = mock(Location.class); + when(loc.getBlock()).thenReturn(block); + return loc; + } + + private void invokeDetect(String method, Location loc) throws Exception { + Method m = EntityLimitListener.class.getDeclaredMethod(method, Location.class); + m.setAccessible(true); + m.invoke(ell, loc); + } + } From 8a5e874e2b92453709ababd47b4cac3ab425384e Mon Sep 17 00:00:00 2001 From: daniel-skopek Date: Fri, 12 Jun 2026 19:20:37 +0200 Subject: [PATCH 20/24] Fix three entity counting bugs in EntityLimitListener MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix entity counts drifting above reality when mobs die off-island. trackSpawn counted the entity against the island where it spawned, but onEntityRemove used getIslandAt(entity.getLocation()) which returns empty when the entity wandered outside island bounds before dying or despawning. Added an entityIslandMap (UUID → islandId) so decrement always targets the correct island regardless of the entity's current location. onEntityPortal also updates the mapping for env-to-env transfers. A justPortaled debounce list prevents double-decrement if EntityRemoveEvent fires for the source-world removal during portal. - Fix HangingPlaceEvent race: change onBlock priority from MONITOR to LOW. Both onBlock (limit check) and onHangingPlaceTrack (tracking) were at MONITOR. Bukkit does not guarantee handler dispatch order within a single listener class, so trackSpawn could increment the entity count before the limit check had a chance to cancel the event, leaving a spurious +1 in the persistent count. - Fix processIsland undoing other plugins' event cancellations. cancelableEvent.setCancelled(false) was called unconditionally when the entity was not on a tracked island. Now guarded by runAsync so it only reverses this plugin's own pre-cancellation for async spawns (golems, withers). Other plugins' cancellations are left intact. --- .../limits/listeners/EntityLimitListener.java | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java b/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java index 62919e1..0c82cfe 100644 --- a/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java +++ b/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java @@ -45,6 +45,10 @@ public class EntityLimitListener implements Listener { private final Limits addon; /** Entity UUIDs that have just spawned to prevent double-processing. */ private final List justSpawned = new ArrayList<>(); + /** Entity UUIDs that are currently portaling to prevent double-decrement on cross-world removal. */ + private final List justPortaled = new ArrayList<>(); + /** Maps entity UUID to island ID so decrement works even when the entity dies off-island. */ + private final Map entityIslandMap = new HashMap<>(); /** Cardinal directions used for block structure detection. */ private static final List CARDINALS = List.of(BlockFace.UP, BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST, BlockFace.DOWN); @@ -118,7 +122,7 @@ public void onCreatureSpawn(final CreatureSpawnEvent creatureSpawnEvent) { } } - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) public void onBlock(HangingPlaceEvent hangingPlaceEvent) { if (!addon.inGameModeWorld(hangingPlaceEvent.getBlock().getWorld())) return; Player player = hangingPlaceEvent.getPlayer(); @@ -175,6 +179,7 @@ private void trackSpawn(Entity entity) { .ifPresent(island -> { IslandBlockCount ibc = addon.getBlockLimitListener().getIsland(island); ibc.incrementEntity(envOf(w), entity.getType()); + entityIslandMap.put(entity.getUniqueId(), island.getUniqueId()); }); } @@ -189,6 +194,19 @@ public void onEntityRemove(EntityRemoveEvent e) { Entity entity = e.getEntity(); World w = entity.getWorld(); if (!addon.inGameModeWorld(w)) return; + + // Entity is being portaled — count transfer was already handled by onEntityPortal. + if (justPortaled.remove(entity.getUniqueId())) return; + + String islandId = entityIslandMap.remove(entity.getUniqueId()); + if (islandId != null) { + IslandBlockCount ibc = addon.getBlockLimitListener().getIsland(islandId); + if (ibc != null) { + ibc.decrementEntity(envOf(w), entity.getType()); + } + return; + } + addon.getIslands().getIslandAt(entity.getLocation()) .filter(island -> !island.isSpawn()) .ifPresent(island -> { @@ -214,8 +232,12 @@ public void onEntityPortal(EntityPortalEvent e) { if (fromEnv == toEnv) return; if (!addon.inGameModeWorld(fromWorld) && !addon.inGameModeWorld(toWorld)) return; + // Prevent onEntityRemove from double-decrementing for the source-world removal. + justPortaled.add(entity.getUniqueId()); + // Decrement at source if on a tracked island if (addon.inGameModeWorld(fromWorld)) { + entityIslandMap.remove(entity.getUniqueId()); addon.getIslands().getIslandAt(entity.getLocation()) .filter(island -> !island.isSpawn()) .ifPresent(island -> { @@ -232,6 +254,7 @@ public void onEntityPortal(EntityPortalEvent e) { .ifPresent(island -> { IslandBlockCount ibc = addon.getBlockLimitListener().getIsland(island); ibc.incrementEntity(toEnv, entity.getType()); + entityIslandMap.put(entity.getUniqueId(), island.getUniqueId()); }); } } @@ -252,7 +275,9 @@ private boolean checkLimit(Cancellable cancelableEvent, LivingEntity livingEntit private boolean processIsland(Cancellable cancelableEvent, LivingEntity livingEntity, Location location, SpawnReason spawnReason, boolean runAsync) { if (addon.getIslands().getIslandAt(livingEntity.getLocation()).isEmpty()) { - cancelableEvent.setCancelled(false); + if (runAsync) { + cancelableEvent.setCancelled(false); + } return true; } Island island = addon.getIslands().getIslandAt(livingEntity.getLocation()).get(); From f78f97f3dc3307480dfeb6d7c4a2cc22d4db4341 Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 12 Jun 2026 17:05:44 -0700 Subject: [PATCH 21/24] Deny spawn-egg use before consumption when at the entity limit (#134) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the only entity-limit check for spawn eggs was on the resulting CreatureSpawnEvent, which is cancelled when over the limit — but by then the egg item has already been consumed, so players lost the egg and got no spawn (and no message). Reported for right-clicking a mob with a spawn egg. Add LOW-priority guards on PlayerInteractEvent (egg on a block) and PlayerInteractEntityEvent (egg on a mob) that resolve the egg's entity type, check the island's limit for that type up front, and cancel the interaction (keeping the egg) with the entity-limits.hit-limit message when at the limit. A spawn egg always produces its own type, so the egg material maps directly to the entity type being checked. Refactors atLimit(Island, Entity) to delegate to a new atLimit(Island, EntityType, Environment) overload so the limit can be checked without an entity instance. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../limits/listeners/EntityLimitListener.java | 91 ++++++++++++++++++- .../listeners/EntityLimitListenerTest.java | 75 +++++++++++++++ 2 files changed, 164 insertions(+), 2 deletions(-) diff --git a/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java b/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java index 38379e1..827c172 100644 --- a/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java +++ b/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java @@ -14,8 +14,12 @@ import org.bukkit.event.entity.EntityBreedEvent; import org.bukkit.event.entity.EntityPortalEvent; import org.bukkit.event.entity.EntityRemoveEvent; +import org.bukkit.event.block.Action; import org.bukkit.event.hanging.HangingPlaceEvent; +import org.bukkit.event.player.PlayerInteractEntityEvent; +import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.event.vehicle.VehicleCreateEvent; +import org.bukkit.inventory.ItemStack; import org.eclipse.jdt.annotation.Nullable; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.localization.TextVariables; @@ -153,6 +157,87 @@ public void onBlock(HangingPlaceEvent hangingPlaceEvent) { }); } + /** + * Deny spawn-egg use the moment a player would exceed an entity limit, before the egg is + * consumed. The {@link CreatureSpawnEvent} path already cancels the over-limit spawn, but by + * then the egg item has been used up; cancelling the interaction here keeps the egg (#134). + */ + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void onSpawnEggUseOnBlock(PlayerInteractEvent e) { + if (e.getAction() != Action.RIGHT_CLICK_BLOCK || e.getClickedBlock() == null) { + return; + } + EntityType type = spawnEggType(e.getItem()); + if (type != null) { + checkSpawnEggLimit(e, e.getPlayer(), e.getClickedBlock().getLocation(), type); + } + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void onSpawnEggUseOnEntity(PlayerInteractEntityEvent e) { + if (e.getHand() == null) { + return; + } + EntityType type = spawnEggType(e.getPlayer().getInventory().getItem(e.getHand())); + if (type != null) { + checkSpawnEggLimit(e, e.getPlayer(), e.getRightClicked().getLocation(), type); + } + } + + private void checkSpawnEggLimit(Cancellable event, Player player, Location location, EntityType type) { + if (!addon.inGameModeWorld(location.getWorld())) { + return; + } + addon.getIslands().getIslandAt(location).ifPresent(island -> { + boolean bypass = player.isOp() || player.hasPermission( + addon.getPlugin().getIWM().getPermissionPrefix(location.getWorld()) + MOD_BYPASS); + if (bypass || island.isSpawn()) { + return; + } + AtLimitResult res = atLimit(island, type, envOf(location.getWorld())); + if (res.hit()) { + event.setCancelled(true); + notifyEntityLimit(player, type, res); + } + }); + } + + /** + * Resolve the entity type a spawn-egg item would create, e.g. {@code PIG_SPAWN_EGG -> PIG}. + * + * @return the entity type, or {@code null} if the item is not a recognised spawn egg + */ + private EntityType spawnEggType(ItemStack item) { + if (item == null) { + return null; + } + String name = item.getType().name(); + if (!name.endsWith("_SPAWN_EGG")) { + return null; + } + try { + return EntityType.valueOf(name.substring(0, name.length() - "_SPAWN_EGG".length())); + } catch (IllegalArgumentException ex) { + return null; + } + } + + private void notifyEntityLimit(Player player, EntityType type, AtLimitResult res) { + if (res.getTypelimit() != null) { + User.getInstance(player).notify("entity-limits.hit-limit", "[entity]", + Util.prettifyText(type.toString()), TextVariables.NUMBER, + String.valueOf(res.getTypelimit().getValue())); + } else { + User.getInstance(player).notify("entity-limits.hit-limit", "[entity]", + res.getGrouplimit().getKey().getName() + " (" + + res.getGrouplimit().getKey().getTypes().stream() + .map(x -> Util.prettifyText(x.toString())) + .collect(Collectors.joining(", ")) + + ")", + TextVariables.NUMBER, String.valueOf(res.getGrouplimit().getValue())); + } + } + /* ========================================================================= * Increment handlers (MONITOR priority — count entities that actually spawned) * ========================================================================= */ @@ -473,8 +558,10 @@ private void tellPlayers(Location location, Entity entity, SpawnReason spawnReas * for the entity's current environment. */ AtLimitResult atLimit(Island island, Entity entity) { - Environment env = envOf(entity.getWorld()); - EntityType type = entity.getType(); + return atLimit(island, entity.getType(), envOf(entity.getWorld())); + } + + AtLimitResult atLimit(Island island, EntityType type, Environment env) { @Nullable IslandBlockCount ibc = addon.getBlockLimitListener().getIsland(island.getUniqueId()); diff --git a/src/test/java/world/bentobox/limits/listeners/EntityLimitListenerTest.java b/src/test/java/world/bentobox/limits/listeners/EntityLimitListenerTest.java index 4e51a7d..f23f41e 100644 --- a/src/test/java/world/bentobox/limits/listeners/EntityLimitListenerTest.java +++ b/src/test/java/world/bentobox/limits/listeners/EntityLimitListenerTest.java @@ -35,13 +35,18 @@ import org.bukkit.event.entity.CreatureSpawnEvent; import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason; import org.bukkit.event.entity.EntityBreedEvent; +import org.bukkit.event.block.Action; import org.bukkit.event.hanging.HangingPlaceEvent; +import org.bukkit.event.player.PlayerInteractEntityEvent; +import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.entity.Hanging; import org.bukkit.entity.Minecart; import org.bukkit.entity.Painting; import org.bukkit.entity.Villager; import org.bukkit.event.vehicle.VehicleCreateEvent; import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; import org.bukkit.Material; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -680,4 +685,74 @@ private void invokeDetect(String method, Location loc) throws Exception { m.invoke(ell, loc); } + // --- Spawn-egg interact guard tests (#134) --- + + @Test + public void testSpawnEggOnEntityAtLimitIsCancelled() { + ibc.setEntityLimit(Environment.NORMAL, EntityType.ENDERMAN, 4); // seeded count is 4 -> at limit + Player p = eggPlayer(Material.ENDERMAN_SPAWN_EGG, EquipmentSlot.HAND); + Entity clicked = mock(Entity.class); + when(clicked.getLocation()).thenReturn(location); + PlayerInteractEntityEvent e = new PlayerInteractEntityEvent(p, clicked, EquipmentSlot.HAND); + + ell.onSpawnEggUseOnEntity(e); + + // Cancelled before the egg is consumed, rather than only blocking the later spawn. + assertTrue(e.isCancelled()); + } + + @Test + public void testSpawnEggOnEntityUnderLimitNotCancelled() { + ibc.setEntityLimit(Environment.NORMAL, EntityType.ENDERMAN, 10); // count 4 < 10 + Player p = eggPlayer(Material.ENDERMAN_SPAWN_EGG, EquipmentSlot.HAND); + Entity clicked = mock(Entity.class); + when(clicked.getLocation()).thenReturn(location); + PlayerInteractEntityEvent e = new PlayerInteractEntityEvent(p, clicked, EquipmentSlot.HAND); + + ell.onSpawnEggUseOnEntity(e); + + assertFalse(e.isCancelled()); + } + + @Test + public void testNonSpawnEggOnEntityIgnored() { + ibc.setEntityLimit(Environment.NORMAL, EntityType.ENDERMAN, 4); + Player p = eggPlayer(Material.STICK, EquipmentSlot.HAND); + Entity clicked = mock(Entity.class); + when(clicked.getLocation()).thenReturn(location); + PlayerInteractEntityEvent e = new PlayerInteractEntityEvent(p, clicked, EquipmentSlot.HAND); + + ell.onSpawnEggUseOnEntity(e); + + assertFalse(e.isCancelled()); + } + + @Test + public void testSpawnEggOnBlockAtLimitIsCancelled() { + ibc.setEntityLimit(Environment.NORMAL, EntityType.ENDERMAN, 4); + Player p = mock(Player.class); + when(p.isOp()).thenReturn(false); + when(p.hasPermission(anyString())).thenReturn(false); + when(p.getUniqueId()).thenReturn(UUID.randomUUID()); + Block clicked = mock(Block.class); + when(clicked.getLocation()).thenReturn(location); + PlayerInteractEvent e = new PlayerInteractEvent(p, Action.RIGHT_CLICK_BLOCK, + new ItemStack(Material.ENDERMAN_SPAWN_EGG), clicked, BlockFace.UP); + + ell.onSpawnEggUseOnBlock(e); + + assertTrue(e.isCancelled()); + } + + private Player eggPlayer(Material item, EquipmentSlot hand) { + Player p = mock(Player.class); + when(p.isOp()).thenReturn(false); + when(p.hasPermission(anyString())).thenReturn(false); + when(p.getUniqueId()).thenReturn(UUID.randomUUID()); + PlayerInventory inv = mock(PlayerInventory.class); + when(p.getInventory()).thenReturn(inv); + when(inv.getItem(hand)).thenReturn(new ItemStack(item)); + return p; + } + } From 2a535e7ea4c26de2dae2ac2f634fada8e486f97b Mon Sep 17 00:00:00 2001 From: tastybento Date: Sat, 13 Jun 2026 08:49:05 -0700 Subject: [PATCH 22/24] Resolve 1.21.9 copper materials by name to support older servers fixMaterial() hard-referenced Material.COPPER_WALL_TORCH, COPPER_TORCH and the COPPER_*_CHEST family, which only exist on Minecraft 1.21.9+. On older servers (e.g. Paper 1.21.8) the JVM failed to link these static field references, throwing NoSuchFieldError on the first BlockFadeEvent. Resolve these constants by name via Guava's Enums.getIfPresent into static final Optionals at class load, and guard the fixMaterial branches on isPresent(). The same jar now links and runs on both 1.21.8 and 1.21.9+. Lookups happen once, not in the per-tick hot path. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../limits/listeners/BlockLimitsListener.java | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java b/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java index 890500b..47507d3 100644 --- a/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java +++ b/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java @@ -44,6 +44,9 @@ import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; +import com.google.common.base.Enums; +import com.google.common.base.Optional; + import world.bentobox.bentobox.api.events.island.IslandDeleteEvent; import world.bentobox.bentobox.api.localization.TextVariables; import world.bentobox.bentobox.api.user.User; @@ -65,6 +68,20 @@ public class BlockLimitsListener implements Listener { Material.NETHER_PORTAL.getKey()); private static final List STACKABLE = List.of(Material.SUGAR_CANE.getKey(), Material.BAMBOO.getKey()); + /* + * Materials added in Minecraft 1.21.9 ("Copper Age"). Resolved by name so the + * addon links and runs on older servers (< 1.21.9) where these constants are + * absent, instead of throwing NoSuchFieldError. Absent Optional == not on this server. + */ + private static final Optional COPPER_WALL_TORCH = Enums.getIfPresent(Material.class, "COPPER_WALL_TORCH"); + private static final Optional COPPER_TORCH = Enums.getIfPresent(Material.class, "COPPER_TORCH"); + private static final Optional COPPER_CHEST = Enums.getIfPresent(Material.class, "COPPER_CHEST"); + /** All weathered/waxed copper chest variants that normalise to {@link #COPPER_CHEST}. */ + private static final List COPPER_CHEST_VARIANTS = List.of("EXPOSED_COPPER_CHEST", "WEATHERED_COPPER_CHEST", + "OXIDIZED_COPPER_CHEST", "WAXED_COPPER_CHEST", "WAXED_EXPOSED_COPPER_CHEST", "WAXED_WEATHERED_COPPER_CHEST", + "WAXED_OXIDIZED_COPPER_CHEST").stream().map(name -> Enums.getIfPresent(Material.class, name).orNull()) + .filter(Objects::nonNull).toList(); + /** Save every 10 blocks of change */ private static final Integer CHANGE_LIMIT = 9; private final Limits addon; @@ -369,8 +386,8 @@ public NamespacedKey fixMaterial(BlockData b) { return Material.REDSTONE_TORCH.getKey(); } else if (mat == Material.WALL_TORCH) { return Material.TORCH.getKey(); - } else if (mat == Material.COPPER_WALL_TORCH) { - return Material.COPPER_TORCH.getKey(); + } else if (COPPER_WALL_TORCH.isPresent() && mat == COPPER_WALL_TORCH.get() && COPPER_TORCH.isPresent()) { + return COPPER_TORCH.get().getKey(); } else if (mat == Material.ZOMBIE_WALL_HEAD) { return Material.ZOMBIE_HEAD.getKey(); } else if (mat == Material.CREEPER_WALL_HEAD) { @@ -388,11 +405,8 @@ public NamespacedKey fixMaterial(BlockData b) { } else { return Material.STICKY_PISTON.getKey(); } - } else if (mat == Material.EXPOSED_COPPER_CHEST || mat == Material.WEATHERED_COPPER_CHEST - || mat == Material.OXIDIZED_COPPER_CHEST || mat == Material.WAXED_COPPER_CHEST - || mat == Material.WAXED_EXPOSED_COPPER_CHEST || mat == Material.WAXED_WEATHERED_COPPER_CHEST - || mat == Material.WAXED_OXIDIZED_COPPER_CHEST) { - return Material.COPPER_CHEST.getKey(); + } else if (COPPER_CHEST.isPresent() && COPPER_CHEST_VARIANTS.contains(mat)) { + return COPPER_CHEST.get().getKey(); } return mat.getKey(); } From 6e655bb2dbe310131cd6eab2f3987a9140553252 Mon Sep 17 00:00:00 2001 From: tastybento Date: Sat, 13 Jun 2026 09:10:26 -0700 Subject: [PATCH 23/24] Address SonarCloud issues across main and test sources Resolve the open issues reported on the new-code period: Main code: - BlockLimitsListener: replace Guava Optional copper fields with nullable Material (S4738); refactor fixMaterial to a lookup map and extract loadLimits helpers to cut cognitive complexity (S3776); lazy-supplier logging (S2629); drop unused handleBreak param (S1172). - EntityLimitListener: constants for duplicated literals (S1192); extract assignment from condition (S1121); remove always-false getHand() null check (S2583); single Optional resolve before get (S3655). - Stream.toList() over collect(toList()) (S6204) in Limits, RecountCalculator, OffsetCommand; rename shadowing locals (S1117); switch->if in CalcCommand/RecountCommand (S1301); CalcCommand island field -> local (S1450); drop unused OffsetCommand alias param (S1172); remove redundant lambda braces in JoinListener (S1602). Tests: - Drop redundant public modifier on 181 JUnit 5 methods (S5786). - Replace deprecated PlayerJoinEvent(String)/BlockMultiPlaceEvent ctors and PlayerInteractEvent.isCancelled() (S5738/S1874). - Parameterize the four invalid-perm-format tests (S5976); method reference (S1612); assertNotEquals (S5785); drop useless eq() matchers (S6068); remove unused imports/vars/throws/commented code and add a missing assertion (S1128/S1481/S1854/S1130/S125/S2699). All 243 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../java/world/bentobox/limits/Limits.java | 3 +- .../limits/calculators/RecountCalculator.java | 13 +- .../limits/commands/admin/CalcCommand.java | 10 +- .../limits/commands/admin/OffsetCommand.java | 15 +- .../commands/player/RecountCommand.java | 8 +- .../limits/listeners/BlockLimitsListener.java | 137 +++++++++-------- .../limits/listeners/EntityLimitListener.java | 28 ++-- .../limits/listeners/JoinListener.java | 21 ++- .../bentobox/limits/EntityGroupTest.java | 30 ++-- .../bentobox/limits/JoinListenerTest.java | 145 +++++++----------- .../bentobox/limits/LimitsPladdonTest.java | 11 +- .../world/bentobox/limits/LimitsTest.java | 38 +++-- .../world/bentobox/limits/SettingsTest.java | 28 ++-- .../limits/calculators/PipelinerTest.java | 2 +- .../limits/calculators/ResultsTest.java | 10 +- .../commands/player/LimitPanelTest.java | 9 +- .../limits/commands/player/LimitTabTest.java | 6 +- .../events/LimitsJoinPermCheckEventTest.java | 26 ++-- .../events/LimitsPermCheckEventTest.java | 22 +-- .../listeners/BlockLimitsListenerTest.java | 16 +- .../listeners/EntityLimitListenerTest.java | 68 ++++---- .../PaperShulkerLimitListenerTest.java | 3 +- .../limits/objects/EntityLimitsDOTest.java | 28 ++-- .../limits/objects/IslandBlockCountTest.java | 46 +++--- 24 files changed, 345 insertions(+), 378 deletions(-) diff --git a/src/main/java/world/bentobox/limits/Limits.java b/src/main/java/world/bentobox/limits/Limits.java index 988c74c..43f7b04 100644 --- a/src/main/java/world/bentobox/limits/Limits.java +++ b/src/main/java/world/bentobox/limits/Limits.java @@ -5,7 +5,6 @@ import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.stream.Collectors; import org.bukkit.Material; import org.bukkit.NamespacedKey; @@ -54,7 +53,7 @@ public void onEnable() { settings = new Settings(this); gameModes = getPlugin().getAddonsManager().getGameModeAddons().stream() .filter(gm -> settings.getGameModes().contains(gm.getDescription().getName())) - .collect(Collectors.toList()); + .toList(); gameModes.forEach(gm -> { gm.getAdminCommand().ifPresent(a -> new AdminCommand(this, a)); gm.getPlayerCommand().ifPresent(a -> new PlayerCommand(this, a)); diff --git a/src/main/java/world/bentobox/limits/calculators/RecountCalculator.java b/src/main/java/world/bentobox/limits/calculators/RecountCalculator.java index e094a63..fd14ae0 100644 --- a/src/main/java/world/bentobox/limits/calculators/RecountCalculator.java +++ b/src/main/java/world/bentobox/limits/calculators/RecountCalculator.java @@ -11,7 +11,6 @@ import java.util.concurrent.ConcurrentLinkedQueue; import java.util.function.BooleanSupplier; import java.util.function.LongSupplier; -import java.util.stream.Collectors; import org.bukkit.Bukkit; import org.bukkit.Chunk; @@ -120,12 +119,12 @@ public Results getResults() { private CompletableFuture> getWorldChunk(Environment env, Queue> pairList) { if (worlds.containsKey(env)) { - World world = worlds.get(env); - boolean isNether = world.getEnvironment().equals(Environment.NETHER); + World envWorld = worlds.get(env); + boolean isNether = envWorld.getEnvironment().equals(Environment.NETHER); List> futures = new ArrayList<>(); while (!pairList.isEmpty()) { Pair p = pairList.poll(); - futures.add(Util.getChunkAtAsync(world, p.x, p.z, isNether)); + futures.add(Util.getChunkAtAsync(envWorld, p.x, p.z, isNether)); } if (futures.isEmpty()) { return CompletableFuture.completedFuture(Collections.emptyList()); @@ -134,7 +133,7 @@ private CompletableFuture> getWorldChunk(Environment env, Queue futures.stream() .map(CompletableFuture::join) .filter(Objects::nonNull) - .collect(Collectors.toList())); + .toList()); } return CompletableFuture.completedFuture(Collections.emptyList()); } @@ -242,7 +241,7 @@ public void tidyUp() { } public void scanIsland(LongSupplier startTime, Runnable onRemove, BooleanSupplier isCancelled, Runnable recurse) { - scanNextChunk().thenAccept(r -> { + scanNextChunk().thenAccept(hasMoreChunks -> { if (!Bukkit.isPrimaryThread()) { addon.getPlugin().logError("scanChunk not on Primary Thread!"); } @@ -253,7 +252,7 @@ public void scanIsland(LongSupplier startTime, Runnable onRemove, BooleanSupplie + getIsland()); return; } - if (Boolean.TRUE.equals(r) && !isCancelled.getAsBoolean()) { + if (Boolean.TRUE.equals(hasMoreChunks) && !isCancelled.getAsBoolean()) { recurse.run(); } else { onRemove.run(); diff --git a/src/main/java/world/bentobox/limits/commands/admin/CalcCommand.java b/src/main/java/world/bentobox/limits/commands/admin/CalcCommand.java index fc53cf4..b20fc58 100644 --- a/src/main/java/world/bentobox/limits/commands/admin/CalcCommand.java +++ b/src/main/java/world/bentobox/limits/commands/admin/CalcCommand.java @@ -20,7 +20,6 @@ public class CalcCommand extends CompositeCommand { private final Limits addon; - private Island island; /** * Admin command @@ -54,7 +53,7 @@ public boolean execute(User user, String label, List args) { user.sendMessage("general.errors.unknown-player", args.get(0)); return true; } - island = addon.getIslands().getIsland(getWorld(), playerUUID); + Island island = addon.getIslands().getIsland(getWorld(), playerUUID); if (island == null) { user.sendMessage("general.errors.player-has-no-island"); return false; @@ -77,9 +76,10 @@ private void handlePipelineResult(User user, Results results) { if (results == null) { user.sendMessage("island.limits.recount.in-progress"); } else { - switch (results.getState()) { - case TIMEOUT -> user.sendMessage("admin.limits.calc.timeout"); - default -> user.sendMessage("admin.limits.calc.finished"); + if (results.getState() == Results.Result.TIMEOUT) { + user.sendMessage("admin.limits.calc.timeout"); + } else { + user.sendMessage("admin.limits.calc.finished"); } } } diff --git a/src/main/java/world/bentobox/limits/commands/admin/OffsetCommand.java b/src/main/java/world/bentobox/limits/commands/admin/OffsetCommand.java index b7ae60c..c173ef9 100644 --- a/src/main/java/world/bentobox/limits/commands/admin/OffsetCommand.java +++ b/src/main/java/world/bentobox/limits/commands/admin/OffsetCommand.java @@ -188,7 +188,7 @@ public boolean execute(User user, String label, List args) @Override public Optional> tabComplete(User user, String alias, List args) { - return OffsetCommand.craftTabComplete(user, alias, args); + return OffsetCommand.craftTabComplete(user, args); } @@ -305,7 +305,7 @@ public boolean execute(User user, String label, List args) @Override public Optional> tabComplete(User user, String alias, List args) { - return OffsetCommand.craftTabComplete(user, alias, args); + return OffsetCommand.craftTabComplete(user, args); } @@ -422,7 +422,7 @@ public boolean execute(User user, String label, List args) @Override public Optional> tabComplete(User user, String alias, List args) { - return OffsetCommand.craftTabComplete(user, alias, args); + return OffsetCommand.craftTabComplete(user, args); } @@ -524,7 +524,7 @@ public boolean execute(User user, String label, List args) @Override public Optional> tabComplete(User user, String alias, List args) { - return OffsetCommand.craftTabComplete(user, alias, args); + return OffsetCommand.craftTabComplete(user, args); } @@ -624,7 +624,7 @@ public boolean execute(User user, String label, List args) @Override public Optional> tabComplete(User user, String alias, List args) { - return OffsetCommand.craftTabComplete(user, alias, args); + return OffsetCommand.craftTabComplete(user, args); } @@ -660,11 +660,10 @@ private static EntityType matchEntity(String name) /** * This method crafts tab complete for all subcommands * @param user User who runs command. - * @param alias Command alias. * @param args List of args. * @return Optional list of strings. */ - private static Optional> craftTabComplete(User user, String alias, List args) + private static Optional> craftTabComplete(User user, List args) { String lastArg = !args.isEmpty() ? args.get(args.size() - 1) : ""; @@ -686,7 +685,7 @@ else if (args.size() == 5) options.addAll(Arrays.stream(EntityType.values()). map(Enum::name). - collect(Collectors.toList())); + toList()); return Optional.of(Util.tabLimit(options, lastArg)); } diff --git a/src/main/java/world/bentobox/limits/commands/player/RecountCommand.java b/src/main/java/world/bentobox/limits/commands/player/RecountCommand.java index 4b466b4..e75d800 100644 --- a/src/main/java/world/bentobox/limits/commands/player/RecountCommand.java +++ b/src/main/java/world/bentobox/limits/commands/player/RecountCommand.java @@ -9,6 +9,7 @@ import world.bentobox.bentobox.database.objects.Island; import world.bentobox.limits.Limits; import world.bentobox.limits.calculators.Pipeliner; +import world.bentobox.limits.calculators.Results; /** * @@ -65,9 +66,10 @@ public boolean execute(User user, String label, List args) { if (results == null) { user.sendMessage("island.limits.recount.in-progress"); } else { - switch (results.getState()) { - case TIMEOUT -> user.sendMessage("admin.limits.calc.timeout"); - default -> user.sendMessage("admin.limits.calc.finished"); + if (results.getState() == Results.Result.TIMEOUT) { + user.sendMessage("admin.limits.calc.timeout"); + } else { + user.sendMessage("admin.limits.calc.finished"); } } }); diff --git a/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java b/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java index 47507d3..254f279 100644 --- a/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java +++ b/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java @@ -21,7 +21,6 @@ import org.bukkit.block.data.type.TechnicalPiston; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.event.Cancellable; -import org.bukkit.event.Event; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; @@ -45,7 +44,6 @@ import org.eclipse.jdt.annotation.Nullable; import com.google.common.base.Enums; -import com.google.common.base.Optional; import world.bentobox.bentobox.api.events.island.IslandDeleteEvent; import world.bentobox.bentobox.api.localization.TextVariables; @@ -73,15 +71,42 @@ public class BlockLimitsListener implements Listener { * addon links and runs on older servers (< 1.21.9) where these constants are * absent, instead of throwing NoSuchFieldError. Absent Optional == not on this server. */ - private static final Optional COPPER_WALL_TORCH = Enums.getIfPresent(Material.class, "COPPER_WALL_TORCH"); - private static final Optional COPPER_TORCH = Enums.getIfPresent(Material.class, "COPPER_TORCH"); - private static final Optional COPPER_CHEST = Enums.getIfPresent(Material.class, "COPPER_CHEST"); + @Nullable + private static final Material COPPER_WALL_TORCH = Enums.getIfPresent(Material.class, "COPPER_WALL_TORCH").orNull(); + @Nullable + private static final Material COPPER_TORCH = Enums.getIfPresent(Material.class, "COPPER_TORCH").orNull(); + @Nullable + private static final Material COPPER_CHEST = Enums.getIfPresent(Material.class, "COPPER_CHEST").orNull(); /** All weathered/waxed copper chest variants that normalise to {@link #COPPER_CHEST}. */ private static final List COPPER_CHEST_VARIANTS = List.of("EXPOSED_COPPER_CHEST", "WEATHERED_COPPER_CHEST", "OXIDIZED_COPPER_CHEST", "WAXED_COPPER_CHEST", "WAXED_EXPOSED_COPPER_CHEST", "WAXED_WEATHERED_COPPER_CHEST", "WAXED_OXIDIZED_COPPER_CHEST").stream().map(name -> Enums.getIfPresent(Material.class, name).orNull()) .filter(Objects::nonNull).toList(); + /** + * Variant block materials mapped to the canonical material we count them as. + * Pistons are handled separately because the canonical form depends on block data. + */ + private static final Map VARIANT_MAP = new EnumMap<>(Material.class); + static { + VARIANT_MAP.put(Material.CHIPPED_ANVIL, Material.ANVIL); + VARIANT_MAP.put(Material.DAMAGED_ANVIL, Material.ANVIL); + VARIANT_MAP.put(Material.REDSTONE_WALL_TORCH, Material.REDSTONE_TORCH); + VARIANT_MAP.put(Material.WALL_TORCH, Material.TORCH); + VARIANT_MAP.put(Material.ZOMBIE_WALL_HEAD, Material.ZOMBIE_HEAD); + VARIANT_MAP.put(Material.CREEPER_WALL_HEAD, Material.CREEPER_HEAD); + VARIANT_MAP.put(Material.PLAYER_WALL_HEAD, Material.PLAYER_HEAD); + VARIANT_MAP.put(Material.DRAGON_WALL_HEAD, Material.DRAGON_HEAD); + VARIANT_MAP.put(Material.BAMBOO_SAPLING, Material.BAMBOO); + // 1.21.9 materials: only mapped when present on this server + if (COPPER_WALL_TORCH != null && COPPER_TORCH != null) { + VARIANT_MAP.put(COPPER_WALL_TORCH, COPPER_TORCH); + } + if (COPPER_CHEST != null) { + COPPER_CHEST_VARIANTS.forEach(variant -> VARIANT_MAP.put(variant, COPPER_CHEST)); + } + } + /** Save every 10 blocks of change */ private static final Integer CHANGE_LIMIT = 9; private final Limits addon; @@ -159,41 +184,45 @@ private void loadEnvSection(String section, Environment env) { private Map loadLimits(ConfigurationSection cs) { Map limits = new HashMap<>(); for (String key : cs.getKeys(false)) { - int limit = cs.getInt(key); - NamespacedKey nsKey; - if (key.contains(":")) { - nsKey = NamespacedKey.fromString(key.toLowerCase(Locale.ROOT)); - if (nsKey == null) { - Bukkit.getLogger().warning("Invalid namespaced key in config, skipping: " + key); - continue; - } - } else { - nsKey = new NamespacedKey(NamespacedKey.MINECRAFT, key.toLowerCase(Locale.ROOT)); + NamespacedKey nsKey = parseConfigKey(key); + if (nsKey != null) { + registerLimit(limits, nsKey, key, cs.getInt(key)); } - boolean matched = false; - Material mat = Registry.MATERIAL.get(nsKey); - if (mat != null) { - matched = true; - if (!mat.isBlock()) { - Bukkit.getLogger().warning("Non-block material in block limits config: " + key); - } else if (DO_NOT_COUNT.contains(mat.getKey())) { - Bukkit.getLogger().warning("Uncountable material in block limits config: " + key); - } else { - limits.put(mat.getKey(), limit); - } - } - if (!matched) { - Tag tag = Bukkit.getTag("blocks", nsKey, Material.class); - if (tag != null) { - limits.put(tag.getKey(), limit); - matched = true; - } + } + return limits; + } + + /** Parse a config key into a NamespacedKey, or null if it is an invalid namespaced key. */ + private NamespacedKey parseConfigKey(String key) { + if (key.contains(":")) { + NamespacedKey nsKey = NamespacedKey.fromString(key.toLowerCase(Locale.ROOT)); + if (nsKey == null) { + Bukkit.getLogger().warning(() -> "Invalid namespaced key in config, skipping: " + key); } - if (!matched) { - Bukkit.getLogger().warning("Unknown material or tag in config: " + key); + return nsKey; + } + return new NamespacedKey(NamespacedKey.MINECRAFT, key.toLowerCase(Locale.ROOT)); + } + + /** Resolve a key to a material or block tag and store its limit, warning on any mismatch. */ + private void registerLimit(Map limits, NamespacedKey nsKey, String key, int limit) { + Material mat = Registry.MATERIAL.get(nsKey); + if (mat != null) { + if (!mat.isBlock()) { + Bukkit.getLogger().warning(() -> "Non-block material in block limits config: " + key); + } else if (DO_NOT_COUNT.contains(mat.getKey())) { + Bukkit.getLogger().warning(() -> "Uncountable material in block limits config: " + key); + } else { + limits.put(mat.getKey(), limit); } + return; } - return limits; + Tag tag = Bukkit.getTag("blocks", nsKey, Material.class); + if (tag != null) { + limits.put(tag.getKey(), limit); + return; + } + Bukkit.getLogger().warning(() -> "Unknown material or tag in config: " + key); } /** Save the count database completely */ @@ -223,18 +252,18 @@ public void onBlock(BlockBreakEvent e) { if (e.getBlock().hasMetadata("blockbreakevent-ignore")) { return; } - handleBreak(e, e.getBlock()); + handleBreak(e.getBlock()); } @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onTurtleEggBreak(PlayerInteractEvent e) { if (e.getAction().equals(Action.PHYSICAL) && e.getClickedBlock() != null && e.getClickedBlock().getType().equals(Material.TURTLE_EGG)) { - handleBreak(e, e.getClickedBlock()); + handleBreak(e.getClickedBlock()); } } - private void handleBreak(Event e, Block b) { + private void handleBreak(Block b) { if (!addon.inGameModeWorld(b.getWorld())) { return; } @@ -380,35 +409,11 @@ public void onBlock(BlockFromToEvent e) { */ public NamespacedKey fixMaterial(BlockData b) { Material mat = b.getMaterial(); - if (mat.equals(Material.CHIPPED_ANVIL) || mat.equals(Material.DAMAGED_ANVIL)) { - return Material.ANVIL.getKey(); - } else if (mat == Material.REDSTONE_WALL_TORCH) { - return Material.REDSTONE_TORCH.getKey(); - } else if (mat == Material.WALL_TORCH) { - return Material.TORCH.getKey(); - } else if (COPPER_WALL_TORCH.isPresent() && mat == COPPER_WALL_TORCH.get() && COPPER_TORCH.isPresent()) { - return COPPER_TORCH.get().getKey(); - } else if (mat == Material.ZOMBIE_WALL_HEAD) { - return Material.ZOMBIE_HEAD.getKey(); - } else if (mat == Material.CREEPER_WALL_HEAD) { - return Material.CREEPER_HEAD.getKey(); - } else if (mat == Material.PLAYER_WALL_HEAD) { - return Material.PLAYER_HEAD.getKey(); - } else if (mat == Material.DRAGON_WALL_HEAD) { - return Material.DRAGON_HEAD.getKey(); - } else if (mat == Material.BAMBOO_SAPLING) { - return Material.BAMBOO.getKey(); - } else if (mat == Material.PISTON_HEAD || mat == Material.MOVING_PISTON) { + if (mat == Material.PISTON_HEAD || mat == Material.MOVING_PISTON) { TechnicalPiston tp = (TechnicalPiston) b; - if (tp.getType() == TechnicalPiston.Type.NORMAL) { - return Material.PISTON.getKey(); - } else { - return Material.STICKY_PISTON.getKey(); - } - } else if (COPPER_CHEST.isPresent() && COPPER_CHEST_VARIANTS.contains(mat)) { - return COPPER_CHEST.get().getKey(); + return (tp.getType() == TechnicalPiston.Type.NORMAL ? Material.PISTON : Material.STICKY_PISTON).getKey(); } - return mat.getKey(); + return VARIANT_MAP.getOrDefault(mat, mat).getKey(); } private int process(Block b, boolean add) { diff --git a/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java b/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java index 827c172..25f02f0 100644 --- a/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java +++ b/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java @@ -46,6 +46,10 @@ public class EntityLimitListener implements Listener { /** Permission node for bypassing limits. */ private static final String MOD_BYPASS = "mod.bypass"; + /** Locale reference for the entity limit notification. */ + private static final String ENTITY_LIMIT_HIT = "entity-limits.hit-limit"; + /** Notification placeholder for the entity name. */ + private static final String ENTITY_PLACEHOLDER = "[entity]"; private final Limits addon; /** Entity UUIDs that have just spawned to prevent double-processing. */ private final List justSpawned = new ArrayList<>(); @@ -137,8 +141,11 @@ public void onBlock(HangingPlaceEvent hangingPlaceEvent) { boolean bypass = Objects.requireNonNull(player).isOp() || player.hasPermission( addon.getPlugin().getIWM().getPermissionPrefix(hangingPlaceEvent.getEntity().getWorld()) + MOD_BYPASS); - AtLimitResult res; - if (!bypass && !island.isSpawn() && (res = atLimit(island, hangingPlaceEvent.getEntity())).hit()) { + if (bypass || island.isSpawn()) { + return; + } + AtLimitResult res = atLimit(island, hangingPlaceEvent.getEntity()); + if (res.hit()) { hangingPlaceEvent.setCancelled(true); if (res.getTypelimit() != null) { User.getInstance(player).notify("block-limits.hit-limit", "[material]", @@ -175,9 +182,7 @@ public void onSpawnEggUseOnBlock(PlayerInteractEvent e) { @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) public void onSpawnEggUseOnEntity(PlayerInteractEntityEvent e) { - if (e.getHand() == null) { - return; - } + // PlayerInteractEntityEvent.getHand() is never null (always HAND or OFF_HAND) EntityType type = spawnEggType(e.getPlayer().getInventory().getItem(e.getHand())); if (type != null) { checkSpawnEggLimit(e, e.getPlayer(), e.getRightClicked().getLocation(), type); @@ -224,11 +229,11 @@ private EntityType spawnEggType(ItemStack item) { private void notifyEntityLimit(Player player, EntityType type, AtLimitResult res) { if (res.getTypelimit() != null) { - User.getInstance(player).notify("entity-limits.hit-limit", "[entity]", + User.getInstance(player).notify(ENTITY_LIMIT_HIT, ENTITY_PLACEHOLDER, Util.prettifyText(type.toString()), TextVariables.NUMBER, String.valueOf(res.getTypelimit().getValue())); } else { - User.getInstance(player).notify("entity-limits.hit-limit", "[entity]", + User.getInstance(player).notify(ENTITY_LIMIT_HIT, ENTITY_PLACEHOLDER, res.getGrouplimit().getKey().getName() + " (" + res.getGrouplimit().getKey().getTypes().stream() .map(x -> Util.prettifyText(x.toString())) @@ -361,13 +366,14 @@ private boolean checkLimit(Cancellable cancelableEvent, LivingEntity livingEntit private boolean processIsland(Cancellable cancelableEvent, LivingEntity livingEntity, Location location, SpawnReason spawnReason, boolean runAsync) { - if (addon.getIslands().getIslandAt(livingEntity.getLocation()).isEmpty()) { + Optional optionalIsland = addon.getIslands().getIslandAt(livingEntity.getLocation()); + if (optionalIsland.isEmpty()) { if (runAsync) { cancelableEvent.setCancelled(false); } return true; } - Island island = addon.getIslands().getIslandAt(livingEntity.getLocation()).get(); + Island island = optionalIsland.get(); AtLimitResult res = atLimit(island, livingEntity); if (island.isSpawn() || !res.hit()) { if (runAsync) { @@ -536,11 +542,11 @@ private void tellPlayers(Location location, Entity entity, SpawnReason spawnReas if (ent instanceof Player p) { p.updateInventory(); if (res.getTypelimit() != null) { - User.getInstance(p).notify("entity-limits.hit-limit", "[entity]", + User.getInstance(p).notify(ENTITY_LIMIT_HIT, ENTITY_PLACEHOLDER, Util.prettifyText(entity.getType().toString()), TextVariables.NUMBER, String.valueOf(res.getTypelimit().getValue())); } else { - User.getInstance(p).notify("entity-limits.hit-limit", "[entity]", + User.getInstance(p).notify(ENTITY_LIMIT_HIT, ENTITY_PLACEHOLDER, res.getGrouplimit().getKey().getName() + " (" + res.getGrouplimit().getKey().getTypes().stream() .map(x -> Util.prettifyText(x.toString())) diff --git a/src/main/java/world/bentobox/limits/listeners/JoinListener.java b/src/main/java/world/bentobox/limits/listeners/JoinListener.java index f1e90c7..1fcafa2 100644 --- a/src/main/java/world/bentobox/limits/listeners/JoinListener.java +++ b/src/main/java/world/bentobox/limits/listeners/JoinListener.java @@ -251,17 +251,16 @@ public void onOwnerChange(TeamSetownerEvent event) { @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onPlayerJoin(PlayerJoinEvent event) { - addon.getGameModes().forEach(gameMode -> { - addon.getIslands().getIslands(gameMode.getOverWorld(), event.getPlayer().getUniqueId()).stream() - .filter(island -> event.getPlayer().getUniqueId().equals(island.getOwner())) - .map(Island::getUniqueId).forEach(islandId -> { - IslandBlockCount islandBlockCount = addon.getBlockLimitListener().getIsland(islandId); - if (!joinEventCheck(event.getPlayer(), islandId, islandBlockCount)) { - checkPerms(event.getPlayer(), gameMode.getPermissionPrefix() + "island.limit.", islandId, - gameMode.getDescription().getName()); - } - }); - }); + addon.getGameModes().forEach(gameMode -> addon.getIslands() + .getIslands(gameMode.getOverWorld(), event.getPlayer().getUniqueId()).stream() + .filter(island -> event.getPlayer().getUniqueId().equals(island.getOwner())) + .map(Island::getUniqueId).forEach(islandId -> { + IslandBlockCount islandBlockCount = addon.getBlockLimitListener().getIsland(islandId); + if (!joinEventCheck(event.getPlayer(), islandId, islandBlockCount)) { + checkPerms(event.getPlayer(), gameMode.getPermissionPrefix() + "island.limit.", islandId, + gameMode.getDescription().getName()); + } + })); } private boolean joinEventCheck(Player player, String islandId, IslandBlockCount islandBlockCount) { diff --git a/src/test/java/world/bentobox/limits/EntityGroupTest.java b/src/test/java/world/bentobox/limits/EntityGroupTest.java index d93e81d..d44b5ee 100644 --- a/src/test/java/world/bentobox/limits/EntityGroupTest.java +++ b/src/test/java/world/bentobox/limits/EntityGroupTest.java @@ -16,85 +16,85 @@ import org.mockbukkit.mockbukkit.MockBukkit; -public class EntityGroupTest { +class EntityGroupTest { private EntityGroup group; @BeforeEach - public void setUp() { + void setUp() { MockBukkit.mock(); group = new EntityGroup("monsters", Set.of(EntityType.ZOMBIE, EntityType.SKELETON), 10, Material.ZOMBIE_HEAD); } @AfterEach - public void tearDown() { + void tearDown() { MockBukkit.unmock(); } @Test - public void testConstructorAndGetters() { + void testConstructorAndGetters() { assertEquals("monsters", group.getName()); assertEquals(Set.of(EntityType.ZOMBIE, EntityType.SKELETON), group.getTypes()); assertEquals(10, group.getLimit()); } @Test - public void testContainsMember() { + void testContainsMember() { assertTrue(group.contains(EntityType.ZOMBIE)); } @Test - public void testContainsNonMember() { + void testContainsNonMember() { assertFalse(group.contains(EntityType.CREEPER)); } @Test - public void testGetIconWhenSet() { + void testGetIconWhenSet() { assertEquals(Material.ZOMBIE_HEAD, group.getIcon()); } @Test - public void testGetIconWhenNull() { + void testGetIconWhenNull() { EntityGroup noIcon = new EntityGroup("passive", Set.of(EntityType.COW), 5, null); assertEquals(Material.BARRIER, noIcon.getIcon()); } @Test - public void testEqualsSameName() { + void testEqualsSameName() { EntityGroup other = new EntityGroup("monsters", Set.of(EntityType.CREEPER), 20, Material.STONE); assertEquals(group, other); } @Test - public void testEqualsDifferentName() { + void testEqualsDifferentName() { EntityGroup other = new EntityGroup("animals", Set.of(EntityType.ZOMBIE), 10, Material.ZOMBIE_HEAD); assertNotEquals(group, other); } @Test - public void testEqualsNull() { + void testEqualsNull() { assertNotEquals(null, group); } @Test - public void testEqualsDifferentClass() { + void testEqualsDifferentClass() { assertNotEquals("monsters", group); } @Test - public void testHashCodeConsistent() { + void testHashCodeConsistent() { EntityGroup other = new EntityGroup("monsters", Set.of(EntityType.CREEPER), 20, Material.STONE); assertEquals(group.hashCode(), other.hashCode()); } @Test - public void testHashCodeDifferent() { + void testHashCodeDifferent() { EntityGroup other = new EntityGroup("animals", Set.of(EntityType.ZOMBIE), 10, Material.ZOMBIE_HEAD); assertNotEquals(group.hashCode(), other.hashCode()); } @Test - public void testToStringContainsName() { + void testToStringContainsName() { String str = group.toString(); assertNotNull(str); assertTrue(str.contains("monsters")); diff --git a/src/test/java/world/bentobox/limits/JoinListenerTest.java b/src/test/java/world/bentobox/limits/JoinListenerTest.java index c1b5898..96e8049 100644 --- a/src/test/java/world/bentobox/limits/JoinListenerTest.java +++ b/src/test/java/world/bentobox/limits/JoinListenerTest.java @@ -17,6 +17,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Stream; import java.util.UUID; import net.kyori.adventure.text.Component; @@ -35,6 +36,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.Mockito; @@ -59,7 +63,7 @@ */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) -public class JoinListenerTest { +class JoinListenerTest { @Mock private Limits addon; @@ -88,7 +92,7 @@ public class JoinListenerTest { private MockedStatic mockedBukkit; @BeforeEach - public void setUp() { + void setUp() { MockBukkit.mock(); jl = new JoinListener(addon); // Setup addon @@ -126,12 +130,12 @@ public void setUp() { when(owner.isOnline()).thenReturn(true); when(owner.getPlayer()).thenReturn(player); mockedBukkit.when(() -> Bukkit.getOfflinePlayer(any(UUID.class))).thenReturn(owner); - mockedBukkit.when(() -> Bukkit.getPluginManager()).thenReturn(pim); + mockedBukkit.when(Bukkit::getPluginManager).thenReturn(pim); } @AfterEach - public void tearDown() { + void tearDown() { mockedBukkit.close(); MockBukkit.unmock(); } @@ -141,7 +145,7 @@ public void tearDown() { * {@link world.bentobox.limits.listeners.JoinListener#onNewIsland(world.bentobox.bentobox.api.events.island.IslandEvent)}. */ @Test - public void testOnNewIslandWrongReason() { + void testOnNewIslandWrongReason() { IslandEvent e = new IslandEvent(island, null, false, null, IslandEvent.Reason.BAN); jl.onNewIsland(e); verify(island, never()).getWorld(); @@ -152,7 +156,7 @@ public void testOnNewIslandWrongReason() { * {@link world.bentobox.limits.listeners.JoinListener#onNewIsland(world.bentobox.bentobox.api.events.island.IslandEvent)}. */ @Test - public void testOnNewIslandRegistered() { + void testOnNewIslandRegistered() { IslandEvent e = new IslandEvent(island, null, false, null, IslandEvent.Reason.REGISTERED); jl.onNewIsland(e); verify(island).getWorld(); @@ -163,7 +167,7 @@ public void testOnNewIslandRegistered() { * {@link world.bentobox.limits.listeners.JoinListener#onNewIsland(world.bentobox.bentobox.api.events.island.IslandEvent)}. */ @Test - public void testOnNewIslandResetted() { + void testOnNewIslandResetted() { IslandEvent e = new IslandEvent(island, null, false, null, IslandEvent.Reason.RESETTED); jl.onNewIsland(e); verify(island).getWorld(); @@ -173,7 +177,7 @@ public void testOnNewIslandResetted() { * Test method for {@link world.bentobox.limits.listeners.JoinListener#onNewIsland(world.bentobox.bentobox.api.events.island.IslandEvent)}. */ @Test - public void testOnNewIslandCreated() { + void testOnNewIslandCreated() { when(addon.inGameModeWorld(any())).thenReturn(true); IslandEvent e = new IslandEvent(island, null, false, null, IslandEvent.Reason.CREATED); jl.onNewIsland(e); @@ -185,7 +189,7 @@ public void testOnNewIslandCreated() { * Test method for {@link world.bentobox.limits.listeners.JoinListener#onNewIsland(world.bentobox.bentobox.api.events.island.IslandEvent)}. */ @Test - public void testOnNewIslandCreatedOffline() { + void testOnNewIslandCreatedOffline() { when(owner.isOnline()).thenReturn(false); when(addon.inGameModeWorld(any())).thenReturn(true); IslandEvent e = new IslandEvent(island, null, false, null, IslandEvent.Reason.CREATED); @@ -198,7 +202,7 @@ public void testOnNewIslandCreatedOffline() { * Test method for {@link world.bentobox.limits.listeners.JoinListener#onNewIsland(world.bentobox.bentobox.api.events.island.IslandEvent)}. */ @Test - public void testOnNewIslandCreatedNoNameOrPermPrefix() { + void testOnNewIslandCreatedNoNameOrPermPrefix() { when(addon.getGameModeName(any())).thenReturn(""); when(addon.getGameModePermPrefix(any())).thenReturn("bskyblock."); when(addon.inGameModeWorld(any())).thenReturn(true); @@ -215,7 +219,7 @@ public void testOnNewIslandCreatedNoNameOrPermPrefix() { * {@link world.bentobox.limits.listeners.JoinListener#onOwnerChange(world.bentobox.bentobox.api.events.team.TeamEvent.TeamSetownerEvent)}. */ @Test - public void testOnOwnerChange() { + void testOnOwnerChange() { TeamSetownerEvent e = mock(TeamSetownerEvent.class); when(e.getIsland()).thenReturn(island); when(e.getNewOwner()).thenReturn(UUID.randomUUID()); @@ -229,8 +233,8 @@ public void testOnOwnerChange() { * {@link world.bentobox.limits.listeners.JoinListener#onPlayerJoin(org.bukkit.event.player.PlayerJoinEvent)}. */ @Test - public void testOnPlayerJoin() { - PlayerJoinEvent e = new PlayerJoinEvent(player, "welcome"); + void testOnPlayerJoin() { + PlayerJoinEvent e = new PlayerJoinEvent(player, Component.text("welcome")); jl.onPlayerJoin(e); verify(addon).getGameModes(); verify(bll).setIsland("unique_id", ibc); @@ -241,9 +245,9 @@ public void testOnPlayerJoin() { * {@link world.bentobox.limits.listeners.JoinListener#onPlayerJoin(org.bukkit.event.player.PlayerJoinEvent)}. */ @Test - public void testOnPlayerJoinIBCNull() { + void testOnPlayerJoinIBCNull() { ibc = null; - PlayerJoinEvent e = new PlayerJoinEvent(player, "welcome"); + PlayerJoinEvent e = new PlayerJoinEvent(player, Component.text("welcome")); jl.onPlayerJoin(e); verify(addon).getGameModes(); verify(bll, never()).setIsland("unique_id", ibc); @@ -254,88 +258,47 @@ public void testOnPlayerJoinIBCNull() { * {@link world.bentobox.limits.listeners.JoinListener#onPlayerJoin(org.bukkit.event.player.PlayerJoinEvent)}. */ @Test - public void testOnPlayerJoinWithPermNotLimits() { + void testOnPlayerJoinWithPermNotLimits() { Set perms = new HashSet<>(); PermissionAttachmentInfo permAtt = mock(PermissionAttachmentInfo.class); when(permAtt.getPermission()).thenReturn("bskyblock.my.perm.for.game"); perms.add(permAtt); when(player.getEffectivePermissions()).thenReturn(perms); - PlayerJoinEvent e = new PlayerJoinEvent(player, "welcome"); + PlayerJoinEvent e = new PlayerJoinEvent(player, Component.text("welcome")); jl.onPlayerJoin(e); verify(addon).getGameModes(); verify(bll).setIsland("unique_id", ibc); } - /** - * Test method for - * {@link world.bentobox.limits.listeners.JoinListener#onPlayerJoin(org.bukkit.event.player.PlayerJoinEvent)}. - */ - @Test - public void testOnPlayerJoinWithPermLimitsWrongSize() { - Set perms = new HashSet<>(); - PermissionAttachmentInfo permAtt = mock(PermissionAttachmentInfo.class); - when(permAtt.getPermission()).thenReturn("bskyblock.island.limit.my.perm.for.game"); - when(permAtt.getValue()).thenReturn(true); - perms.add(permAtt); - when(player.getEffectivePermissions()).thenReturn(perms); - PlayerJoinEvent e = new PlayerJoinEvent(player, "welcome"); - jl.onPlayerJoin(e); - verify(addon).logError( - "Player tastybento has permission: 'bskyblock.island.limit.my.perm.for.game' but format must be 'bskyblock.island.limit.[ENV.]KEY.NUMBER' where ENV is overworld|nether|end and KEY is a material, entity type, or group name. Ignoring..."); - } - - /** - * Test method for - * {@link world.bentobox.limits.listeners.JoinListener#onPlayerJoin(org.bukkit.event.player.PlayerJoinEvent)}. - */ - @Test - public void testOnPlayerJoinWithPermLimitsInvalidMaterial() { - Set perms = new HashSet<>(); - PermissionAttachmentInfo permAtt = mock(PermissionAttachmentInfo.class); - when(permAtt.getPermission()).thenReturn("bskyblock.island.limit.mumbo.34"); - when(permAtt.getValue()).thenReturn(true); - perms.add(permAtt); - when(player.getEffectivePermissions()).thenReturn(perms); - PlayerJoinEvent e = new PlayerJoinEvent(player, "welcome"); - jl.onPlayerJoin(e); - verify(addon).logError( - "Player tastybento has permission: 'bskyblock.island.limit.mumbo.34' but MUMBO is not a valid material or entity type/group. Ignoring..."); + private static Stream invalidPermissionLimits() { + return Stream.of( + Arguments.of("bskyblock.island.limit.my.perm.for.game", + "Player tastybento has permission: 'bskyblock.island.limit.my.perm.for.game' but format must be 'bskyblock.island.limit.[ENV.]KEY.NUMBER' where ENV is overworld|nether|end and KEY is a material, entity type, or group name. Ignoring..."), + Arguments.of("bskyblock.island.limit.mumbo.34", + "Player tastybento has permission: 'bskyblock.island.limit.mumbo.34' but MUMBO is not a valid material or entity type/group. Ignoring..."), + Arguments.of("bskyblock.island.limit.*", + "Player tastybento has permission: 'bskyblock.island.limit.*' but wildcards are not allowed. Ignoring..."), + Arguments.of("bskyblock.island.limit.STONE.abc", + "Player tastybento has permission: 'bskyblock.island.limit.STONE.abc' but the last part MUST be an integer! Ignoring...")); } /** * Test method for * {@link world.bentobox.limits.listeners.JoinListener#onPlayerJoin(org.bukkit.event.player.PlayerJoinEvent)}. + * Each invalid permission format should be logged and ignored. */ - @Test - public void testOnPlayerJoinWithPermLimitsWildcard() { - Set perms = new HashSet<>(); - PermissionAttachmentInfo permAtt = mock(PermissionAttachmentInfo.class); - when(permAtt.getPermission()).thenReturn("bskyblock.island.limit.*"); - when(permAtt.getValue()).thenReturn(true); - perms.add(permAtt); - when(player.getEffectivePermissions()).thenReturn(perms); - PlayerJoinEvent e = new PlayerJoinEvent(player, "welcome"); - jl.onPlayerJoin(e); - verify(addon).logError( - "Player tastybento has permission: 'bskyblock.island.limit.*' but wildcards are not allowed. Ignoring..."); - } - - /** - * Test method for - * {@link world.bentobox.limits.listeners.JoinListener#onPlayerJoin(org.bukkit.event.player.PlayerJoinEvent)}. - */ - @Test - public void testOnPlayerJoinWithPermLimitsNotNumber() { + @ParameterizedTest + @MethodSource("invalidPermissionLimits") + void testOnPlayerJoinWithInvalidPermLimits(String permission, String expectedError) { Set perms = new HashSet<>(); PermissionAttachmentInfo permAtt = mock(PermissionAttachmentInfo.class); - when(permAtt.getPermission()).thenReturn("bskyblock.island.limit.STONE.abc"); + when(permAtt.getPermission()).thenReturn(permission); when(permAtt.getValue()).thenReturn(true); perms.add(permAtt); when(player.getEffectivePermissions()).thenReturn(perms); - PlayerJoinEvent e = new PlayerJoinEvent(player, "welcome"); + PlayerJoinEvent e = new PlayerJoinEvent(player, Component.text("welcome")); jl.onPlayerJoin(e); - verify(addon).logError( - "Player tastybento has permission: 'bskyblock.island.limit.STONE.abc' but the last part MUST be an integer! Ignoring..."); + verify(addon).logError(expectedError); } /** @@ -343,14 +306,14 @@ public void testOnPlayerJoinWithPermLimitsNotNumber() { * {@link world.bentobox.limits.listeners.JoinListener#onPlayerJoin(org.bukkit.event.player.PlayerJoinEvent)}. */ @Test - public void testOnPlayerJoinWithPermLimitsSuccess() { + void testOnPlayerJoinWithPermLimitsSuccess() { Set perms = new HashSet<>(); PermissionAttachmentInfo permAtt = mock(PermissionAttachmentInfo.class); when(permAtt.getPermission()).thenReturn("bskyblock.island.limit.STONE.24"); when(permAtt.getValue()).thenReturn(true); perms.add(permAtt); when(player.getEffectivePermissions()).thenReturn(perms); - PlayerJoinEvent e = new PlayerJoinEvent(player, "welcome"); + PlayerJoinEvent e = new PlayerJoinEvent(player, Component.text("welcome")); jl.onPlayerJoin(e); verify(addon, never()).logError(anyString()); verify(ibc).setBlockLimit(Environment.NORMAL, Material.STONE.getKey(), 24); @@ -361,14 +324,14 @@ public void testOnPlayerJoinWithPermLimitsSuccess() { * {@link world.bentobox.limits.listeners.JoinListener#onPlayerJoin(org.bukkit.event.player.PlayerJoinEvent)}. */ @Test - public void testOnPlayerJoinWithPermLimitsSuccessEntity() { + void testOnPlayerJoinWithPermLimitsSuccessEntity() { Set perms = new HashSet<>(); PermissionAttachmentInfo permAtt = mock(PermissionAttachmentInfo.class); when(permAtt.getPermission()).thenReturn("bskyblock.island.limit.BAT.24"); when(permAtt.getValue()).thenReturn(true); perms.add(permAtt); when(player.getEffectivePermissions()).thenReturn(perms); - PlayerJoinEvent e = new PlayerJoinEvent(player, "welcome"); + PlayerJoinEvent e = new PlayerJoinEvent(player, Component.text("welcome")); jl.onPlayerJoin(e); verify(addon, never()).logError(anyString()); verify(ibc).setEntityLimit(Environment.NORMAL, EntityType.BAT, 24); @@ -379,14 +342,14 @@ public void testOnPlayerJoinWithPermLimitsSuccessEntity() { * {@link world.bentobox.limits.listeners.JoinListener#onPlayerJoin(org.bukkit.event.player.PlayerJoinEvent)}. */ @Test - public void testOnPlayerJoinWithPermLimitsSuccessEntityGroup() { + void testOnPlayerJoinWithPermLimitsSuccessEntityGroup() { Set perms = new HashSet<>(); PermissionAttachmentInfo permAtt = mock(PermissionAttachmentInfo.class); when(permAtt.getPermission()).thenReturn("bskyblock.island.limit.friendly.24"); when(permAtt.getValue()).thenReturn(true); perms.add(permAtt); when(player.getEffectivePermissions()).thenReturn(perms); - PlayerJoinEvent e = new PlayerJoinEvent(player, "welcome"); + PlayerJoinEvent e = new PlayerJoinEvent(player, Component.text("welcome")); jl.onPlayerJoin(e); verify(addon, never()).logError(anyString()); verify(ibc).setEntityGroupLimit(Environment.NORMAL, "friendly", 24); @@ -397,7 +360,7 @@ public void testOnPlayerJoinWithPermLimitsSuccessEntityGroup() { * {@link world.bentobox.limits.listeners.JoinListener#onPlayerJoin(org.bukkit.event.player.PlayerJoinEvent)}. */ @Test - public void testOnPlayerJoinWithPermLimitsMultiPerms() { + void testOnPlayerJoinWithPermLimitsMultiPerms() { Set perms = new HashSet<>(); PermissionAttachmentInfo permAtt = mock(PermissionAttachmentInfo.class); when(permAtt.getPermission()).thenReturn("bskyblock.island.limit.STONE.24"); @@ -425,7 +388,7 @@ public void testOnPlayerJoinWithPermLimitsMultiPerms() { perms.add(permAtt6); when(player.getEffectivePermissions()).thenReturn(perms); - PlayerJoinEvent e = new PlayerJoinEvent(player, "welcome"); + PlayerJoinEvent e = new PlayerJoinEvent(player, Component.text("welcome")); jl.onPlayerJoin(e); verify(addon, never()).logError(anyString()); verify(ibc).setBlockLimit(Environment.NORMAL, Material.STONE.getKey(), 24); @@ -439,7 +402,7 @@ public void testOnPlayerJoinWithPermLimitsMultiPerms() { * Test method for {@link world.bentobox.limits.listeners.JoinListener#onPlayerJoin(org.bukkit.event.player.PlayerJoinEvent)}. */ @Test - public void testOnPlayerJoinWithPermLimitsMultiPermsSameMaterial() { + void testOnPlayerJoinWithPermLimitsMultiPermsSameMaterial() { // IBC - set the block limit for STONE to be 25 already when(ibc.getBlockLimit(any(Environment.class), any(NamespacedKey.class))).thenReturn(25); Set perms = new HashSet<>(); @@ -456,7 +419,7 @@ public void testOnPlayerJoinWithPermLimitsMultiPermsSameMaterial() { when(permAtt3.getValue()).thenReturn(true); perms.add(permAtt3); when(player.getEffectivePermissions()).thenReturn(perms); - PlayerJoinEvent e = new PlayerJoinEvent(player, "welcome"); + PlayerJoinEvent e = new PlayerJoinEvent(player, Component.text("welcome")); jl.onPlayerJoin(e); verify(addon, never()).logError(anyString()); // Only the limit over 25 should be set @@ -469,7 +432,7 @@ public void testOnPlayerJoinWithPermLimitsMultiPermsSameMaterial() { * Test method for {@link world.bentobox.limits.listeners.JoinListener#onPlayerJoin(org.bukkit.event.player.PlayerJoinEvent)}. */ @Test - public void testOnPlayerJoinWithPermLimitsMultiPermsSameEntity() { + void testOnPlayerJoinWithPermLimitsMultiPermsSameEntity() { // IBC - set the entity limit for BAT to be 25 already when(ibc.getEntityLimit(any(Environment.class), any(EntityType.class))).thenReturn(25); Set perms = new HashSet<>(); @@ -486,7 +449,7 @@ public void testOnPlayerJoinWithPermLimitsMultiPermsSameEntity() { when(permAtt3.getValue()).thenReturn(true); perms.add(permAtt3); when(player.getEffectivePermissions()).thenReturn(perms); - PlayerJoinEvent e = new PlayerJoinEvent(player, "welcome"); + PlayerJoinEvent e = new PlayerJoinEvent(player, Component.text("welcome")); jl.onPlayerJoin(e); verify(addon, never()).logError(anyString()); // Only the limit over 25 should be set @@ -500,7 +463,7 @@ public void testOnPlayerJoinWithPermLimitsMultiPermsSameEntity() { * {@link world.bentobox.limits.listeners.JoinListener#onUnregisterIsland(world.bentobox.bentobox.api.events.island.IslandEvent)}. */ @Test - public void testOnUnregisterIslandNotUnregistered() { + void testOnUnregisterIslandNotUnregistered() { IslandEvent e = new IslandEvent(island, null, false, null, IslandEvent.Reason.BAN); jl.onUnregisterIsland(e); verify(island, never()).getWorld(); @@ -511,7 +474,7 @@ public void testOnUnregisterIslandNotUnregistered() { * {@link world.bentobox.limits.listeners.JoinListener#onUnregisterIsland(world.bentobox.bentobox.api.events.island.IslandEvent)}. */ @Test - public void testOnUnregisterIslandNotInWorld() { + void testOnUnregisterIslandNotInWorld() { IslandEvent e = new IslandEvent(island, null, false, null, IslandEvent.Reason.UNREGISTERED); jl.onUnregisterIsland(e); verify(island).getWorld(); @@ -523,7 +486,7 @@ public void testOnUnregisterIslandNotInWorld() { * {@link world.bentobox.limits.listeners.JoinListener#onUnregisterIsland(world.bentobox.bentobox.api.events.island.IslandEvent)}. */ @Test - public void testOnUnregisterIslandInWorld() { + void testOnUnregisterIslandInWorld() { when(addon.inGameModeWorld(any())).thenReturn(true); when(addon.getBlockLimitListener()).thenReturn(bll); when(bll.getIsland(island.getUniqueId())).thenReturn(ibc); @@ -540,7 +503,7 @@ public void testOnUnregisterIslandInWorld() { * Test method for {@link world.bentobox.limits.listeners.JoinListener#onUnregisterIsland(world.bentobox.bentobox.api.events.island.IslandEvent)}. */ @Test - public void testOnUnregisterIslandInWorldNullIBC() { + void testOnUnregisterIslandInWorldNullIBC() { when(bll.getIsland(anyString())).thenReturn(null); @SuppressWarnings("unchecked") Map map = mock(Map.class); diff --git a/src/test/java/world/bentobox/limits/LimitsPladdonTest.java b/src/test/java/world/bentobox/limits/LimitsPladdonTest.java index 6a74a5b..54d9f42 100644 --- a/src/test/java/world/bentobox/limits/LimitsPladdonTest.java +++ b/src/test/java/world/bentobox/limits/LimitsPladdonTest.java @@ -11,39 +11,38 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.mock; import world.bentobox.bentobox.api.addons.Addon; @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) -public class LimitsPladdonTest { +class LimitsPladdonTest { private LimitsPladdon pladdon; @BeforeEach - public void setUp() { + void setUp() { // LimitsPladdon extends Pladdon extends JavaPlugin, which requires PluginClassLoader. // Use mock with CALLS_REAL_METHODS to bypass the JavaPlugin constructor. pladdon = mock(LimitsPladdon.class, org.mockito.Mockito.CALLS_REAL_METHODS); } @Test - public void testGetAddonReturnsNonNull() { + void testGetAddonReturnsNonNull() { Addon addon = pladdon.getAddon(); assertNotNull(addon); } @Test - public void testGetAddonReturnsSameInstance() { + void testGetAddonReturnsSameInstance() { Addon first = pladdon.getAddon(); Addon second = pladdon.getAddon(); assertSame(first, second); } @Test - public void testGetAddonIsInstanceOfLimits() { + void testGetAddonIsInstanceOfLimits() { Addon addon = pladdon.getAddon(); assertInstanceOf(Limits.class, addon); } diff --git a/src/test/java/world/bentobox/limits/LimitsTest.java b/src/test/java/world/bentobox/limits/LimitsTest.java index 642c3f7..562e3cc 100644 --- a/src/test/java/world/bentobox/limits/LimitsTest.java +++ b/src/test/java/world/bentobox/limits/LimitsTest.java @@ -1,11 +1,11 @@ package world.bentobox.limits; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -56,7 +56,6 @@ import world.bentobox.bentobox.managers.IslandsManager; import world.bentobox.bentobox.managers.PlaceholdersManager; import org.mockbukkit.mockbukkit.MockBukkit; -import org.mockbukkit.mockbukkit.ServerMock; /** * @author tastybento @@ -65,7 +64,7 @@ @SuppressWarnings("deprecation") @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) -public class LimitsTest { +class LimitsTest { private static File jFile; @Mock private User user; @@ -98,7 +97,7 @@ public class LimitsTest { private MockedStatic mockedBentoBox; @BeforeAll - public static void beforeClass() throws Exception { + static void beforeClass() throws Exception { cleanUp(); // Make the addon jar jFile = new File("addon.jar"); @@ -124,8 +123,8 @@ public static void beforeClass() throws Exception { * @throws java.lang.Exception */ @BeforeEach - public void setUp() throws Exception { - ServerMock server = MockBukkit.mock(); + void setUp() { + MockBukkit.mock(); // Set up plugin mockedBentoBox = Mockito.mockStatic(BentoBox.class); @@ -137,7 +136,6 @@ public void setUp() throws Exception { when(plugin.getSettings()).thenReturn(pluginSettings); when(pluginSettings.getDatabaseType()).thenReturn(value); - //when(plugin.isEnabled()).thenReturn(true); // Command manager CommandsManager cm = mock(CommandsManager.class); when(plugin.getCommandsManager()).thenReturn(cm); @@ -213,7 +211,7 @@ public void setUp() throws Exception { * @throws java.lang.Exception */ @AfterEach - public void tearDown() throws Exception { + void tearDown() throws Exception { if (mockedBentoBox != null) { mockedBentoBox.close(); } @@ -224,7 +222,7 @@ public void tearDown() throws Exception { } @AfterAll - public static void cleanUp() throws Exception { + static void cleanUp() throws Exception { new File("addon.jar").delete(); new File("config.yml").delete(); deleteAll(new File("addons")); @@ -243,7 +241,7 @@ private static void deleteAll(File file) throws IOException { * Test method for {@link world.bentobox.limits.Limits#onEnable()}. */ @Test - public void testOnEnable() { + void testOnEnable() { addon.onEnable(); File f = new File("config.yml"); assertTrue(f.exists()); @@ -254,15 +252,15 @@ public void testOnEnable() { * Test method for {@link world.bentobox.limits.Limits#onDisable()}. */ @Test - public void testOnDisable() { - addon.onDisable(); + void testOnDisable() { + assertDoesNotThrow(() -> addon.onDisable()); } /** * Test method for {@link world.bentobox.limits.Limits#getSettings()}. */ @Test - public void testGetSettings() { + void testGetSettings() { assertNull(addon.getSettings()); addon.onEnable(); world.bentobox.limits.Settings set = addon.getSettings(); @@ -273,7 +271,7 @@ public void testGetSettings() { * Test method for {@link world.bentobox.limits.Limits#getGameModes()}. */ @Test - public void testGetGameModes() { + void testGetGameModes() { assertTrue(addon.getGameModes().isEmpty()); addon.onEnable(); assertFalse(addon.getGameModes().isEmpty()); @@ -283,7 +281,7 @@ public void testGetGameModes() { * Test method for {@link world.bentobox.limits.Limits#getBlockLimitListener()}. */ @Test - public void testGetBlockLimitListener() { + void testGetBlockLimitListener() { assertNull(addon.getBlockLimitListener()); addon.onEnable(); assertNotNull(addon.getBlockLimitListener()); @@ -293,7 +291,7 @@ public void testGetBlockLimitListener() { * Test method for {@link world.bentobox.limits.Limits#inGameModeWorld(org.bukkit.World)}. */ @Test - public void testInGameModeWorld() { + void testInGameModeWorld() { addon.onEnable(); assertFalse(addon.inGameModeWorld(world)); when(gameMode.inWorld(world)).thenReturn(true); @@ -304,7 +302,7 @@ public void testInGameModeWorld() { * Test method for {@link world.bentobox.limits.Limits#getGameModeName(org.bukkit.World)}. */ @Test - public void testGetGameModeName() { + void testGetGameModeName() { when(gameMode.inWorld(world)).thenReturn(true); assertTrue(addon.getGameModeName(world).isEmpty()); addon.onEnable(); @@ -315,7 +313,7 @@ public void testGetGameModeName() { * Test method for {@link world.bentobox.limits.Limits#getGameModePermPrefix(org.bukkit.World)}. */ @Test - public void testGetGameModePermPrefix() { + void testGetGameModePermPrefix() { when(gameMode.inWorld(world)).thenReturn(true); addon.onEnable(); assertEquals("bskyblock.", addon.getGameModePermPrefix(world)); @@ -325,7 +323,7 @@ public void testGetGameModePermPrefix() { * Test method for {@link world.bentobox.limits.Limits#isCoveredGameMode(java.lang.String)}. */ @Test - public void testIsCoveredGameMode() { + void testIsCoveredGameMode() { assertFalse(addon.isCoveredGameMode("BSkyBlock")); addon.onEnable(); assertTrue(addon.isCoveredGameMode("BSkyBlock")); @@ -335,7 +333,7 @@ public void testIsCoveredGameMode() { * Test method for {@link world.bentobox.limits.Limits#getJoinListener()}. */ @Test - public void testGetJoinListener() { + void testGetJoinListener() { assertNull(addon.getJoinListener()); addon.onEnable(); assertNotNull(addon.getJoinListener()); diff --git a/src/test/java/world/bentobox/limits/SettingsTest.java b/src/test/java/world/bentobox/limits/SettingsTest.java index 39194de..3a7382d 100644 --- a/src/test/java/world/bentobox/limits/SettingsTest.java +++ b/src/test/java/world/bentobox/limits/SettingsTest.java @@ -31,7 +31,7 @@ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) -public class SettingsTest { +class SettingsTest { @Mock private Limits addon; @@ -40,7 +40,7 @@ public class SettingsTest { private FileConfiguration config; @BeforeEach - public void setUp() throws Exception { + void setUp() throws Exception { MockBukkit.mock(); config = new YamlConfiguration(); @@ -53,12 +53,12 @@ public void setUp() throws Exception { } @AfterEach - public void tearDown() { + void tearDown() { MockBukkit.unmock(); } @Test - public void testGameModesNotEmpty() { + void testGameModesNotEmpty() { List gameModes = settings.getGameModes(); assertNotNull(gameModes); assertFalse(gameModes.isEmpty()); @@ -66,7 +66,7 @@ public void testGameModesNotEmpty() { } @Test - public void testEntityLimitsParsed() { + void testEntityLimitsParsed() { Map limits = settings.getLimits(Environment.NORMAL); assertNotNull(limits); assertFalse(limits.isEmpty()); @@ -75,7 +75,7 @@ public void testEntityLimitsParsed() { } @Test - public void testGroupLimitsParsed() { + void testGroupLimitsParsed() { Map> groupLimits = settings.getGroupLimits(); assertNotNull(groupLimits); assertFalse(groupLimits.isEmpty()); @@ -84,7 +84,7 @@ public void testGroupLimitsParsed() { } @Test - public void testGetGroupLimitDefinitions() { + void testGetGroupLimitDefinitions() { List definitions = settings.getGroupLimitDefinitions(); assertNotNull(definitions); assertFalse(definitions.isEmpty()); @@ -94,17 +94,17 @@ public void testGetGroupLimitDefinitions() { } @Test - public void testLogLimitsOnJoinDefaultsTrue() { + void testLogLimitsOnJoinDefaultsTrue() { assertTrue(settings.isLogLimitsOnJoin()); } @Test - public void testAsyncGolumsDefaultsTrue() { + void testAsyncGolumsDefaultsTrue() { assertTrue(settings.isAsyncGolums()); } @Test - public void testGetGeneralEmpty() { + void testGetGeneralEmpty() { // Default config.yml does not have ANIMALS or MOBS entries in entitylimits Map general = settings.getGeneral(); assertNotNull(general); @@ -113,7 +113,7 @@ public void testGetGeneralEmpty() { } @Test - public void testGetGeneralWithAnimalsAndMobs() throws Exception { + void testGetGeneralWithAnimalsAndMobs() { // Add ANIMALS and MOBS to the entitylimits section config.set("entitylimits.ANIMALS", 100); config.set("entitylimits.MOBS", 50); @@ -124,7 +124,7 @@ public void testGetGeneralWithAnimalsAndMobs() throws Exception { } @Test - public void testUnknownEntityTypeLogsError() throws Exception { + void testUnknownEntityTypeLogsError() { config.set("entitylimits.UNKNOWN_ENTITY_XYZ", 99); Settings s = new Settings(addon); verify(addon, atLeastOnce()).logError("Unknown entity type in entitylimits: UNKNOWN_ENTITY_XYZ - skipping..."); @@ -132,7 +132,7 @@ public void testUnknownEntityTypeLogsError() throws Exception { } @Test - public void testDisallowedEntityTypeLogsError() throws Exception { + void testDisallowedEntityTypeLogsError() { config.set("entitylimits.TNT", 10); Settings s = new Settings(addon); verify(addon).logError("Entity type in entitylimits not supported: TNT - skipping..."); @@ -140,7 +140,7 @@ public void testDisallowedEntityTypeLogsError() throws Exception { } @Test - public void testInvalidGroupIconFallsBackToBarrier() throws Exception { + void testInvalidGroupIconFallsBackToBarrier() { // Clear existing groups and add one with invalid icon config.set("entitygrouplimits", null); config.set("entitygrouplimits.TestGroup.icon", "NOT_A_REAL_MATERIAL"); diff --git a/src/test/java/world/bentobox/limits/calculators/PipelinerTest.java b/src/test/java/world/bentobox/limits/calculators/PipelinerTest.java index c2c20e9..6f341c6 100644 --- a/src/test/java/world/bentobox/limits/calculators/PipelinerTest.java +++ b/src/test/java/world/bentobox/limits/calculators/PipelinerTest.java @@ -28,7 +28,7 @@ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) -public class PipelinerTest { +class PipelinerTest { @Mock private Limits addon; diff --git a/src/test/java/world/bentobox/limits/calculators/ResultsTest.java b/src/test/java/world/bentobox/limits/calculators/ResultsTest.java index 6d32941..e454471 100644 --- a/src/test/java/world/bentobox/limits/calculators/ResultsTest.java +++ b/src/test/java/world/bentobox/limits/calculators/ResultsTest.java @@ -9,16 +9,16 @@ import world.bentobox.limits.calculators.Results.Result; -public class ResultsTest { +class ResultsTest { @Test - public void testDefaultConstructorStateIsAvailable() { + void testDefaultConstructorStateIsAvailable() { Results results = new Results(); assertEquals(Result.AVAILABLE, results.getState()); } @Test - public void testConstructorWithState() { + void testConstructorWithState() { Results results = new Results(Result.IN_PROGRESS); assertEquals(Result.IN_PROGRESS, results.getState()); } @@ -31,14 +31,14 @@ void testGetBlockCountReturnsEmptyMultiset() { } @Test - public void testGetEntityCountReturnsEmptyMultiset() { + void testGetEntityCountReturnsEmptyMultiset() { Results results = new Results(); assertNotNull(results.getEntityCount(Environment.NORMAL)); assertTrue(results.getEntityCount(Environment.NORMAL).isEmpty()); } @Test - public void testResultEnumValues() { + void testResultEnumValues() { Result[] values = Result.values(); assertEquals(3, values.length); assertNotNull(Result.valueOf("AVAILABLE")); diff --git a/src/test/java/world/bentobox/limits/commands/player/LimitPanelTest.java b/src/test/java/world/bentobox/limits/commands/player/LimitPanelTest.java index e0d9b75..dd7f3d0 100644 --- a/src/test/java/world/bentobox/limits/commands/player/LimitPanelTest.java +++ b/src/test/java/world/bentobox/limits/commands/player/LimitPanelTest.java @@ -2,16 +2,13 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.Collections; -import java.util.Map; import java.util.UUID; -import org.bukkit.NamespacedKey; import org.bukkit.World; import org.bukkit.World.Environment; import org.bukkit.entity.Player; @@ -36,7 +33,7 @@ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) -public class LimitPanelTest { +class LimitPanelTest { @Mock private Limits addon; @@ -105,7 +102,7 @@ void testShowLimitsNoIslandOther() { void testShowLimitsNoLimits() { when(im.getIsland(world, targetUUID)).thenReturn(island); when(island.getUniqueId()).thenReturn("island-id"); - when(bll.getMaterialLimits(eq(world), eq("island-id"))).thenReturn(Collections.emptyMap()); + when(bll.getMaterialLimits(world, "island-id")).thenReturn(Collections.emptyMap()); when(settings.getLimits(Environment.NORMAL)).thenReturn(Collections.emptyMap()); // Target player is offline (MockBukkit returns null by default when no players added) @@ -118,7 +115,7 @@ void testShowLimitsNoLimits() { void testShowLimitsTargetPlayerOfflineDoesNotCallCheckPerms() { when(im.getIsland(world, targetUUID)).thenReturn(island); when(island.getUniqueId()).thenReturn("island-id"); - when(bll.getMaterialLimits(eq(world), eq("island-id"))).thenReturn(Collections.emptyMap()); + when(bll.getMaterialLimits(world, "island-id")).thenReturn(Collections.emptyMap()); when(settings.getLimits(Environment.NORMAL)).thenReturn(Collections.emptyMap()); // Target player is offline (MockBukkit returns null by default when no players added) diff --git a/src/test/java/world/bentobox/limits/commands/player/LimitTabTest.java b/src/test/java/world/bentobox/limits/commands/player/LimitTabTest.java index 2c29ed2..7ae6a42 100644 --- a/src/test/java/world/bentobox/limits/commands/player/LimitTabTest.java +++ b/src/test/java/world/bentobox/limits/commands/player/LimitTabTest.java @@ -38,7 +38,7 @@ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) -public class LimitTabTest { +class LimitTabTest { @Mock private Limits addon; @Mock private Island island; @@ -49,7 +49,7 @@ public class LimitTabTest { @Mock private User user; @BeforeEach - public void setUp() { + void setUp() { MockBukkit.mock(); when(island.getWorld()).thenReturn(world); when(addon.getPlugin()).thenReturn(plugin); @@ -82,7 +82,7 @@ public void setUp() { } @AfterEach - public void tearDown() { + void tearDown() { MockBukkit.unmock(); } diff --git a/src/test/java/world/bentobox/limits/events/LimitsJoinPermCheckEventTest.java b/src/test/java/world/bentobox/limits/events/LimitsJoinPermCheckEventTest.java index 07254e2..f0a9333 100644 --- a/src/test/java/world/bentobox/limits/events/LimitsJoinPermCheckEventTest.java +++ b/src/test/java/world/bentobox/limits/events/LimitsJoinPermCheckEventTest.java @@ -19,7 +19,7 @@ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) -public class LimitsJoinPermCheckEventTest { +class LimitsJoinPermCheckEventTest { @Mock private Player player; @@ -28,70 +28,70 @@ public class LimitsJoinPermCheckEventTest { private LimitsJoinPermCheckEvent event; @BeforeEach - public void setUp() { + void setUp() { ibc = new IslandBlockCount("island1", "BSkyBlock"); event = new LimitsJoinPermCheckEvent(player, "island1", ibc); } @Test - public void testConstructorSetsFields() { + void testConstructorSetsFields() { assertEquals(player, event.getPlayer()); assertEquals("island1", event.getIslandId()); assertEquals(ibc, event.getIbc()); } @Test - public void testGetPlayer() { + void testGetPlayer() { assertEquals(player, event.getPlayer()); } @Test - public void testGetIslandId() { + void testGetIslandId() { assertEquals("island1", event.getIslandId()); } @Test - public void testGetSetIbc() { + void testGetSetIbc() { IslandBlockCount newIbc = new IslandBlockCount("island2", "AcidIsland"); event.setIbc(newIbc); assertEquals(newIbc, event.getIbc()); } @Test - public void testSetIbcNull() { + void testSetIbcNull() { event.setIbc(null); assertNull(event.getIbc()); } @Test - public void testIsCancelledDefaultFalse() { + void testIsCancelledDefaultFalse() { assertFalse(event.isCancelled()); } @Test - public void testSetCancelled() { + void testSetCancelled() { event.setCancelled(true); assertTrue(event.isCancelled()); } @Test - public void testIsIgnorePermsDefaultFalse() { + void testIsIgnorePermsDefaultFalse() { assertFalse(event.isIgnorePerms()); } @Test - public void testSetIgnorePerms() { + void testSetIgnorePerms() { event.setIgnorePerms(true); assertTrue(event.isIgnorePerms()); } @Test - public void testGetHandlers() { + void testGetHandlers() { assertNotNull(event.getHandlers()); } @Test - public void testGetHandlerListStatic() { + void testGetHandlerListStatic() { assertNotNull(LimitsJoinPermCheckEvent.getHandlerList()); } } diff --git a/src/test/java/world/bentobox/limits/events/LimitsPermCheckEventTest.java b/src/test/java/world/bentobox/limits/events/LimitsPermCheckEventTest.java index 5374cf9..8a53064 100644 --- a/src/test/java/world/bentobox/limits/events/LimitsPermCheckEventTest.java +++ b/src/test/java/world/bentobox/limits/events/LimitsPermCheckEventTest.java @@ -23,7 +23,7 @@ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) -public class LimitsPermCheckEventTest { +class LimitsPermCheckEventTest { @Mock private Player player; @@ -33,7 +33,7 @@ public class LimitsPermCheckEventTest { private LimitsPermCheckEvent event; @BeforeEach - public void setUp() { + void setUp() { MockBukkit.mock(); ibc = new IslandBlockCount("island1", "BSkyBlock"); entityGroup = new EntityGroup("monsters", Set.of(EntityType.ZOMBIE), 10, Material.ZOMBIE_HEAD); @@ -41,12 +41,12 @@ public void setUp() { } @AfterEach - public void tearDown() { + void tearDown() { MockBukkit.unmock(); } @Test - public void testConstructorSetsAllFields() { + void testConstructorSetsAllFields() { assertEquals(player, event.getPlayer()); assertEquals("island1", event.getIslandId()); assertEquals(ibc, event.getIbc()); @@ -57,44 +57,44 @@ public void testConstructorSetsAllFields() { } @Test - public void testGetSetEntityGroup() { + void testGetSetEntityGroup() { EntityGroup newGroup = new EntityGroup("animals", Set.of(EntityType.COW), 5, null); event.setEntityGroup(newGroup); assertEquals(newGroup, event.getEntityGroup()); } @Test - public void testGetSetEntityType() { + void testGetSetEntityType() { event.setEntityType(EntityType.SKELETON); assertEquals(EntityType.SKELETON, event.getEntityType()); } @Test - public void testGetSetMaterial() { + void testGetSetMaterial() { event.setMaterial(Material.DIAMOND_BLOCK); assertEquals(Material.DIAMOND_BLOCK, event.getMaterial()); } @Test - public void testGetSetValue() { + void testGetSetValue() { event.setValue(100); assertEquals(100, event.getValue()); } @Test - public void testNullEntityGroup() { + void testNullEntityGroup() { event.setEntityGroup(null); assertNull(event.getEntityGroup()); } @Test - public void testNullEntityType() { + void testNullEntityType() { event.setEntityType(null); assertNull(event.getEntityType()); } @Test - public void testNullMaterial() { + void testNullMaterial() { event.setMaterial(null); assertNull(event.getMaterial()); } diff --git a/src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java b/src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java index dc117dd..3331193 100644 --- a/src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java +++ b/src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java @@ -7,7 +7,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; @@ -86,7 +85,7 @@ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) -public class BlockLimitsListenerTest { +class BlockLimitsListenerTest { @Mock private Limits addon; @@ -118,7 +117,7 @@ public class BlockLimitsListenerTest { @SuppressWarnings("unchecked") @BeforeEach - public void setUp() throws Exception { + void setUp() throws Exception { MockBukkit.mock(); // Set up BentoBox static mock so Database class can initialize @@ -171,7 +170,7 @@ public void setUp() throws Exception { } @AfterEach - public void tearDown() { + void tearDown() { User.clearUsers(); if (mockedDb != null) { mockedDb.close(); @@ -504,7 +503,8 @@ void testBlockMultiPlaceAtLimitCancels() { when(state.getBlock()).thenReturn(block); List states = List.of(state); Block clicked = mockBlock(Material.DIRT, blockLocation); - BlockMultiPlaceEvent event = new BlockMultiPlaceEvent(states, clicked, new ItemStack(Material.OAK_PLANKS), player, true); + BlockMultiPlaceEvent event = new BlockMultiPlaceEvent(states, clicked, new ItemStack(Material.OAK_PLANKS), player, + true, EquipmentSlot.HAND); listener.onBlock(event); @@ -526,7 +526,7 @@ void testBedPlacedViaMultiPlaceCountsOnce() { BlockState headState = mock(BlockState.class); when(headState.getBlock()).thenReturn(head); BlockMultiPlaceEvent event = new BlockMultiPlaceEvent(List.of(headState), foot, - new ItemStack(Material.RED_BED), player, true); + new ItemStack(Material.RED_BED), player, true, EquipmentSlot.HAND); org.bukkit.Bukkit.getPluginManager().callEvent(event); @@ -545,7 +545,7 @@ void testBedPlaceThenBreakLeavesZero() { BlockState headState = mock(BlockState.class); when(headState.getBlock()).thenReturn(head); BlockMultiPlaceEvent place = new BlockMultiPlaceEvent(List.of(headState), foot, - new ItemStack(Material.RED_BED), player, true); + new ItemStack(Material.RED_BED), player, true, EquipmentSlot.HAND); org.bukkit.Bukkit.getPluginManager().callEvent(place); // Player breaks one half of the bed (vanilla removes the other half with no break event). @@ -1289,7 +1289,7 @@ void testIslandDeleteCallsDatabaseDelete() { listener.onIslandDelete(event); - verify(dbMock).deleteID(eq("test-island-id")); + verify(dbMock).deleteID("test-island-id"); } // --- save tests --- diff --git a/src/test/java/world/bentobox/limits/listeners/EntityLimitListenerTest.java b/src/test/java/world/bentobox/limits/listeners/EntityLimitListenerTest.java index f23f41e..d548601 100644 --- a/src/test/java/world/bentobox/limits/listeners/EntityLimitListenerTest.java +++ b/src/test/java/world/bentobox/limits/listeners/EntityLimitListenerTest.java @@ -35,6 +35,7 @@ import org.bukkit.event.entity.CreatureSpawnEvent; import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason; import org.bukkit.event.entity.EntityBreedEvent; +import org.bukkit.event.Event; import org.bukkit.event.block.Action; import org.bukkit.event.hanging.HangingPlaceEvent; import org.bukkit.event.player.PlayerInteractEntityEvent; @@ -74,7 +75,7 @@ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) -public class EntityLimitListenerTest { +class EntityLimitListenerTest { @Mock private Limits addon; private EntityLimitListener ell; @@ -95,7 +96,7 @@ public class EntityLimitListenerTest { @BeforeEach - public void setUp() throws Exception { + void setUp() throws Exception { MockBukkit.mock(); // Entity @@ -163,7 +164,7 @@ public void setUp() throws Exception { } @AfterEach - public void tearDown() { + void tearDown() { User.clearUsers(); MockBukkit.unmock(); } @@ -172,7 +173,7 @@ public void tearDown() { * Test for {@link EntityLimitListener#atLimit(Island, Entity)} */ @Test - public void testAtLimitUnderLimit() { + void testAtLimitUnderLimit() { AtLimitResult result = ell.atLimit(island, ent); assertFalse(result.hit()); } @@ -181,7 +182,7 @@ public void testAtLimitUnderLimit() { * Test for {@link EntityLimitListener#atLimit(Island, Entity)} */ @Test - public void testAtLimitAtLimit() { + void testAtLimitAtLimit() { // The default limit for ENDERMAN is 5 (from config), and we have 4 already // Adding one more puts us at the limit ibc.incrementEntity(Environment.NORMAL, EntityType.ENDERMAN); @@ -196,7 +197,7 @@ public void testAtLimitAtLimit() { * Test for {@link EntityLimitListener#atLimit(Island, Entity)} */ @Test - public void testAtLimitUnderLimitIslandLimit() { + void testAtLimitUnderLimitIslandLimit() { ibc.setEntityLimit(Environment.NORMAL, EntityType.ENDERMAN, 6); AtLimitResult result = ell.atLimit(island, ent); assertFalse(result.hit()); @@ -206,7 +207,7 @@ public void testAtLimitUnderLimitIslandLimit() { * Test for {@link EntityLimitListener#atLimit(Island, Entity)} */ @Test - public void testAtLimitAtLimitIslandLimitNotAtLimit() { + void testAtLimitAtLimitIslandLimitNotAtLimit() { // Island limit of 6, we have 4 already, so 5th entity should not be at limit ibc.setEntityLimit(Environment.NORMAL, EntityType.ENDERMAN, 6); ibc.incrementEntity(Environment.NORMAL, EntityType.ENDERMAN); @@ -218,7 +219,7 @@ public void testAtLimitAtLimitIslandLimitNotAtLimit() { * Test for {@link EntityLimitListener#atLimit(Island, Entity)} */ @Test - public void testAtLimitAtLimitIslandLimit() { + void testAtLimitAtLimitIslandLimit() { // Island limit of 6, we have 4, add 2 more to reach exactly 6 ibc.setEntityLimit(Environment.NORMAL, EntityType.ENDERMAN, 6); ibc.incrementEntity(Environment.NORMAL, EntityType.ENDERMAN); @@ -233,7 +234,7 @@ public void testAtLimitAtLimitIslandLimit() { // --- CreatureSpawnEvent tests --- @Test - public void testCreatureSpawnShoulderEntityIgnored() { + void testCreatureSpawnShoulderEntityIgnored() { LivingEntity chicken = mockEntity(EntityType.CHICKEN, location); CreatureSpawnEvent event = new CreatureSpawnEvent(chicken, SpawnReason.SHOULDER_ENTITY); @@ -243,7 +244,7 @@ public void testCreatureSpawnShoulderEntityIgnored() { } @Test - public void testCreatureSpawnBreedingNonVillagerIgnored() { + void testCreatureSpawnBreedingNonVillagerIgnored() { LivingEntity cow = mockEntity(EntityType.COW, location); CreatureSpawnEvent event = new CreatureSpawnEvent(cow, SpawnReason.BREEDING); @@ -253,7 +254,7 @@ public void testCreatureSpawnBreedingNonVillagerIgnored() { } @Test - public void testCreatureSpawnOutsideGameModeWorldIgnored() { + void testCreatureSpawnOutsideGameModeWorldIgnored() { when(addon.inGameModeWorld(world)).thenReturn(false); LivingEntity chicken = mockEntity(EntityType.CHICKEN, location); @@ -266,7 +267,7 @@ public void testCreatureSpawnOutsideGameModeWorldIgnored() { } @Test - public void testCreatureSpawnOnSpawnIslandAllowed() { + void testCreatureSpawnOnSpawnIslandAllowed() { when(island.isSpawn()).thenReturn(true); // Put CHICKEN at its limit (10 in config) by adding 10 chickens @@ -285,7 +286,7 @@ public void testCreatureSpawnOnSpawnIslandAllowed() { } @Test - public void testSpawnerSpawnReasonNoPlayerNotification() { + void testSpawnerSpawnReasonNoPlayerNotification() { // Put CHICKEN at limit (10 from config) so it gets cancelled // Pre-fill with 10 chickens for (int i = 0; i < 10; i++) { @@ -305,7 +306,7 @@ public void testSpawnerSpawnReasonNoPlayerNotification() { // --- HangingPlaceEvent tests --- @Test - public void testHangingPlaceNullPlayerIgnored() { + void testHangingPlaceNullPlayerIgnored() { Hanging hanging = mock(Hanging.class); when(hanging.getLocation()).thenReturn(location); when(hanging.getWorld()).thenReturn(world); @@ -321,7 +322,7 @@ public void testHangingPlaceNullPlayerIgnored() { // --- EntityBreedEvent tests --- @Test - public void testBreedingOpPlayerBypasses() { + void testBreedingOpPlayerBypasses() { // Create an op player as breeder Player opPlayer = mock(Player.class); when(opPlayer.isOp()).thenReturn(true); @@ -360,7 +361,7 @@ public void testBreedingOpPlayerBypasses() { // --- CreatureSpawn at-limit tests --- @Test - public void testCreatureSpawnAtLimitCancels() { + void testCreatureSpawnAtLimitCancels() { // Set island-specific CHICKEN limit to 1, pre-fill with 1 ibc.setEntityLimit(Environment.NORMAL, EntityType.CHICKEN, 1); ibc.incrementEntity(Environment.NORMAL, EntityType.CHICKEN); @@ -374,7 +375,7 @@ public void testCreatureSpawnAtLimitCancels() { } @Test - public void testCreatureSpawnBreedingVillagerProcessed() { + void testCreatureSpawnBreedingVillagerProcessed() { // Set island-specific VILLAGER limit to 1, pre-fill with 1 ibc.setEntityLimit(Environment.NORMAL, EntityType.VILLAGER, 1); ibc.incrementEntity(Environment.NORMAL, EntityType.VILLAGER); @@ -392,7 +393,7 @@ public void testCreatureSpawnBreedingVillagerProcessed() { } @Test - public void testCreatureSpawnDebounceSkipsSecond() throws Exception { + void testCreatureSpawnDebounceSkipsSecond() throws Exception { // Access justSpawned list via reflection Field justSpawnedField = EntityLimitListener.class.getDeclaredField("justSpawned"); justSpawnedField.setAccessible(true); @@ -417,7 +418,7 @@ public void testCreatureSpawnDebounceSkipsSecond() throws Exception { // --- VehicleCreateEvent tests --- @Test - public void testVehicleCreateAtLimitCancels() { + void testVehicleCreateAtLimitCancels() { // Set island-specific MINECART limit to 1, pre-fill with 1 ibc.setEntityLimit(Environment.NORMAL, EntityType.MINECART, 1); ibc.incrementEntity(Environment.NORMAL, EntityType.MINECART); @@ -435,7 +436,7 @@ public void testVehicleCreateAtLimitCancels() { } @Test - public void testVehicleCreateDebounceSkipsSecond() throws Exception { + void testVehicleCreateDebounceSkipsSecond() throws Exception { Field justSpawnedField = EntityLimitListener.class.getDeclaredField("justSpawned"); justSpawnedField.setAccessible(true); @SuppressWarnings("unchecked") @@ -466,7 +467,7 @@ public void testVehicleCreateDebounceSkipsSecond() throws Exception { // --- HangingPlaceEvent at-limit and bypass tests --- @Test - public void testHangingPlaceAtLimitCancels() { + void testHangingPlaceAtLimitCancels() { // Set island-specific PAINTING limit to 1, pre-fill with 1 ibc.setEntityLimit(Environment.NORMAL, EntityType.PAINTING, 1); ibc.incrementEntity(Environment.NORMAL, EntityType.PAINTING); @@ -490,7 +491,7 @@ public void testHangingPlaceAtLimitCancels() { } @Test - public void testHangingPlaceOpPlayerBypasses() { + void testHangingPlaceOpPlayerBypasses() { // Set island-specific PAINTING limit to 1, pre-fill with 1 ibc.setEntityLimit(Environment.NORMAL, EntityType.PAINTING, 1); Painting painting = mock(Painting.class); @@ -518,7 +519,7 @@ public void testHangingPlaceOpPlayerBypasses() { // --- Entity group limit tests --- @Test - public void testEntityGroupLimitBlocksSpawnWhenGroupFull() { + void testEntityGroupLimitBlocksSpawnWhenGroupFull() { // Create a custom group "testanimals" covering CHICKEN and COW with limit 2 EntityGroup testGroup = new EntityGroup("testanimals", Set.of(EntityType.CHICKEN, EntityType.COW), 2, Material.BARRIER); Settings settings = addon.getSettings(); @@ -552,7 +553,7 @@ public void testEntityGroupLimitBlocksSpawnWhenGroupFull() { } @Test - public void testEntityGroupLimitAllowsSpawnWhenGroupUnderLimit() { + void testEntityGroupLimitAllowsSpawnWhenGroupUnderLimit() { // Create a custom group "testanimals" covering CHICKEN and COW with limit 3 EntityGroup testGroup = new EntityGroup("testanimals", Set.of(EntityType.CHICKEN, EntityType.COW), 3, Material.BARRIER); Settings settings = addon.getSettings(); @@ -590,7 +591,7 @@ private LivingEntity mockEntity(EntityType type, Location location) { // --- Golem / snowman block-removal tests (#127) --- @Test - public void testDetectIronGolemRemovesAllBlocksWhenSpawnedAtBody() throws Exception { + void testDetectIronGolemRemovesAllBlocksWhenSpawnedAtBody() throws Exception { grid = new HashMap<>(); Block base = setBlock(0, 64, 0, Material.IRON_BLOCK); Block body = setBlock(0, 65, 0, Material.IRON_BLOCK); @@ -609,7 +610,7 @@ public void testDetectIronGolemRemovesAllBlocksWhenSpawnedAtBody() throws Except } @Test - public void testDetectIronGolemRemovesAllBlocksWhenSpawnedAtBase() throws Exception { + void testDetectIronGolemRemovesAllBlocksWhenSpawnedAtBase() throws Exception { grid = new HashMap<>(); Block base = setBlock(0, 64, 0, Material.IRON_BLOCK); Block body = setBlock(0, 65, 0, Material.IRON_BLOCK); @@ -626,7 +627,7 @@ public void testDetectIronGolemRemovesAllBlocksWhenSpawnedAtBase() throws Except } @Test - public void testDetectSnowmanRemovesAllBlocks() throws Exception { + void testDetectSnowmanRemovesAllBlocks() throws Exception { grid = new HashMap<>(); Block base = setBlock(0, 64, 0, Material.SNOW_BLOCK); Block body = setBlock(0, 65, 0, Material.SNOW_BLOCK); @@ -641,7 +642,7 @@ public void testDetectSnowmanRemovesAllBlocks() throws Exception { } @Test - public void testDetectIronGolemLeavesUnrelatedBlocksAlone() throws Exception { + void testDetectIronGolemLeavesUnrelatedBlocksAlone() throws Exception { grid = new HashMap<>(); // A lone iron block with no pumpkin is not a golem — nothing may be erased. Block stray = setBlock(0, 65, 0, Material.IRON_BLOCK); @@ -688,7 +689,7 @@ private void invokeDetect(String method, Location loc) throws Exception { // --- Spawn-egg interact guard tests (#134) --- @Test - public void testSpawnEggOnEntityAtLimitIsCancelled() { + void testSpawnEggOnEntityAtLimitIsCancelled() { ibc.setEntityLimit(Environment.NORMAL, EntityType.ENDERMAN, 4); // seeded count is 4 -> at limit Player p = eggPlayer(Material.ENDERMAN_SPAWN_EGG, EquipmentSlot.HAND); Entity clicked = mock(Entity.class); @@ -702,7 +703,7 @@ public void testSpawnEggOnEntityAtLimitIsCancelled() { } @Test - public void testSpawnEggOnEntityUnderLimitNotCancelled() { + void testSpawnEggOnEntityUnderLimitNotCancelled() { ibc.setEntityLimit(Environment.NORMAL, EntityType.ENDERMAN, 10); // count 4 < 10 Player p = eggPlayer(Material.ENDERMAN_SPAWN_EGG, EquipmentSlot.HAND); Entity clicked = mock(Entity.class); @@ -715,7 +716,7 @@ public void testSpawnEggOnEntityUnderLimitNotCancelled() { } @Test - public void testNonSpawnEggOnEntityIgnored() { + void testNonSpawnEggOnEntityIgnored() { ibc.setEntityLimit(Environment.NORMAL, EntityType.ENDERMAN, 4); Player p = eggPlayer(Material.STICK, EquipmentSlot.HAND); Entity clicked = mock(Entity.class); @@ -728,7 +729,7 @@ public void testNonSpawnEggOnEntityIgnored() { } @Test - public void testSpawnEggOnBlockAtLimitIsCancelled() { + void testSpawnEggOnBlockAtLimitIsCancelled() { ibc.setEntityLimit(Environment.NORMAL, EntityType.ENDERMAN, 4); Player p = mock(Player.class); when(p.isOp()).thenReturn(false); @@ -741,7 +742,8 @@ public void testSpawnEggOnBlockAtLimitIsCancelled() { ell.onSpawnEggUseOnBlock(e); - assertTrue(e.isCancelled()); + // PlayerInteractEvent.isCancelled() is deprecated; cancelling denies the item-in-hand use + assertEquals(Event.Result.DENY, e.useItemInHand()); } private Player eggPlayer(Material item, EquipmentSlot hand) { diff --git a/src/test/java/world/bentobox/limits/listeners/PaperShulkerLimitListenerTest.java b/src/test/java/world/bentobox/limits/listeners/PaperShulkerLimitListenerTest.java index c33c729..ab1c361 100644 --- a/src/test/java/world/bentobox/limits/listeners/PaperShulkerLimitListenerTest.java +++ b/src/test/java/world/bentobox/limits/listeners/PaperShulkerLimitListenerTest.java @@ -1,7 +1,6 @@ package world.bentobox.limits.listeners; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -31,7 +30,7 @@ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) -public class PaperShulkerLimitListenerTest { +class PaperShulkerLimitListenerTest { @Mock private Limits addon; diff --git a/src/test/java/world/bentobox/limits/objects/EntityLimitsDOTest.java b/src/test/java/world/bentobox/limits/objects/EntityLimitsDOTest.java index 3d4b45b..504b294 100644 --- a/src/test/java/world/bentobox/limits/objects/EntityLimitsDOTest.java +++ b/src/test/java/world/bentobox/limits/objects/EntityLimitsDOTest.java @@ -1,7 +1,6 @@ package world.bentobox.limits.objects; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -12,30 +11,30 @@ import org.junit.jupiter.api.Test; -public class EntityLimitsDOTest { +class EntityLimitsDOTest { @Test - public void testDefaultConstructorCreatesEmptySpawnLoc() { + void testDefaultConstructorCreatesEmptySpawnLoc() { EntityLimitsDO eld = new EntityLimitsDO(); assertNotNull(eld.getSpawnLoc()); assertTrue(eld.getSpawnLoc().isEmpty()); } @Test - public void testConstructorWithIdSetsUniqueId() { + void testConstructorWithIdSetsUniqueId() { EntityLimitsDO eld = new EntityLimitsDO("test-id"); assertEquals("test-id", eld.getUniqueId()); } @Test - public void testGetSetUniqueId() { + void testGetSetUniqueId() { EntityLimitsDO eld = new EntityLimitsDO(); eld.setUniqueId("new-id"); assertEquals("new-id", eld.getUniqueId()); } @Test - public void testGetSetSpawnLoc() { + void testGetSetSpawnLoc() { EntityLimitsDO eld = new EntityLimitsDO("id"); Map spawnLoc = new HashMap<>(); UUID uuid = UUID.randomUUID(); @@ -46,46 +45,47 @@ public void testGetSetSpawnLoc() { } @Test - public void testEqualsSameId() { + void testEqualsSameId() { EntityLimitsDO a = new EntityLimitsDO("same"); EntityLimitsDO b = new EntityLimitsDO("same"); assertEquals(a, b); } @Test - public void testEqualsDifferentId() { + void testEqualsDifferentId() { EntityLimitsDO a = new EntityLimitsDO("one"); EntityLimitsDO b = new EntityLimitsDO("two"); assertNotEquals(a, b); } @Test - public void testEqualsNull() { + void testEqualsNull() { EntityLimitsDO a = new EntityLimitsDO("one"); - assertFalse(a.equals(null)); + // assertNotEquals(a, null) so a.equals(null) is actually exercised + assertNotEquals(a, null); } @Test - public void testEqualsSameInstance() { + void testEqualsSameInstance() { EntityLimitsDO a = new EntityLimitsDO("one"); assertEquals(a, a); } @Test - public void testEqualsDifferentClass() { + void testEqualsDifferentClass() { EntityLimitsDO a = new EntityLimitsDO("one"); assertNotEquals("one", a); } @Test - public void testHashCodeSameId() { + void testHashCodeSameId() { EntityLimitsDO a = new EntityLimitsDO("same"); EntityLimitsDO b = new EntityLimitsDO("same"); assertEquals(a.hashCode(), b.hashCode()); } @Test - public void testHashCodeDifferentId() { + void testHashCodeDifferentId() { EntityLimitsDO a = new EntityLimitsDO("one"); EntityLimitsDO b = new EntityLimitsDO("two"); assertNotEquals(a.hashCode(), b.hashCode()); diff --git a/src/test/java/world/bentobox/limits/objects/IslandBlockCountTest.java b/src/test/java/world/bentobox/limits/objects/IslandBlockCountTest.java index 7043c51..252f1b7 100644 --- a/src/test/java/world/bentobox/limits/objects/IslandBlockCountTest.java +++ b/src/test/java/world/bentobox/limits/objects/IslandBlockCountTest.java @@ -17,32 +17,32 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; -public class IslandBlockCountTest { +class IslandBlockCountTest { private IslandBlockCount ibc; private NamespacedKey stoneKey; @BeforeEach - public void setUp() { + void setUp() { MockBukkit.mock(); stoneKey = Material.STONE.getKey(); ibc = new IslandBlockCount("island1", "BSkyBlock"); } @AfterEach - public void tearDown() { + void tearDown() { MockBukkit.unmock(); } @Test - public void testConstructorSetsFields() { + void testConstructorSetsFields() { assertEquals("island1", ibc.getUniqueId()); assertEquals("BSkyBlock", ibc.getGameMode()); assertTrue(ibc.isChanged()); } @Test - public void testAddIncrementsCount() { + void testAddIncrementsCount() { ibc.add(Environment.NORMAL, stoneKey); assertEquals(1, ibc.getBlockCount(stoneKey)); ibc.add(Environment.NORMAL, stoneKey); @@ -50,7 +50,7 @@ public void testAddIncrementsCount() { } @Test - public void testRemoveDecrementsAndRemovesAtZero() { + void testRemoveDecrementsAndRemovesAtZero() { ibc.add(Environment.NORMAL, stoneKey); ibc.add(Environment.NORMAL, stoneKey); assertEquals(2, ibc.getBlockCount(stoneKey)); @@ -65,35 +65,35 @@ public void testRemoveDecrementsAndRemovesAtZero() { } @Test - public void testGetBlockCountUnknownMaterial() { + void testGetBlockCountUnknownMaterial() { assertEquals(0, ibc.getBlockCount(stoneKey)); } @Test - public void testGetBlockLimitUnknownMaterial() { + void testGetBlockLimitUnknownMaterial() { assertEquals(-1, ibc.getBlockLimit(Environment.NORMAL, stoneKey)); } @Test - public void testSetBlockLimitThenGet() { + void testSetBlockLimitThenGet() { ibc.setBlockLimit(Environment.NORMAL, stoneKey, 50); assertEquals(50, ibc.getBlockLimit(Environment.NORMAL, stoneKey)); } @Test - public void testIsAtLimitNoLimitSet() { + void testIsAtLimitNoLimitSet() { assertFalse(ibc.isAtLimit(Environment.NORMAL, stoneKey)); } @Test - public void testIsAtLimitCountBelowLimit() { + void testIsAtLimitCountBelowLimit() { ibc.setBlockLimit(Environment.NORMAL, stoneKey, 10); ibc.add(Environment.NORMAL, stoneKey); assertFalse(ibc.isAtLimit(Environment.NORMAL, stoneKey)); } @Test - public void testIsAtLimitCountAtLimit() { + void testIsAtLimitCountAtLimit() { ibc.setBlockLimit(Environment.NORMAL, stoneKey, 2); ibc.add(Environment.NORMAL, stoneKey); ibc.add(Environment.NORMAL, stoneKey); @@ -101,7 +101,7 @@ public void testIsAtLimitCountAtLimit() { } @Test - public void testIsAtLimitWithOffset() { + void testIsAtLimitWithOffset() { // count=10, limit=10, offset=5 → effective limit is 15, so NOT at limit ibc.setBlockLimit(Environment.NORMAL, stoneKey, 10); ibc.setBlockLimitsOffset(Environment.NORMAL, stoneKey, 5); @@ -112,7 +112,7 @@ public void testIsAtLimitWithOffset() { } @Test - public void testIsAtLimitOverloadWithOffset() { + void testIsAtLimitOverloadWithOffset() { ibc.setBlockLimitsOffset(Environment.NORMAL, stoneKey, 5); for (int i = 0; i < 10; i++) { ibc.add(Environment.NORMAL, stoneKey); @@ -124,14 +124,14 @@ public void testIsAtLimitOverloadWithOffset() { } @Test - public void testIsBlockLimited() { + void testIsBlockLimited() { assertFalse(ibc.isBlockLimited(Environment.NORMAL, stoneKey)); ibc.setBlockLimit(Environment.NORMAL, stoneKey, 10); assertTrue(ibc.isBlockLimited(Environment.NORMAL, stoneKey)); } @Test - public void testEntityLimits() { + void testEntityLimits() { assertEquals(-1, ibc.getEntityLimit(Environment.NORMAL, EntityType.ZOMBIE)); ibc.setEntityLimit(Environment.NORMAL, EntityType.ZOMBIE, 25); assertEquals(25, ibc.getEntityLimit(Environment.NORMAL, EntityType.ZOMBIE)); @@ -140,7 +140,7 @@ public void testEntityLimits() { } @Test - public void testEntityGroupLimits() { + void testEntityGroupLimits() { assertEquals(-1, ibc.getEntityGroupLimit(Environment.NORMAL, "monsters")); ibc.setEntityGroupLimit(Environment.NORMAL, "monsters", 30); assertEquals(30, ibc.getEntityGroupLimit(Environment.NORMAL, "monsters")); @@ -149,40 +149,40 @@ public void testEntityGroupLimits() { } @Test - public void testBlockLimitsOffset() { + void testBlockLimitsOffset() { assertEquals(0, ibc.getBlockLimitOffset(Environment.NORMAL, stoneKey)); ibc.setBlockLimitsOffset(Environment.NORMAL, stoneKey, 7); assertEquals(7, ibc.getBlockLimitOffset(Environment.NORMAL, stoneKey)); } @Test - public void testEntityLimitsOffset() { + void testEntityLimitsOffset() { assertEquals(0, ibc.getEntityLimitOffset(Environment.NORMAL, EntityType.ZOMBIE)); ibc.setEntityLimitsOffset(Environment.NORMAL, EntityType.ZOMBIE, 3); assertEquals(3, ibc.getEntityLimitOffset(Environment.NORMAL, EntityType.ZOMBIE)); } @Test - public void testEntityGroupLimitsOffset() { + void testEntityGroupLimitsOffset() { assertEquals(0, ibc.getEntityGroupLimitOffset(Environment.NORMAL, "monsters")); ibc.setEntityGroupLimitsOffset(Environment.NORMAL, "monsters", 4); assertEquals(4, ibc.getEntityGroupLimitOffset(Environment.NORMAL, "monsters")); } @Test - public void testSetChangedFalse() { + void testSetChangedFalse() { ibc.setChanged(false); assertFalse(ibc.isChanged()); } @Test - public void testIsGameMode() { + void testIsGameMode() { assertTrue(ibc.isGameMode("BSkyBlock")); assertFalse(ibc.isGameMode("AcidIsland")); } @Test - public void testSetAndGetUniqueId() { + void testSetAndGetUniqueId() { ibc.setUniqueId("island2"); assertEquals("island2", ibc.getUniqueId()); } From 1ad244f3f868248fe9bbe05ad2ee4bf51dcaa16e Mon Sep 17 00:00:00 2001 From: tastybento Date: Sat, 13 Jun 2026 09:53:12 -0700 Subject: [PATCH 24/24] Sync locale keys and convert color codes to MiniMessage Add the four keys introduced this release to all 20 translation files: admin.limits.offset.description (reused each file's existing translation, previously orphaned under offset.main.description) and the panel env-overworld/env-nether/env-end dimension names, localized per language. Convert 375 legacy & color codes to MiniMessage tags across all 21 locale files (e.g. &c -> , &a -> ). Also fix eight pre-existing broken codes in translations where the colour letter had been dropped (hr, es, uk, tr messages and it regular-color) to . CRLF line endings preserved. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/main/resources/locales/cs.yml | 40 +++++++++++++------------ src/main/resources/locales/de.yml | 40 +++++++++++++------------ src/main/resources/locales/en-US.yml | 36 +++++++++++------------ src/main/resources/locales/es.yml | 40 +++++++++++++------------ src/main/resources/locales/fr.yml | 40 +++++++++++++------------ src/main/resources/locales/hr.yml | 38 +++++++++++++----------- src/main/resources/locales/hu.yml | 40 +++++++++++++------------ src/main/resources/locales/id.yml | 40 +++++++++++++------------ src/main/resources/locales/it.yml | 40 +++++++++++++------------ src/main/resources/locales/ja.yml | 36 +++++++++++++---------- src/main/resources/locales/ko.yml | 40 +++++++++++++------------ src/main/resources/locales/lv.yml | 38 +++++++++++++----------- src/main/resources/locales/nl.yml | 40 +++++++++++++------------ src/main/resources/locales/pl.yml | 40 +++++++++++++------------ src/main/resources/locales/pt.yml | 40 +++++++++++++------------ src/main/resources/locales/ro.yml | 40 +++++++++++++------------ src/main/resources/locales/tr.yml | 44 +++++++++++++++------------- src/main/resources/locales/uk.yml | 38 +++++++++++++----------- src/main/resources/locales/vi.yml | 40 +++++++++++++------------ src/main/resources/locales/zh-CN.yml | 40 +++++++++++++------------ src/main/resources/locales/zh-TW.yml | 40 +++++++++++++------------ 21 files changed, 455 insertions(+), 375 deletions(-) diff --git a/src/main/resources/locales/cs.yml b/src/main/resources/locales/cs.yml index bab9a97..e069d1e 100644 --- a/src/main/resources/locales/cs.yml +++ b/src/main/resources/locales/cs.yml @@ -1,8 +1,8 @@ --- block-limits: - hit-limit: "&c[material] omezen na [number]!" + hit-limit: "[material] omezen na [number]!" entity-limits: - hit-limit: "&cSpawnování [entity] omezeno na [number]!" + hit-limit: "Spawnování [entity] omezeno na [number]!" limits: panel-title: Omezení ostrovů admin: @@ -13,51 +13,55 @@ admin: calc: parameters: "" description: přepočítat omezení ostrova hráče - finished: "&aPřepočítání ostrova úspěšně dokončeno!" + finished: "Přepočítání ostrova úspěšně dokončeno!" offset: - unknown: "&c Neznámý materiál nebo entita [name]." + unknown: " Neznámý materiál nebo entita [name]." + description: "umožňuje spravovat limity offsetů pro materiály a entity" main: description: umožňuje spravovat limity offsetů pro materiály a entity set: parameters: " <číslo>" description: nastaví nový offset pro limit materiálu nebo entity - success: "&a Mezní posun pro [name] je nastaven na [number]." - same: "&c Mezní posun pro [name] je již [number]." + success: " Mezní posun pro [name] je nastaven na [number]." + same: " Mezní posun pro [name] je již [number]." add: parameters: " <číslo>" description: přidá offset pro limit materiálu nebo entity - success: "&a Posun limitu pro [name] se zvýší na [number]." + success: " Posun limitu pro [name] se zvýší na [number]." remove: parameters: " <číslo>" description: snižuje offset pro limit materiálu nebo entity - success: "&a Posun limitu pro [name] se sníží na [number]." + success: " Posun limitu pro [name] se sníží na [number]." reset: parameters: " " description: odstraní offset pro materiál nebo entitu - success: "&a Mezní posun pro [jméno] je nastaven na 0." + success: " Mezní posun pro [jméno] je nastaven na 0." view: parameters: " " description: zobrazuje posun pro materiál nebo entitu - message: "&odsazení [name] je nastaveno na [number]." + message: "dsazení [name] je nastaveno na [number]." island: limits: description: ukázat omezení tvého ostrova - max-color: "&c" - regular-color: "&a" + max-color: "" + regular-color: "" block-limit-syntax: "[number]/[limit]" - no-limits: "&c V tomto světě nejsou stanovena žádná omezení" + no-limits: " V tomto světě nejsou stanovena žádná omezení" panel: title-syntax: "[title] [sort]" entity-group-name-syntax: "[name]" entity-name-syntax: "[name]" block-name-syntax: "[name]" + env-overworld: "Přesvět" + env-nether: "Nether" + env-end: "Konec" A2Z: a > z Z2A: z > a errors: - no-owner: "&c Tento ostrov nemá vlastníka" - not-on-island: "&c Toto umístění nemá nastavena omezení." + no-owner: " Tento ostrov nemá vlastníka" + not-on-island: " Toto umístění nemá nastavena omezení." recount: description: přepočítá omezení tvého ostrova - now-recounting: "&b Nyní vyprávění. Může to chvíli trvat, čekejte prosím..." - in-progress: "&c Probíhá obnovení ostrova. Čekejte prosím..." - time-out: "&c Časový limit při přepočítávání. Je ostrov opravdu velký?" + now-recounting: " Nyní vyprávění. Může to chvíli trvat, čekejte prosím..." + in-progress: " Probíhá obnovení ostrova. Čekejte prosím..." + time-out: " Časový limit při přepočítávání. Je ostrov opravdu velký?" diff --git a/src/main/resources/locales/de.yml b/src/main/resources/locales/de.yml index 5400e29..a741c85 100644 --- a/src/main/resources/locales/de.yml +++ b/src/main/resources/locales/de.yml @@ -1,8 +1,8 @@ --- block-limits: - hit-limit: "&c[material] limitiert auf [number]!" + hit-limit: "[material] limitiert auf [number]!" entity-limits: - hit-limit: "&c[entity] spawning limitiert auf [number]!" + hit-limit: "[entity] spawning limitiert auf [number]!" limits: panel-title: Insel Limitierungen admin: @@ -13,52 +13,56 @@ admin: calc: parameters: "" description: Berechne die Insel Limitierungen für den Spieler neu - finished: "&aInselberechnung erfolgreich abgeschlossen!" + finished: "Inselberechnung erfolgreich abgeschlossen!" offset: - unknown: "&c Unbekanntes Material oder Entität [name]." + unknown: " Unbekanntes Material oder Entität [name]." + description: "ermöglicht die Verwaltung von Grenzwertverschiebungen für Materialien und Entitäten" main: description: ermöglicht die Verwaltung von Grenzwertverschiebungen für Materialien und Entitäten set: parameters: " " description: legt einen neuen Offset für Material- oder Entity-Grenzwert fest - success: "&a Der Grenzoffset für [name] ist auf [number] eingestellt." - same: "&c Der Grenzoffset für [name] ist bereits [number]." + success: " Der Grenzoffset für [name] ist auf [number] eingestellt." + same: " Der Grenzoffset für [name] ist bereits [number]." add: parameters: " " description: fügt einen Offset für Material- oder Entitätslimit hinzu - success: "&a Der Limit-Offset für [name] wird bis [number] erhöht." + success: " Der Limit-Offset für [name] wird bis [number] erhöht." remove: parameters: " " description: reduziert den Offset für Material- oder Entitätslimit - success: "&a Der Grenzoffset für [name] wird auf [number] reduziert." + success: " Der Grenzoffset für [name] wird auf [number] reduziert." reset: parameters: " " description: Entfernt den Offset für Material oder Entität - success: "&a Der Grenzoffset für [name] wird auf 0 gesetzt." + success: " Der Grenzoffset für [name] wird auf 0 gesetzt." view: parameters: " " description: zeigt den Offset für Material oder Entität an - message: "&a [name]-Offset ist auf [number] gesetzt." + message: " [name]-Offset ist auf [number] gesetzt." island: limits: description: Zeige deine Insel Limitierungen - max-color: "&c" - regular-color: "&a" + max-color: "" + regular-color: "" block-limit-syntax: "[number]/[limit]" - no-limits: "&c Es gibt keine Grenzen in dieser Welt" + no-limits: " Es gibt keine Grenzen in dieser Welt" panel: title-syntax: "[title] [sort]" entity-group-name-syntax: "[name]" entity-name-syntax: "[name]" block-name-syntax: "[name]" + env-overworld: "Oberwelt" + env-nether: "Nether" + env-end: "Ende" A2Z: a > z Z2A: z > a errors: - no-owner: "&c Diese Insel hat keinen Besitzer" - not-on-island: "&c Für diesen Standort sind keine Beschränkungen festgelegt." + no-owner: " Diese Insel hat keinen Besitzer" + not-on-island: " Für diesen Standort sind keine Beschränkungen festgelegt." recount: description: Zählt die Limitierungen für deine Insel auf - now-recounting: "&b Ich erzähle jetzt. Dies kann eine Weile dauern, bitte warten..." - in-progress: "&c Inselrückmeldung läuft. Warten Sie mal..." - time-out: "&c Zeitüberschreitung beim Erzählen. Ist die Insel wirklich so groß?" + now-recounting: " Ich erzähle jetzt. Dies kann eine Weile dauern, bitte warten..." + in-progress: " Inselrückmeldung läuft. Warten Sie mal..." + time-out: " Zeitüberschreitung beim Erzählen. Ist die Insel wirklich so groß?" diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index 1d2b10b..477425b 100755 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -4,9 +4,9 @@ ########################################################################################### block-limits: - hit-limit: "&c[material] limited to [number]!" + hit-limit: "[material] limited to [number]!" entity-limits: - hit-limit: "&c[entity] spawning limited to [number]!" + hit-limit: "[entity] spawning limited to [number]!" limits: panel-title: "Island limits" @@ -18,38 +18,38 @@ admin: calc: parameters: "" description: "recalculate the island limits for player" - finished: "&a Island recalc finished successfully!" + finished: " Island recalc finished successfully!" offset: - unknown: "&c Unknown material or entity [name]." + unknown: " Unknown material or entity [name]." description: "allows to manage limits offsets for materials and entities" set: parameters: " " description: "sets new offset for material or entity limit" - success: "&a Limit offset for [name] is set to [number]." - same: "&c Limit offset for [name] is already [number]." + success: " Limit offset for [name] is set to [number]." + same: " Limit offset for [name] is already [number]." add: parameters: " " description: "adds offset for material or entity limit" - success: "&a Limit offset for [name] is increased till [number]." + success: " Limit offset for [name] is increased till [number]." remove: parameters: " " description: "reduces offset for material or entity limit" - success: "&a Limit offset for [name] is reduced till [number]." + success: " Limit offset for [name] is reduced till [number]." reset: parameters: " " description: "removes offset for material or entity" - success: "&a Limit offset for [name] is set to 0." + success: " Limit offset for [name] is set to 0." view: parameters: " " description: "displays offset for material or entity" - message: "&a [name] offset is set to [number]." + message: " [name] offset is set to [number]." island: limits: description: "show your island limits" - max-color: "&c" - regular-color: "&a" + max-color: "" + regular-color: "" block-limit-syntax: "[number]/[limit]" - no-limits: "&c No limits set in this world" + no-limits: " No limits set in this world" panel: title-syntax: '[title] [env]' entity-group-name-syntax: '[name]' @@ -59,11 +59,11 @@ island: env-nether: "Nether" env-end: "End" errors: - no-owner: "&c That island has no owner" - not-on-island: "&c This location does not have limits set." + no-owner: " That island has no owner" + not-on-island: " This location does not have limits set." recount: description: "recounts limits for your island" - now-recounting: "&b Now recounting. This could take a while, please wait..." - in-progress: "&c Island recound is in progress. Please wait..." - time-out: "&c Time out when recounting. Is the island really big?" + now-recounting: " Now recounting. This could take a while, please wait..." + in-progress: " Island recound is in progress. Please wait..." + time-out: " Time out when recounting. Is the island really big?" diff --git a/src/main/resources/locales/es.yml b/src/main/resources/locales/es.yml index 89a4ce4..1af7ab7 100644 --- a/src/main/resources/locales/es.yml +++ b/src/main/resources/locales/es.yml @@ -1,8 +1,8 @@ --- block-limits: - hit-limit: "&c[material] limitado a [number]!" + hit-limit: "[material] limitado a [number]!" entity-limits: - hit-limit: "¡Aparición de &c[entity] limitada a [number]!" + hit-limit: "¡Aparición de [entity] limitada a [number]!" limits: panel-title: Límites de la isla admin: @@ -13,9 +13,10 @@ admin: calc: parameters: "" description: recalcular los límites de la isla para el jugador - finished: "&a ¡El recálculo de la Isla terminó con éxito!" + finished: " ¡El recálculo de la Isla terminó con éxito!" offset: - unknown: "&c Material o entidad desconocida [name]." + unknown: " Material o entidad desconocida [name]." + description: "permite gestionar compensaciones de límites para materiales y entidades" main: description: permite gestionar compensaciones de límites para materiales y entidades @@ -23,43 +24,46 @@ admin: parameters: " " description: establece una nueva compensación para el límite de material o entidad - success: "&a El desplazamiento límite para [name] está establecido en [number]." - same: "&c El límite de compensación para [name] ya es [number]." + success: " El desplazamiento límite para [name] está establecido en [number]." + same: " El límite de compensación para [name] ya es [number]." add: parameters: " " description: agrega compensación por límite de material o entidad - success: "&a El límite de compensación para [name] aumenta hasta [number]." + success: " El límite de compensación para [name] aumenta hasta [number]." remove: parameters: " " description: reduce la compensación por límite de material o entidad - success: "&a La compensación límite para [name] se reduce hasta [number]." + success: " La compensación límite para [name] se reduce hasta [number]." reset: parameters: " " description: elimina la compensación para material o entidad - success: "&a El desplazamiento límite para [number] está establecido en 0." + success: " El desplazamiento límite para [number] está establecido en 0." view: parameters: " " description: muestra el desplazamiento para material o entidad - message: "& desplazamiento de [name] se establece en [number]." + message: " desplazamiento de [name] se establece en [number]." island: limits: description: muestra los límites de tu isla - max-color: "&c" - regular-color: "&a" + max-color: "" + regular-color: "" block-limit-syntax: "[number]/[limit]" - no-limits: "&c No hay límites establecidos en este mundo." + no-limits: " No hay límites establecidos en este mundo." panel: title-syntax: "[title] [sort]" entity-group-name-syntax: "[name]" entity-name-syntax: "[name]" block-name-syntax: "[name]" + env-overworld: "Superficie" + env-nether: "Nether" + env-end: "El End" A2Z: a > z Z2A: z > a errors: - no-owner: "&c Esa isla no tiene dueño" - not-on-island: "&c Esta ubicación no tiene límites establecidos." + no-owner: " Esa isla no tiene dueño" + not-on-island: " Esta ubicación no tiene límites establecidos." recount: description: cuenta los límites para tu isla - now-recounting: "&b Ahora contando. Esto podría tomar un tiempo, por favor espere..." - in-progress: "&c El recuento de la isla está en progreso. Espere por favor..." - time-out: "&c Ha pasado demasiado tiempo contando. ¿La isla es realmente grande?" + now-recounting: " Ahora contando. Esto podría tomar un tiempo, por favor espere..." + in-progress: " El recuento de la isla está en progreso. Espere por favor..." + time-out: " Ha pasado demasiado tiempo contando. ¿La isla es realmente grande?" diff --git a/src/main/resources/locales/fr.yml b/src/main/resources/locales/fr.yml index 6c1891f..25ebb40 100644 --- a/src/main/resources/locales/fr.yml +++ b/src/main/resources/locales/fr.yml @@ -1,8 +1,8 @@ --- block-limits: - hit-limit: "&c[material] limité à [number]!" + hit-limit: "[material] limité à [number]!" entity-limits: - hit-limit: "&c[entity] spawning limited to [number]!" + hit-limit: "[entity] spawning limited to [number]!" limits: panel-title: Limites de l'île admin: @@ -13,54 +13,58 @@ admin: calc: parameters: "" description: recalcule les limites de l'île pour le joueur - finished: "&a Recomptage terminé avec succès!" + finished: " Recomptage terminé avec succès!" offset: - unknown: "&c Matériau ou entité inconnu [name]." + unknown: " Matériau ou entité inconnu [name]." + description: "permet de gérer les décalages de limites pour les matériaux et les entités" main: description: permet de gérer les décalages de limites pour les matériaux et les entités set: parameters: " " description: définit un nouveau décalage pour la limite de matériau ou d'entité - success: "&a Le décalage limite pour [name] est défini sur [number]." - same: "&c Le décalage limite pour [name] est déjà [number]." + success: " Le décalage limite pour [name] est défini sur [number]." + same: " Le décalage limite pour [name] est déjà [number]." add: parameters: " " description: ajoute un décalage pour la limite de matériau ou d'entité - success: "&a Le décalage limite pour [name] est augmenté jusqu'à [number]." + success: " Le décalage limite pour [name] est augmenté jusqu'à [number]." remove: parameters: " " description: réduit le décalage pour la limite de matériau ou d'entité - success: "&a Le décalage limite pour [name] est réduit jusqu'à [number]." + success: " Le décalage limite pour [name] est réduit jusqu'à [number]." reset: parameters: " " description: supprime le décalage pour le matériau ou l'entité - success: "&a Le décalage limite pour [name] est défini sur 0." + success: " Le décalage limite pour [name] est défini sur 0." view: parameters: " " description: affiche le décalage pour le matériau ou l'entité - message: "&a [name] le décalage est défini sur [number]." + message: " [name] le décalage est défini sur [number]." island: limits: description: affichez les limites de votre île - max-color: "&c" - regular-color: "&a" + max-color: "" + regular-color: "" block-limit-syntax: "[number]/[limit]" - no-limits: "&c Aucune limite n'est fixée dans ce monde" + no-limits: " Aucune limite n'est fixée dans ce monde" panel: title-syntax: "[title] [sort]" entity-group-name-syntax: "[name]" entity-name-syntax: "[name]" block-name-syntax: "[name]" + env-overworld: "Surface" + env-nether: "Nether" + env-end: "l'End" A2Z: a > z Z2A: z > a errors: - no-owner: "&c Cette île n'a pas de propriétaire" - not-on-island: "&c Cet emplacement n'a pas de limites définies." + no-owner: " Cette île n'a pas de propriétaire" + not-on-island: " Cet emplacement n'a pas de limites définies." recount: description: recompte les limites de votre île - now-recounting: "&b Recomptage en cours. Cela peut prendre un certain temps, + now-recounting: " Recomptage en cours. Cela peut prendre un certain temps, veuillez patienter..." - in-progress: "&c Le recomptage de l'île est en cours. Veuillez patienter s'il + in-progress: " Le recomptage de l'île est en cours. Veuillez patienter s'il vous plaît..." - time-out: "&c Time out lors du recomptage. L'île est-elle vraiment grande?" + time-out: " Time out lors du recomptage. L'île est-elle vraiment grande?" diff --git a/src/main/resources/locales/hr.yml b/src/main/resources/locales/hr.yml index 5c10936..9d571e8 100644 --- a/src/main/resources/locales/hr.yml +++ b/src/main/resources/locales/hr.yml @@ -1,8 +1,8 @@ --- block-limits: - hit-limit: "&c[material] ograničen na [number]!" + hit-limit: "[material] ograničen na [number]!" entity-limits: - hit-limit: "&c[entity] stvaranje ograničeno na [number]!" + hit-limit: "[entity] stvaranje ograničeno na [number]!" limits: panel-title: Granice otoka admin: @@ -13,51 +13,55 @@ admin: calc: parameters: "" description: ponovno izračunati ograničenja otoka za igrača - finished: "&a Recalc otoka uspješno završen!" + finished: " Recalc otoka uspješno završen!" offset: - unknown: "&c Nepoznati materijal ili entitet [name]." + unknown: " Nepoznati materijal ili entitet [name]." + description: "omogućuje upravljanje pomacima ograničenja za materijale i entitete" main: description: omogućuje upravljanje pomacima ograničenja za materijale i entitete set: parameters: " " description: postavlja novi pomak za ograničenje materijala ili entiteta - success: "&a Ograničenje pomaka za [name] postavljeno je na [number]." - same: "&c Ograničenje pomaka za [name] već je [number]." + success: " Ograničenje pomaka za [name] postavljeno je na [number]." + same: " Ograničenje pomaka za [name] već je [number]." add: parameters: " " description: dodaje pomak za ograničenje materijala ili entiteta - success: "&a Ograničenje pomaka za [name] povećava se do [number]." + success: " Ograničenje pomaka za [name] povećava se do [number]." remove: parameters: " " description: smanjuje pomak za ograničenje materijala ili entiteta - success: "&a Ograničenje pomaka za [name] smanjeno je do [number]." + success: " Ograničenje pomaka za [name] smanjeno je do [number]." reset: parameters: " " description: uklanja pomak za materijal ili entitet - success: "&a Ograničenje pomaka za [name] postavljeno je na 0." + success: " Ograničenje pomaka za [name] postavljeno je na 0." view: parameters: " " description: prikazuje pomak za materijal ili entitet - message: "&pomak [name] postavljen je na [number]." + message: "pomak [name] postavljen je na [number]." island: limits: description: pokazati granice vašeg otoka max-color: i c - regular-color: "&a" + regular-color: "" block-limit-syntax: "[number]/[limit]" - no-limits: "&c U ovom svijetu nema ograničenja" + no-limits: " U ovom svijetu nema ograničenja" panel: title-syntax: "[title] [sort]" entity-group-name-syntax: "[name]" entity-name-syntax: "[name]" block-name-syntax: "[name]" + env-overworld: "Nadsvijet" + env-nether: "Nether" + env-end: "Kraj" A2Z: a > z Z2A: z > a errors: - no-owner: "&c Taj otok nema vlasnika" - not-on-island: "&c Ova lokacija nema postavljena ograničenja." + no-owner: " Taj otok nema vlasnika" + not-on-island: " Ova lokacija nema postavljena ograničenja." recount: description: preračunava ograničenja za vaš otok - now-recounting: "&b Sada prepričavam. Ovo bi moglo potrajati, pričekajte..." - in-progress: "&c Otok je u tijeku. Molimo pričekajte..." - time-out: "&c Time out kod prepričavanja. Je li otok stvarno velik?" + now-recounting: " Sada prepričavam. Ovo bi moglo potrajati, pričekajte..." + in-progress: " Otok je u tijeku. Molimo pričekajte..." + time-out: " Time out kod prepričavanja. Je li otok stvarno velik?" diff --git a/src/main/resources/locales/hu.yml b/src/main/resources/locales/hu.yml index 9b3a760..f7d2840 100644 --- a/src/main/resources/locales/hu.yml +++ b/src/main/resources/locales/hu.yml @@ -1,8 +1,8 @@ --- block-limits: - hit-limit: "&c[material] limitálva ennyire [number]!" + hit-limit: "[material] limitálva ennyire [number]!" entity-limits: - hit-limit: "&c[entity] idézés limitálva ennyire [number]!" + hit-limit: "[entity] idézés limitálva ennyire [number]!" limits: panel-title: Sziget limitek admin: @@ -13,51 +13,55 @@ admin: calc: parameters: "" description: Egy játékos sziget limitjének újraszámolása - finished: "&aA Sziget újraszámolás sikeresen befejeződött!" + finished: "A Sziget újraszámolás sikeresen befejeződött!" offset: - unknown: "&c Ismeretlen anyag vagy entitás [name]." + unknown: " Ismeretlen anyag vagy entitás [name]." + description: "lehetővé teszi az anyagok és entitások limiteltolásainak kezelését" main: description: lehetővé teszi az anyagok és entitások limiteltolásainak kezelését set: parameters: " " description: új eltolást állít be az anyag- vagy entitáskorláthoz - success: "&a A [name] határeltolódása [number] értékre van állítva." - same: "&c A [name] korláteltolása már [number]." + success: " A [name] határeltolódása [number] értékre van állítva." + same: " A [name] korláteltolása már [number]." add: parameters: " " description: eltolást ad hozzá az anyag- vagy entitáskorláthoz - success: "&a A [name] határeltolódása a [number] értékig nő." + success: " A [name] határeltolódása a [number] értékig nő." remove: parameters: " " description: csökkenti az anyag- vagy entitáskorlát ellentételezését - success: "&a A [name] limiteltolása [number] értékre csökken." + success: " A [name] limiteltolása [number] értékre csökken." reset: parameters: " " description: eltávolítja az anyag vagy entitás eltolását - success: "&a A [name] határeltolódása 0-ra van állítva." + success: " A [name] határeltolódása 0-ra van állítva." view: parameters: " " description: anyag vagy entitás eltolását jeleníti meg - message: "&a [name] eltolás értéke [number]." + message: " [name] eltolás értéke [number]." island: limits: description: Sziget limitek megtekintése - max-color: "&c" - regular-color: "&a" + max-color: "" + regular-color: "" block-limit-syntax: "[number]/[limit]" - no-limits: "&c Nincsenek korlátok ezen a világon" + no-limits: " Nincsenek korlátok ezen a világon" panel: title-syntax: "[title] [sort]" entity-group-name-syntax: "[name]" entity-name-syntax: "[name]" block-name-syntax: "[name]" + env-overworld: "Felvilág" + env-nether: "Nether" + env-end: "Vég" A2Z: a > z Z2A: z > a errors: - no-owner: "&c Annak a szigetnek nincs gazdája" - not-on-island: "&c Ennek a helynek nincs korlátja beállítva." + no-owner: " Annak a szigetnek nincs gazdája" + not-on-island: " Ennek a helynek nincs korlátja beállítva." recount: description: Limitek újraszámolása a szigeteden - now-recounting: "&b Most mesélünk. Ez eltarthat egy ideig, kérem, várjon..." - in-progress: "&c Sziget visszaállítása folyamatban van. Kérem, várjon..." - time-out: "&c Időtúllépés az újraszámláláskor. Tényleg nagy a sziget?" + now-recounting: " Most mesélünk. Ez eltarthat egy ideig, kérem, várjon..." + in-progress: " Sziget visszaállítása folyamatban van. Kérem, várjon..." + time-out: " Időtúllépés az újraszámláláskor. Tényleg nagy a sziget?" diff --git a/src/main/resources/locales/id.yml b/src/main/resources/locales/id.yml index c365d9c..b9c6822 100644 --- a/src/main/resources/locales/id.yml +++ b/src/main/resources/locales/id.yml @@ -1,8 +1,8 @@ --- block-limits: - hit-limit: "&c[material] dibatasi sampai [number]!" + hit-limit: "[material] dibatasi sampai [number]!" entity-limits: - hit-limit: "&c[entity] spawning dibatasi sampai [number]!" + hit-limit: "[entity] spawning dibatasi sampai [number]!" limits: panel-title: Batasan Pulau admin: @@ -13,53 +13,57 @@ admin: calc: parameters: "" description: menghitung ulang batasan pulau untuk pemain - finished: "&aPenghitungan ulang pulau selesai tanpa masalah!" + finished: "Penghitungan ulang pulau selesai tanpa masalah!" offset: - unknown: "&c Material atau entitas tidak dikenal [name]." + unknown: " Material atau entitas tidak dikenal [name]." + description: "memungkinkan untuk mengelola batas offset untuk material dan entitas" main: description: memungkinkan untuk mengelola batas offset untuk material dan entitas set: parameters: " " description: menetapkan offset baru untuk batasan material atau entitas - success: "&a Batas offset untuk [name] ditetapkan ke [number]." - same: "&a Batas offset untuk [name] ditetapkan ke [number]." + success: " Batas offset untuk [name] ditetapkan ke [number]." + same: " Batas offset untuk [name] ditetapkan ke [number]." add: parameters: " " description: menambahkan offset untuk batasan material atau entitas - success: "&a Batas offset untuk [name] ditingkatkan hingga [number]." + success: " Batas offset untuk [name] ditingkatkan hingga [number]." remove: parameters: " " description: mengurangi offset untuk batas material atau entitas - success: "&a Batas offset untuk [name] dikurangi hingga [number]." + success: " Batas offset untuk [name] dikurangi hingga [number]." reset: parameters: " " description: menghapus offset untuk material atau entitas - success: "&a Batas offset untuk [name] ditetapkan ke 0." + success: " Batas offset untuk [name] ditetapkan ke 0." view: parameters: " " description: menampilkan offset untuk material atau entitas - message: "&a [name] offset diatur ke [number]." + message: " [name] offset diatur ke [number]." island: limits: description: menampilkan batasan pulau kamu - max-color: "&c" - regular-color: "&a" + max-color: "" + regular-color: "" block-limit-syntax: "[number]/[limit]" - no-limits: "&c Tidak ada batasan yang ditetapkan di dunia ini" + no-limits: " Tidak ada batasan yang ditetapkan di dunia ini" panel: title-syntax: "[title] [sort]" entity-group-name-syntax: "[name]" entity-name-syntax: "[name]" block-name-syntax: "[name]" + env-overworld: "Dunia Atas" + env-nether: "Nether" + env-end: "End" A2Z: a > z Z2A: z > a errors: - no-owner: "&c Pulau itu tidak memiliki pemilik" - not-on-island: "&c Lokasi ini tidak memiliki batasan yang ditetapkan." + no-owner: " Pulau itu tidak memiliki pemilik" + not-on-island: " Lokasi ini tidak memiliki batasan yang ditetapkan." recount: description: menghitung ulang batasan untuk pulau kamu - now-recounting: "&b Menghitung ulang. Membutuhkan waktu beberapa saat, silahkan + now-recounting: " Menghitung ulang. Membutuhkan waktu beberapa saat, silahkan tunggu..." - in-progress: "&c Perhitungan pulau sedang diproses. Silahkan tunggu..." - time-out: "&c Gagal menghitung ulang. Apakah pulau terlalu besar?" + in-progress: " Perhitungan pulau sedang diproses. Silahkan tunggu..." + time-out: " Gagal menghitung ulang. Apakah pulau terlalu besar?" diff --git a/src/main/resources/locales/it.yml b/src/main/resources/locales/it.yml index 53fb2ca..af5f909 100644 --- a/src/main/resources/locales/it.yml +++ b/src/main/resources/locales/it.yml @@ -1,8 +1,8 @@ --- block-limits: - hit-limit: "&c[material] limitato a [number]!" + hit-limit: "[material] limitato a [number]!" entity-limits: - hit-limit: "&c[entity] la generazione è limitata a [number]!" + hit-limit: "[entity] la generazione è limitata a [number]!" limits: panel-title: Limiti dell'isola admin: @@ -13,51 +13,55 @@ admin: calc: parameters: "" description: ricalcola i limiti dell'isola per il giocatore - finished: "&a Il ricalcolo dell'isola è stato completato con successo!" + finished: " Il ricalcolo dell'isola è stato completato con successo!" offset: - unknown: "&c Materiale o entità sconosciuta [name]." + unknown: " Materiale o entità sconosciuta [name]." + description: "consente di gestire gli offset dei limiti per materiali ed entità" main: description: consente di gestire gli offset dei limiti per materiali ed entità set: parameters: " " description: imposta un nuovo offset per il limite di materiale o entità - success: "&a Il limite di offset per [name] è impostato su [number]." - same: "&c Il limite di offset per [name] è già [number]." + success: " Il limite di offset per [name] è impostato su [number]." + same: " Il limite di offset per [name] è già [number]." add: parameters: " " description: aggiunge offset per limite di materiale o entità - success: "&a Il limite di offset per [name] è aumentato fino a [number]." + success: " Il limite di offset per [name] è aumentato fino a [number]." remove: parameters: " " description: riduce l'offset per il limite di materiale o entità - success: "&a Il limite di offset per [name] è ridotto fino a [number]." + success: " Il limite di offset per [name] è ridotto fino a [number]." reset: parameters: " " description: rimuove l'offset per materiale o entità - success: "&a Il limite di offset per [name] è impostato su 0." + success: " Il limite di offset per [name] è impostato su 0." view: parameters: " " description: visualizza l'offset per materiale o entità - message: "&a [name] offset è impostato su [number]." + message: " [name] offset è impostato su [number]." island: limits: description: mostra i limiti della tua isola - max-color: "&C" - regular-color: "&UN" + max-color: "" + regular-color: "" block-limit-syntax: "[number]/[limit]" - no-limits: "&c Non ci sono limiti in questo mondo" + no-limits: " Non ci sono limiti in questo mondo" panel: title-syntax: "[title] [sort]" entity-group-name-syntax: "[name]" entity-name-syntax: "[name]" block-name-syntax: "[name]" + env-overworld: "Sopramondo" + env-nether: "Nether" + env-end: "L'End" A2Z: a > z Z2A: z > a errors: - no-owner: "&c Quell'isola non ha proprietario" - not-on-island: "&c Questa posizione non ha limiti impostati." + no-owner: " Quell'isola non ha proprietario" + not-on-island: " Questa posizione non ha limiti impostati." recount: description: racconta i limiti per la tua isola - now-recounting: "&b Ora sto raccontando. Potrebbe volerci un po', attendi..." - in-progress: "&c Il conteggio dell'isola è in corso. Attendi..." - time-out: "&c Time out quando si racconta. L'isola è davvero grande?" + now-recounting: " Ora sto raccontando. Potrebbe volerci un po', attendi..." + in-progress: " Il conteggio dell'isola è in corso. Attendi..." + time-out: " Time out quando si racconta. L'isola è davvero grande?" diff --git a/src/main/resources/locales/ja.yml b/src/main/resources/locales/ja.yml index 2b73b34..700d3e5 100644 --- a/src/main/resources/locales/ja.yml +++ b/src/main/resources/locales/ja.yml @@ -1,7 +1,7 @@ block-limits: - hit-limit: '&c[material]は[number]に制限されています!' + hit-limit: '[material]は[number]に制限されています!' entity-limits: - hit-limit: '&c[entity]の生成は[number]に制限されています!' + hit-limit: '[entity]の生成は[number]に制限されています!' limits: panel-title: 島の制限 admin: @@ -12,32 +12,33 @@ admin: calc: parameters: <プレイヤー> description: プレイヤーの島の制限を再計算します - finished: '&a 島の再計算が正常に完了しました!' + finished: ' 島の再計算が正常に完了しました!' offset: - unknown: '&c 不明な素材または実体 [name]。' + unknown: ' 不明な素材または実体 [name]。' + description: "材料とエンティティの制限オフセットを管理できます" main: description: 材料とエンティティの制限オフセットを管理できます set: parameters: <プレイヤー> <マテリアル|エンティティ> <番号> description: 材料またはエンティティの制限の新しいオフセットを設定します - success: '&a [name] の制限オフセットが [number] に設定されています。' - same: '&c [name] の制限オフセットはすでに [number] です。' + success: ' [name] の制限オフセットが [number] に設定されています。' + same: ' [name] の制限オフセットはすでに [number] です。' add: parameters: <プレイヤー> <マテリアル|エンティティ> <番号> description: 材料またはエンティティの制限のオフセットを追加します - success: '&a [name] の制限オフセットが [number] まで増加されます。' + success: ' [name] の制限オフセットが [number] まで増加されます。' remove: parameters: <プレイヤー> <マテリアル|エンティティ> <番号> description: 材料またはエンティティの制限のオフセットを削減します - success: '&a [name] の制限オフセットが [number] まで削減されます。' + success: ' [name] の制限オフセットが [number] まで削減されます。' reset: parameters: <プレイヤー> <マテリアル|エンティティ> description: マテリアルまたはエンティティのオフセットを削除します - success: '&a [name] の制限オフセットが 0 に設定されています。' + success: ' [name] の制限オフセットが 0 に設定されています。' view: parameters: <プレイヤー> <マテリアル|エンティティ> description: 材料またはエンティティのオフセットを表示します - message: '&a [name] オフセットが [number] に設定されています。' + message: ' [name] オフセットが [number] に設定されています。' island: limits: description: 島の限界を示す @@ -48,15 +49,18 @@ island: entity-group-name-syntax: '[name]' entity-name-syntax: '[name]' block-name-syntax: '[name]' + env-overworld: "オーバーワールド" + env-nether: "ネザー" + env-end: "ジ・エンド" A2Z: a > z Z2A: z > a errors: no-owner: その島には所有者がいない - not-on-island: '&c この場所には制限が設定されていません。' + not-on-island: ' この場所には制限が設定されていません。' recount: description: あなたの島の限界を語る - now-recounting: '&b 今、話を戻します。これにはしばらく時間がかかるかもしれませんので、お待ちください...' - in-progress: '&c 島の回復作業が進行中です。お待ちください...' - time-out: '&c もう一度話すとタイムアウトになります。島は本当に大きいですか?' - max-color: '&c' - regular-color: '&a' + now-recounting: ' 今、話を戻します。これにはしばらく時間がかかるかもしれませんので、お待ちください...' + in-progress: ' 島の回復作業が進行中です。お待ちください...' + time-out: ' もう一度話すとタイムアウトになります。島は本当に大きいですか?' + max-color: '' + regular-color: '' diff --git a/src/main/resources/locales/ko.yml b/src/main/resources/locales/ko.yml index ae33d07..0468f31 100644 --- a/src/main/resources/locales/ko.yml +++ b/src/main/resources/locales/ko.yml @@ -1,8 +1,8 @@ --- block-limits: - hit-limit: "&c[material]는 [number]개로 제한됩니다!" + hit-limit: "[material]는 [number]개로 제한됩니다!" entity-limits: - hit-limit: "&c[entity]는 [number]개로 제한됩니다!" + hit-limit: "[entity]는 [number]개로 제한됩니다!" limits: panel-title: 섬의 경계 admin: @@ -13,51 +13,55 @@ admin: calc: parameters: "<플레이어>" description: 플레이어의 섬 한계를 다시 계산합니다 - finished: "&a 섬 재계산이 성공적으로 완료되었습니다!" + finished: " 섬 재계산이 성공적으로 완료되었습니다!" offset: - unknown: "&c 알 수 없는 자료 또는 엔터티 [name]." + unknown: " 알 수 없는 자료 또는 엔터티 [name]." + description: "재료 및 엔티티에 대한 제한 오프셋을 관리할 수 있습니다." main: description: 재료 및 엔티티에 대한 제한 오프셋을 관리할 수 있습니다. set: parameters: "<플레이어> <소재|엔티티> <숫자>" description: 재료 또는 엔티티 제한에 대한 새로운 오프셋을 설정합니다. - success: "&a [name]에 대한 제한 오프셋이 [number]로 설정되었습니다." - same: "&c [name]에 대한 제한 오프셋은 이미 [number]입니다." + success: " [name]에 대한 제한 오프셋이 [number]로 설정되었습니다." + same: " [name]에 대한 제한 오프셋은 이미 [number]입니다." add: parameters: "<플레이어> <소재|엔티티> <숫자>" description: 재료 또는 엔티티 제한에 대한 오프셋을 추가합니다. - success: "&a [name]에 대한 오프셋 제한이 [number]까지 증가합니다." + success: " [name]에 대한 오프셋 제한이 [number]까지 증가합니다." remove: parameters: "<플레이어> <소재|엔티티> <숫자>" description: 재료 또는 엔티티 제한에 대한 오프셋을 줄입니다. - success: "&a [name]에 대한 오프셋 제한은 [number]까지 감소합니다." + success: " [name]에 대한 오프셋 제한은 [number]까지 감소합니다." reset: parameters: "<플레이어> <소재|엔티티>" description: 재료 또는 엔티티에 대한 오프셋을 제거합니다. - success: "&a [name]에 대한 제한 오프셋이 0으로 설정되었습니다." + success: " [name]에 대한 제한 오프셋이 0으로 설정되었습니다." view: parameters: "<플레이어> <소재|엔티티>" description: 재료 또는 엔티티에 대한 오프셋을 표시합니다. - message: "&a [name] 오프셋이 [number]로 설정되었습니다." + message: " [name] 오프셋이 [number]로 설정되었습니다." island: limits: description: 섬의 경계를 보여주세요 - max-color: "&c" - regular-color: "&a" + max-color: "" + regular-color: "" block-limit-syntax: "[number]/[limit]" - no-limits: "&c 이 세상에는 제한이 없습니다" + no-limits: " 이 세상에는 제한이 없습니다" panel: title-syntax: "[title] [sort]" entity-group-name-syntax: "[name]" entity-name-syntax: "[name]" block-name-syntax: "[name]" + env-overworld: "오버월드" + env-nether: "네더" + env-end: "디 엔드" A2Z: 가 > 지 Z2A: 지 > 아 errors: - no-owner: "&c 그 섬에는 주인이 없다" - not-on-island: "&c 이 위치에는 제한이 설정되어 있지 않습니다." + no-owner: " 그 섬에는 주인이 없다" + not-on-island: " 이 위치에는 제한이 설정되어 있지 않습니다." recount: description: 당신의 섬에 대한 한계를 다시 계산합니다 - now-recounting: "&b 지금 다시 계산 중입니다. 시간이 좀 걸릴 수 있으니 기다려 주세요..." - in-progress: "&c 섬 복구가 진행 중입니다. 잠시만 기다려 주세요..." - time-out: "&c 계산할 때 시간 초과. 섬이 정말 큰가요?" + now-recounting: " 지금 다시 계산 중입니다. 시간이 좀 걸릴 수 있으니 기다려 주세요..." + in-progress: " 섬 복구가 진행 중입니다. 잠시만 기다려 주세요..." + time-out: " 계산할 때 시간 초과. 섬이 정말 큰가요?" diff --git a/src/main/resources/locales/lv.yml b/src/main/resources/locales/lv.yml index 2881f12..2a9a010 100644 --- a/src/main/resources/locales/lv.yml +++ b/src/main/resources/locales/lv.yml @@ -1,8 +1,8 @@ --- block-limits: - hit-limit: "&c[material] ierobežots līdz [number]!" + hit-limit: "[material] ierobežots līdz [number]!" entity-limits: - hit-limit: "&c[entity] rašanās ierobežots līdz [number]!" + hit-limit: "[entity] rašanās ierobežots līdz [number]!" limits: panel-title: Salas ierobežojumi admin: @@ -13,51 +13,55 @@ admin: calc: parameters: "" description: pārrēķinā salas ierobežojumus priekš spēlētāja - finished: "&aSalas ierobežojumu pārrēķināšana pabeigta!" + finished: "Salas ierobežojumu pārrēķināšana pabeigta!" offset: unknown: un c Nezināms materiāls vai vienība [name]. + description: "ļauj pārvaldīt limitu kompensācijas materiāliem un entītijām" main: description: ļauj pārvaldīt limitu kompensācijas materiāliem un entītijām set: parameters: " " description: nosaka jaunu nobīdi materiāla vai entītijas ierobežojumam - success: "&a Ierobežojuma nobīde [name] ir iestatīta uz [number]." - same: "&c [name] limita nobīde jau ir [number]." + success: " Ierobežojuma nobīde [name] ir iestatīta uz [number]." + same: " [name] limita nobīde jau ir [number]." add: parameters: " " description: pievieno nobīdi materiāla vai entītijas ierobežojumam - success: "&a Ierobežojuma nobīde [name] tiek palielināta līdz [number]." + success: " Ierobežojuma nobīde [name] tiek palielināta līdz [number]." remove: parameters: " " description: samazina materiāla vai entītijas limita kompensāciju - success: "&a Ierobežojuma nobīde [name] tiek samazināta līdz [number]." + success: " Ierobežojuma nobīde [name] tiek samazināta līdz [number]." reset: parameters: " " description: noņem materiāla vai entītijas nobīdi - success: "&a Ierobežojuma nobīde [name] ir iestatīta uz 0." + success: " Ierobežojuma nobīde [name] ir iestatīta uz 0." view: parameters: " " description: parāda nobīdi materiālam vai vienībai - message: "&a [name] nobīde ir iestatīta uz [number]." + message: " [name] nobīde ir iestatīta uz [number]." island: limits: description: rāda tavas salas ierobežojumus - max-color: "&c" - regular-color: "&a" + max-color: "" + regular-color: "" block-limit-syntax: "[number]/[limit]" - no-limits: "&c Šajā pasaulē nav noteikti ierobežojumi" + no-limits: " Šajā pasaulē nav noteikti ierobežojumi" panel: title-syntax: "[title] [sort]" entity-group-name-syntax: "[name]" entity-name-syntax: "[name]" block-name-syntax: "[name]" + env-overworld: "Virszeme" + env-nether: "Nether" + env-end: "Gals" A2Z: a > z Z2A: z > a errors: - no-owner: "&c Tai salai nav saimnieka" - not-on-island: "&c Šai vietai nav noteikti ierobežojumi." + no-owner: " Tai salai nav saimnieka" + not-on-island: " Šai vietai nav noteikti ierobežojumi." recount: description: pārrēķina ierobežojumus tavai salai - now-recounting: "&b Tagad atstāsta. Tas var aizņemt kādu laiku, lūdzu, uzgaidiet..." - in-progress: "&c Notiek salas atjaunošana. Lūdzu, uzgaidiet..." - time-out: "&c Noildze, pārskaitot. Vai tiešām sala ir liela?" + now-recounting: " Tagad atstāsta. Tas var aizņemt kādu laiku, lūdzu, uzgaidiet..." + in-progress: " Notiek salas atjaunošana. Lūdzu, uzgaidiet..." + time-out: " Noildze, pārskaitot. Vai tiešām sala ir liela?" diff --git a/src/main/resources/locales/nl.yml b/src/main/resources/locales/nl.yml index bdde128..789695a 100644 --- a/src/main/resources/locales/nl.yml +++ b/src/main/resources/locales/nl.yml @@ -1,8 +1,8 @@ --- block-limits: - hit-limit: "&c[material] beperkt tot [number]!" + hit-limit: "[material] beperkt tot [number]!" entity-limits: - hit-limit: "&c[entity] spawning beperkt tot [number]!" + hit-limit: "[entity] spawning beperkt tot [number]!" limits: panel-title: Eilandgrenzen admin: @@ -13,53 +13,57 @@ admin: calc: parameters: "" description: herbereken de eilandlimieten voor de speler - finished: "&a Eiland herberekening succesvol afgerond!" + finished: " Eiland herberekening succesvol afgerond!" offset: - unknown: "&c Onbekend materiaal of entiteit [name]." + unknown: " Onbekend materiaal of entiteit [name]." + description: "maakt het mogelijk om limietoffsets voor materialen en entiteiten te beheren" main: description: maakt het mogelijk om limietoffsets voor materialen en entiteiten te beheren set: parameters: " " description: stelt nieuwe offset in voor materiaal- of entiteitslimiet - success: "&a Limietoffset voor [name] is ingesteld op [number]." - same: "&c Limietoffset voor [name] is al [number]." + success: " Limietoffset voor [name] is ingesteld op [number]." + same: " Limietoffset voor [name] is al [number]." add: parameters: " " description: voegt offset toe voor materiaal- of entiteitslimiet - success: "&a Limietoffset voor [name] is verhoogd tot [number]." + success: " Limietoffset voor [name] is verhoogd tot [number]." remove: parameters: " " description: vermindert offset voor materiaal- of entiteitslimiet - success: "&a Limietoffset voor [name] is verlaagd tot [number]." + success: " Limietoffset voor [name] is verlaagd tot [number]." reset: parameters: " " description: verwijdert offset voor materiaal of entiteit - success: "&a Limietoffset voor [name] is ingesteld op 0." + success: " Limietoffset voor [name] is ingesteld op 0." view: parameters: " " description: geeft offset weer voor materiaal of entiteit - message: "&a [name] offset is ingesteld op [number]." + message: " [name] offset is ingesteld op [number]." island: limits: description: toon uw eilandgrenzen - max-color: "&c" - regular-color: "&a" + max-color: "" + regular-color: "" block-limit-syntax: "[number]/[limit]" - no-limits: "&c Er zijn geen grenzen gesteld in deze wereld" + no-limits: " Er zijn geen grenzen gesteld in deze wereld" panel: title-syntax: "[title] [sort]" entity-group-name-syntax: "[name]" entity-name-syntax: "[name]" block-name-syntax: "[name]" + env-overworld: "Bovenwereld" + env-nether: "Nether" + env-end: "Het Einde" A2Z: a > z Z2A: z > a errors: - no-owner: "&c Dat eiland heeft geen eigenaar" - not-on-island: "&c Voor deze locatie zijn geen limieten ingesteld." + no-owner: " Dat eiland heeft geen eigenaar" + not-on-island: " Voor deze locatie zijn geen limieten ingesteld." recount: description: vertelt over de grenzen van uw eiland - now-recounting: "&b Nu aan het navertellen. Dit kan even duren, even geduld + now-recounting: " Nu aan het navertellen. Dit kan even duren, even geduld aub..." - in-progress: "&c Eilandrecovery is bezig. Even geduld..." - time-out: "&c Time-out bij het navertellen. Is het eiland echt groot?" + in-progress: " Eilandrecovery is bezig. Even geduld..." + time-out: " Time-out bij het navertellen. Is het eiland echt groot?" diff --git a/src/main/resources/locales/pl.yml b/src/main/resources/locales/pl.yml index aa338d0..1f9f689 100644 --- a/src/main/resources/locales/pl.yml +++ b/src/main/resources/locales/pl.yml @@ -1,8 +1,8 @@ --- block-limits: - hit-limit: "&c[material] limitowany do [number]!" + hit-limit: "[material] limitowany do [number]!" entity-limits: - hit-limit: "&cSpawnowanie [entity] limitowane do [number]!" + hit-limit: "Spawnowanie [entity] limitowane do [number]!" limits: panel-title: Limity wysp admin: @@ -13,52 +13,56 @@ admin: calc: parameters: "" description: ponownie oblicza limity wyspy dla gracza - finished: "&aPrzeliczanie zakończone!" + finished: "Przeliczanie zakończone!" offset: - unknown: "&c Nieznany materiał lub podmiot [name]." + unknown: " Nieznany materiał lub podmiot [name]." + description: "pozwala zarządzać limitów dla materiałów i podmiotów," main: description: pozwala zarządzać limitów dla materiałów i podmiotów, set: parameters: " " description: ustawia nową wartość dla limitu materiału lub encji - success: "&a Przesunięcie limitu dla [name] jest ustawione na [number]." - same: "&c Limit dla [name] jest aktualnie [number]." + success: " Przesunięcie limitu dla [name] jest ustawione na [number]." + same: " Limit dla [name] jest aktualnie [number]." add: parameters: " " description: dodaje przesunięcie dla limitu materiału lub potworów - success: "&a Przesunięcie limitu dla [name] jest zwiększone do [number]." + success: " Przesunięcie limitu dla [name] jest zwiększone do [number]." remove: parameters: " " description: zmniejsza offset dla limitu materiału lub podmiotu - success: "&a Limit dla bloku [name] \nzostał zmniejszony do [number]." + success: " Limit dla bloku [name] \nzostał zmniejszony do [number]." reset: parameters: " " description: usuwa wartość dla materiału lub istoty - success: "&a Limit dla [name] jest ustawione na 0." + success: " Limit dla [name] jest ustawione na 0." view: parameters: " " description: displays offset for material or entity - message: "&a [name] wartość jest ustawiona na [number]." + message: " [name] wartość jest ustawiona na [number]." island: limits: description: pokazuje limity twojej wyspy - max-color: "&c" - regular-color: "&a" + max-color: "" + regular-color: "" block-limit-syntax: "[number]/[limit]" - no-limits: "&c Na tym świecie nie ma żadnych ograniczeń" + no-limits: " Na tym świecie nie ma żadnych ograniczeń" panel: title-syntax: "[title] [sort]" entity-group-name-syntax: "[name]" entity-name-syntax: "[name]" block-name-syntax: "[name]" + env-overworld: "Świat Powierzchniowy" + env-nether: "Nether" + env-end: "Kraj" A2Z: a > z Z2A: z > a errors: - no-owner: "&c Ta wyspa nie ma właściciela" - not-on-island: "&c Ta lokalizacja nie ma ustalonych ograniczeń." + no-owner: " Ta wyspa nie ma właściciela" + not-on-island: " Ta lokalizacja nie ma ustalonych ograniczeń." recount: description: określa limity dla twojej wyspy - now-recounting: "&b Teraz przeliczam. To może chwilę potrwać, proszę czekać..." - in-progress: "&c Trwa odzyskiwanie wyspy. Proszę czekać..." - time-out: "&c Przekroczono limit czasu podczas przeliczania. Czy wyspa jest + now-recounting: " Teraz przeliczam. To może chwilę potrwać, proszę czekać..." + in-progress: " Trwa odzyskiwanie wyspy. Proszę czekać..." + time-out: " Przekroczono limit czasu podczas przeliczania. Czy wyspa jest naprawdę duża?" diff --git a/src/main/resources/locales/pt.yml b/src/main/resources/locales/pt.yml index 3db51a5..67cb8d1 100644 --- a/src/main/resources/locales/pt.yml +++ b/src/main/resources/locales/pt.yml @@ -1,8 +1,8 @@ --- block-limits: - hit-limit: "&c[material] limitado a [number]!" + hit-limit: "[material] limitado a [number]!" entity-limits: - hit-limit: "&c[entity] gerando limitado a [number]!" + hit-limit: "[entity] gerando limitado a [number]!" limits: panel-title: Limites da ilha admin: @@ -13,51 +13,55 @@ admin: calc: parameters: "" description: recalcular os limites da ilha para o jogador - finished: "&a O recálculo da ilha foi concluído com sucesso!" + finished: " O recálculo da ilha foi concluído com sucesso!" offset: - unknown: "&c Material ou entidade desconhecida [name]." + unknown: " Material ou entidade desconhecida [name]." + description: "permite gerenciar limites de deslocamento para materiais e entidades" main: description: permite gerenciar limites de deslocamento para materiais e entidades set: parameters: " " description: define novo deslocamento para limite de material ou entidade - success: "&a O deslocamento de limite para [name] é definido como [number]." - same: "&c O deslocamento limite para [name] já é [number]." + success: " O deslocamento de limite para [name] é definido como [number]." + same: " O deslocamento limite para [name] já é [number]." add: parameters: " " description: adiciona deslocamento para limite de material ou entidade - success: "&a O deslocamento de limite para [name] é aumentado até [number]." + success: " O deslocamento de limite para [name] é aumentado até [number]." remove: parameters: " " description: reduz o deslocamento para o limite de material ou entidade - success: "&a O deslocamento de limite para [name] é reduzido até [number]." + success: " O deslocamento de limite para [name] é reduzido até [number]." reset: parameters: " " description: remove deslocamento para material ou entidade - success: "&a O deslocamento de limite para [name] é definido como 0." + success: " O deslocamento de limite para [name] é definido como 0." view: parameters: " " description: exibe deslocamento para material ou entidade - message: "&a [name] deslocamento é definido como [number]." + message: " [name] deslocamento é definido como [number]." island: limits: description: mostre os limites da sua ilha - max-color: "&c" - regular-color: "&a" + max-color: "" + regular-color: "" block-limit-syntax: "[number]/[limit]" - no-limits: "&c Não há limites neste mundo" + no-limits: " Não há limites neste mundo" panel: title-syntax: "[title] [sort]" entity-group-name-syntax: "[name]" entity-name-syntax: "[name]" block-name-syntax: "[name]" + env-overworld: "Superfície" + env-nether: "Nether" + env-end: "O End" A2Z: a > z Z2A: z > a errors: - no-owner: "&c Aquela ilha não tem dono" - not-on-island: "&c Este local não possui limites definidos." + no-owner: " Aquela ilha não tem dono" + not-on-island: " Este local não possui limites definidos." recount: description: reconta limites para sua ilha - now-recounting: "&b Agora recontando. Isso pode demorar um pouco, aguarde..." - in-progress: "&c A recontagem da ilha está em andamento. Aguarde..." - time-out: "&c Tempo limite ao recontar. A ilha é realmente grande?" + now-recounting: " Agora recontando. Isso pode demorar um pouco, aguarde..." + in-progress: " A recontagem da ilha está em andamento. Aguarde..." + time-out: " Tempo limite ao recontar. A ilha é realmente grande?" diff --git a/src/main/resources/locales/ro.yml b/src/main/resources/locales/ro.yml index 76b0780..8a21765 100644 --- a/src/main/resources/locales/ro.yml +++ b/src/main/resources/locales/ro.yml @@ -1,8 +1,8 @@ --- block-limits: - hit-limit: "&c[material] limitat la [number]!" + hit-limit: "[material] limitat la [number]!" entity-limits: - hit-limit: "&c[entity] reproducere limitata la [number]!" + hit-limit: "[entity] reproducere limitata la [number]!" limits: panel-title: Limitele insulei admin: @@ -13,53 +13,57 @@ admin: calc: parameters: "" description: recalculeaza limitele insulei pentru jocator - finished: "&aRecalcularea insulei terminata cu succes!" + finished: "Recalcularea insulei terminata cu succes!" offset: - unknown: "&c Material sau entitate necunoscută [name]." + unknown: " Material sau entitate necunoscută [name]." + description: "permite gestionarea decalajelor de limite pentru materiale și entități" main: description: permite gestionarea decalajelor de limite pentru materiale și entități set: parameters: " " description: stabilește o nouă compensare pentru limita de material sau entitate - success: "&a Decalajul limită pentru [name] este setat la [number]." - same: "&c Decalajul limită pentru [name] este deja [number]." + success: " Decalajul limită pentru [name] este setat la [number]." + same: " Decalajul limită pentru [name] este deja [number]." add: parameters: " " description: adaugă compensare pentru limita de material sau entitate - success: "&a Compensarea limită pentru [name] este mărită până la [number]." + success: " Compensarea limită pentru [name] este mărită până la [number]." remove: parameters: " " description: reduce compensarea pentru limita de material sau entitate - success: "&a Limita decalajului pentru [name] este redusă până la [number]." + success: " Limita decalajului pentru [name] este redusă până la [number]." reset: parameters: " " description: elimină decalajul pentru material sau entitate - success: "&a Decalajul limită pentru [name] este setat la 0." + success: " Decalajul limită pentru [name] este setat la 0." view: parameters: " " description: afișează offset pentru material sau entitate - message: "&a [name] offset este setat la [number]." + message: " [name] offset este setat la [number]." island: limits: description: iti arata limitele insulei - max-color: "&c" - regular-color: "&a" + max-color: "" + regular-color: "" block-limit-syntax: "[number]/[limit]" - no-limits: "&c Nu există limite stabilite în această lume" + no-limits: " Nu există limite stabilite în această lume" panel: title-syntax: "[title] [sort]" entity-group-name-syntax: "[name]" entity-name-syntax: "[name]" block-name-syntax: "[name]" + env-overworld: "Lumea de Suprafață" + env-nether: "Nether" + env-end: "End" A2Z: a > z Z2A: z > a errors: - no-owner: "&c Insula aceea nu are proprietar" - not-on-island: "&c Această locație nu are limite setate." + no-owner: " Insula aceea nu are proprietar" + not-on-island: " Această locație nu are limite setate." recount: description: renumara limitele insulei tale - now-recounting: "&b Acum povestind. Acest lucru ar putea dura ceva timp, vă + now-recounting: " Acum povestind. Acest lucru ar putea dura ceva timp, vă rugăm să așteptați..." - in-progress: "&c Recuperarea insulei este în curs. Va rugam asteptati..." - time-out: "&c Timpul expirat când relatați. Este insula cu adevărat mare?" + in-progress: " Recuperarea insulei este în curs. Va rugam asteptati..." + time-out: " Timpul expirat când relatați. Este insula cu adevărat mare?" diff --git a/src/main/resources/locales/tr.yml b/src/main/resources/locales/tr.yml index e02eed1..92a7419 100644 --- a/src/main/resources/locales/tr.yml +++ b/src/main/resources/locales/tr.yml @@ -1,10 +1,10 @@ --- block-limits: - hit-limit: "&e[material] &4eşyasından &5[number] &4koyabilirsin!" + hit-limit: "[material] eşyasından [number] koyabilirsin!" entity-limits: - hit-limit: "&e[entity] &4varlığından &5[number] &4tane koyabilirsin!" + hit-limit: "[entity] varlığından [number] tane koyabilirsin!" limits: - panel-title: "&eAda limiti" + panel-title: "Ada limiti" admin: limits: main: @@ -13,53 +13,57 @@ admin: calc: parameters: "" description: Oyuncu için ada limitlerini tekrar hesapla. - finished: "&aAda hesaplaması başarıyla yapıldı." + finished: "Ada hesaplaması başarıyla yapıldı." offset: - unknown: "&c Bilinmeyen malzeme veya varlık [name]." + unknown: " Bilinmeyen malzeme veya varlık [name]." + description: "malzemeler ve varlıklar için limit ofsetlerini yönetmeye olanak tanır" main: description: malzemeler ve varlıklar için limit ofsetlerini yönetmeye olanak tanır set: parameters: " " description: malzeme veya varlık sınırı için yeni ofset ayarlar - success: "&[name] için Limit ofseti [number] olarak ayarlandı." - same: "&c [name] için sınır ofseti zaten [number]'dır." + success: "[name] için Limit ofseti [number] olarak ayarlandı." + same: " [name] için sınır ofseti zaten [number]'dır." add: parameters: " " description: malzeme veya varlık sınırı için ofset ekler - success: "&[name] için Limit ofseti [number]'ya kadar artırıldı." + success: "[name] için Limit ofseti [number]'ya kadar artırıldı." remove: parameters: " " description: malzeme veya varlık sınırı için ofseti azaltır - success: "&[name] için limit ofseti [number]'ya kadar azaltıldı." + success: "[name] için limit ofseti [number]'ya kadar azaltıldı." reset: parameters: " " description: malzeme veya varlık için ofseti kaldırır - success: "&[name] için Limit ofseti 0 olarak ayarlandı." + success: "[name] için Limit ofseti 0 olarak ayarlandı." view: parameters: " " description: malzeme veya varlık için ofseti görüntüler - message: "&a [name] ofseti [number] olarak ayarlandı." + message: " [name] ofseti [number] olarak ayarlandı." island: limits: description: Ada limitlerini gör. - max-color: "&c" - regular-color: "&9" - block-limit-syntax: "&5[number]&7/&e[limit]" - no-limits: "&c Bu dünyada hiçbir sınır belirlenmedi" + max-color: "" + regular-color: "" + block-limit-syntax: "[number]/[limit]" + no-limits: " Bu dünyada hiçbir sınır belirlenmedi" panel: title-syntax: "[title] [sort]" entity-group-name-syntax: "[name]" entity-name-syntax: "[name]" block-name-syntax: "[name]" + env-overworld: "Ana Dünya" + env-nether: "Nether" + env-end: "End" A2Z: a > z Z2A: z > a errors: - no-owner: "&c O adanın bir sahibi yok" - not-on-island: "&c Bu lokasyonun belirlenmiş bir sınırı yok." + no-owner: " O adanın bir sahibi yok" + not-on-island: " Bu lokasyonun belirlenmiş bir sınırı yok." recount: description: Ada limitlerini tekrardan hesaplar. - now-recounting: "&b Şimdi anlatmaya başlıyorum. Biraz zaman alabilir, lütfen + now-recounting: " Şimdi anlatmaya başlıyorum. Biraz zaman alabilir, lütfen bekleyin..." - in-progress: "&c Ada geri kazanımı devam ediyor. Lütfen bekleyin..." - time-out: "&c Anlatırken zaman aşımı. Ada gerçekten büyük mü?" + in-progress: " Ada geri kazanımı devam ediyor. Lütfen bekleyin..." + time-out: " Anlatırken zaman aşımı. Ada gerçekten büyük mü?" diff --git a/src/main/resources/locales/uk.yml b/src/main/resources/locales/uk.yml index abffe7e..98bf500 100644 --- a/src/main/resources/locales/uk.yml +++ b/src/main/resources/locales/uk.yml @@ -1,8 +1,8 @@ --- block-limits: - hit-limit: "&c[material] обмежено [number]!" + hit-limit: "[material] обмежено [number]!" entity-limits: - hit-limit: "&c[entity] породження обмежено до [number]!" + hit-limit: "[entity] породження обмежено до [number]!" limits: panel-title: Межі острова admin: @@ -15,49 +15,53 @@ admin: description: перерахувати обмеження острова для гравця finished: Перерахунок острова успішно завершено! offset: - unknown: "&c Невідомий матеріал або сутність [name]." + unknown: " Невідомий матеріал або сутність [name]." + description: "дозволяє керувати зсувами лімітів для матеріалів і сутностей" main: description: дозволяє керувати зсувами лімітів для матеріалів і сутностей set: parameters: "<гравець> <матеріал|сутність> <номер>" description: встановлює нове зміщення для обмеження матеріалу або сутності - success: "&a Лімітне зміщення для [name] встановлено на [number]." - same: "&c Обмежене зміщення для [name] вже [number]." + success: " Лімітне зміщення для [name] встановлено на [number]." + same: " Обмежене зміщення для [name] вже [number]." add: parameters: "<гравець> <матеріал|сутність> <номер>" description: додає зсув для обмеження матеріалу або сутності - success: "&a Лімітне зміщення для [name] збільшується до [number]." + success: " Лімітне зміщення для [name] збільшується до [number]." remove: parameters: "<гравець> <матеріал|сутність> <номер>" description: зменшує зсув для ліміту матеріалу або сутності - success: "&a Лімітне зміщення для [name] зменшується до [number]." + success: " Лімітне зміщення для [name] зменшується до [number]." reset: parameters: "<гравець> <матеріал|сутність>" description: видаляє зсув для матеріалу або сутності - success: "&a Лімітне зміщення для [name] встановлено на 0." + success: " Лімітне зміщення для [name] встановлено на 0." view: parameters: "<гравець> <матеріал|сутність>" description: відображає зсув для матеріалу або сутності - message: "&зміщення [name] встановлено на [number]." + message: "зміщення [name] встановлено на [number]." island: limits: description: покажіть межі свого острова - max-color: "&c" - regular-color: "&a" + max-color: "" + regular-color: "" block-limit-syntax: "[number]/[limit]" - no-limits: "&c У цьому світі немає обмежень" + no-limits: " У цьому світі немає обмежень" panel: title-syntax: "[title] [sort]" entity-group-name-syntax: "[name]" entity-name-syntax: "[name]" block-name-syntax: "[name]" + env-overworld: "Звичайний світ" + env-nether: "Незер" + env-end: "Край" A2Z: a > z Z2A: z > a errors: - no-owner: "&c Цей острів не має власника" - not-on-island: "&c Це місце не має обмежень." + no-owner: " Цей острів не має власника" + not-on-island: " Це місце не має обмежень." recount: description: перераховує ліміти для вашого острова - now-recounting: "&b Тепер перераховую. Це може зайняти деякий час, зачекайте..." - in-progress: "&c Перерахування острова триває. Будь ласка, зачекайте..." - time-out: "&c Тайм-аут під час перерахунку. Чи справді острів великий?" + now-recounting: " Тепер перераховую. Це може зайняти деякий час, зачекайте..." + in-progress: " Перерахування острова триває. Будь ласка, зачекайте..." + time-out: " Тайм-аут під час перерахунку. Чи справді острів великий?" diff --git a/src/main/resources/locales/vi.yml b/src/main/resources/locales/vi.yml index 4a199ef..cbac863 100644 --- a/src/main/resources/locales/vi.yml +++ b/src/main/resources/locales/vi.yml @@ -1,8 +1,8 @@ --- block-limits: - hit-limit: "&c[material] bị giới hạn trong [number] khối!" + hit-limit: "[material] bị giới hạn trong [number] khối!" entity-limits: - hit-limit: "&c[entity] bị giới hạn trong [number] thực thể!" + hit-limit: "[entity] bị giới hạn trong [number] thực thể!" limits: panel-title: Giới hạn đảo admin: @@ -13,51 +13,55 @@ admin: calc: parameters: "" description: tính toán lại giới hạn đảo của người chơi - finished: "&aTính toán đã hoàn thành!" + finished: "Tính toán đã hoàn thành!" offset: - unknown: "&c Tài liệu hoặc thực thể không xác định [name]." + unknown: " Tài liệu hoặc thực thể không xác định [name]." + description: "cho phép quản lý các giới hạn bù trừ cho vật liệu và thực thể" main: description: cho phép quản lý các giới hạn bù trừ cho vật liệu và thực thể set: parameters: " " description: thiết lập độ lệch mới cho giới hạn vật liệu hoặc thực thể - success: "&a Độ lệch giới hạn cho [name] được đặt thành [number]." - same: "&c Độ lệch giới hạn cho [name] đã là [number]." + success: " Độ lệch giới hạn cho [name] được đặt thành [number]." + same: " Độ lệch giới hạn cho [name] đã là [number]." add: parameters: " " description: thêm độ lệch cho giới hạn vật liệu hoặc thực thể - success: "&a Độ lệch giới hạn cho [name] được tăng lên cho đến [number]." + success: " Độ lệch giới hạn cho [name] được tăng lên cho đến [number]." remove: parameters: " " description: giảm độ lệch cho giới hạn vật liệu hoặc thực thể - success: "&a Độ lệch giới hạn cho [name] được giảm xuống đến [number]." + success: " Độ lệch giới hạn cho [name] được giảm xuống đến [number]." reset: parameters: " " description: xóa phần bù cho vật liệu hoặc thực thể - success: "&a Độ lệch giới hạn cho [name] được đặt thành 0." + success: " Độ lệch giới hạn cho [name] được đặt thành 0." view: parameters: " " description: hiển thị bù trừ cho vật liệu hoặc thực thể - message: "&a [name] bù trừ được đặt thành [number]." + message: " [name] bù trừ được đặt thành [number]." island: limits: description: xem giới hạn đảo của bạn - max-color: "&c" - regular-color: "&a" + max-color: "" + regular-color: "" block-limit-syntax: "[number]/[limit]" - no-limits: "&c Không có giới hạn nào được đặt ra trên thế giới này" + no-limits: " Không có giới hạn nào được đặt ra trên thế giới này" panel: title-syntax: "[title] [sort]" entity-group-name-syntax: "[name]" entity-name-syntax: "[name]" block-name-syntax: "[name]" + env-overworld: "Thế Giới Thường" + env-nether: "Nether" + env-end: "End" A2Z: a > z Z2A: z > a errors: - no-owner: "&c Hòn đảo đó không có chủ sở hữu" - not-on-island: "&c Vị trí này không có giới hạn nào được đặt ra." + no-owner: " Hòn đảo đó không có chủ sở hữu" + not-on-island: " Vị trí này không có giới hạn nào được đặt ra." recount: description: tính toán lại giới hạn đảo của bạn - now-recounting: "&b Đang đếm lại. Việc này có thể mất một lúc, vui lòng đợi..." - in-progress: "&c Đảo đang được thu hồi. Vui lòng đợi..." - time-out: "&c Hết giờ khi kể lại. Hòn đảo có thực sự lớn không?" + now-recounting: " Đang đếm lại. Việc này có thể mất một lúc, vui lòng đợi..." + in-progress: " Đảo đang được thu hồi. Vui lòng đợi..." + time-out: " Hết giờ khi kể lại. Hòn đảo có thực sự lớn không?" diff --git a/src/main/resources/locales/zh-CN.yml b/src/main/resources/locales/zh-CN.yml index 2a816bb..a7b2829 100644 --- a/src/main/resources/locales/zh-CN.yml +++ b/src/main/resources/locales/zh-CN.yml @@ -1,8 +1,8 @@ --- block-limits: - hit-limit: "&c[material] 已限制到 [number]!" + hit-limit: "[material] 已限制到 [number]!" entity-limits: - hit-limit: "&c[entity] 生成已限制到 [number]!" + hit-limit: "[entity] 生成已限制到 [number]!" limits: panel-title: 岛屿限制 admin: @@ -13,51 +13,55 @@ admin: calc: parameters: "<玩家>" description: 重新计算玩家的岛屿限制 - finished: "&a岛屿重计算已完成!" + finished: "岛屿重计算已完成!" offset: - unknown: "&c 未知物质或实体 [name]。" + unknown: " 未知物质或实体 [name]。" + description: "允许管理材料和实体的限制偏移" main: description: 允许管理材料和实体的限制偏移 set: parameters: "<玩家> <材质|实体> <数字>" description: 为材料或实体限制设置新的偏移 - success: "&a 将 [name] 的限制偏移量设置为 [number]。" - same: "&c [name] 的限制偏移量已经为 [number]。" + success: " 将 [name] 的限制偏移量设置为 [number]。" + same: " [name] 的限制偏移量已经为 [number]。" add: parameters: "<玩家> <材质|实体> <数字>" description: 添加材料或实体限制的偏移 - success: "&a [name] 的限制偏移量增加至 [number]。" + success: " [name] 的限制偏移量增加至 [number]。" remove: parameters: "<玩家> <材质|实体> <数字>" description: 减少材料或实体限制的偏移量 - success: "&a [name] 的限制偏移量减少至 [number]。" + success: " [name] 的限制偏移量减少至 [number]。" reset: parameters: "<玩家> <材质|实体>" description: 删除材料或实体的偏移 - success: "&a [name] 的限制偏移量设置为 0。" + success: " [name] 的限制偏移量设置为 0。" view: parameters: "<玩家> <材质|实体>" description: 显示材料或实体的偏移 - message: "&a [name] offset 设置为 [number]。" + message: " [name] offset 设置为 [number]。" island: limits: description: 显示您的岛屿限制 - max-color: "&c" - regular-color: "&a" + max-color: "" + regular-color: "" block-limit-syntax: "[number]/[limit]" - no-limits: "&c 这世上没有限制" + no-limits: " 这世上没有限制" panel: title-syntax: "[title] [sort]" entity-group-name-syntax: "[name]" entity-name-syntax: "[name]" block-name-syntax: "[name]" + env-overworld: "主世界" + env-nether: "下界" + env-end: "末地" A2Z: a > z Z2A: z > a errors: - no-owner: "&c 那个岛没有主人" - not-on-island: "&c 此位置未设置限制。" + no-owner: " 那个岛没有主人" + not-on-island: " 此位置未设置限制。" recount: description: 重新计数岛屿限制 - now-recounting: "&b 开始重新计算. 可能需要一定的时间, 请稍等......" - in-progress: "&c 重新计算岛屿限制中. 请稍等......" - time-out: "&c 重新计算超时. 岛屿太大了吗?" + now-recounting: " 开始重新计算. 可能需要一定的时间, 请稍等......" + in-progress: " 重新计算岛屿限制中. 请稍等......" + time-out: " 重新计算超时. 岛屿太大了吗?" diff --git a/src/main/resources/locales/zh-TW.yml b/src/main/resources/locales/zh-TW.yml index cad8dca..5537b1c 100644 --- a/src/main/resources/locales/zh-TW.yml +++ b/src/main/resources/locales/zh-TW.yml @@ -1,8 +1,8 @@ --- block-limits: - hit-limit: "&c[material]僅限[number]!" + hit-limit: "[material]僅限[number]!" entity-limits: - hit-limit: "&c[entity] 產生僅限於 [number]!" + hit-limit: "[entity] 產生僅限於 [number]!" limits: panel-title: 島嶼限制 admin: @@ -13,51 +13,55 @@ admin: calc: parameters: "<玩家>" description: 重新計算玩家的島嶼限制 - finished: "&a 島嶼重新計算成功完成!" + finished: " 島嶼重新計算成功完成!" offset: - unknown: "&c 未知材料或實體[name]。" + unknown: " 未知材料或實體[name]。" + description: "允許管理材料和實體的限制偏移" main: description: 允許管理材料和實體的限制偏移 set: parameters: "<玩家> <材質|實體> <數字>" description: 設定材料或實體限制的新偏移 - success: "&a [name] 的限制偏移量設定為 [number]。" - same: "&c [name] 的限制偏移量已經是 [number]。" + success: " [name] 的限制偏移量設定為 [number]。" + same: " [name] 的限制偏移量已經是 [number]。" add: parameters: "<玩家> <材質|實體> <數字>" description: 添加材料或實體限制的偏移量 - success: "&a [name] 的限制偏移量增加到 [number]。" + success: " [name] 的限制偏移量增加到 [number]。" remove: parameters: "<玩家> <材質|實體> <數字>" description: 減少材料或實體限制的偏移 - success: "&a [name] 的限制偏移量減少到 [number]。" + success: " [name] 的限制偏移量減少到 [number]。" reset: parameters: "<玩家> <材質|實體>" description: 刪除材質或實體的偏移 - success: "&a [名稱] 的限制偏移量設定為 0。" + success: " [名稱] 的限制偏移量設定為 0。" view: parameters: "<玩家> <材質|實體>" description: 顯示材料或實體的偏移 - message: "&a [name] 偏移量設定為 [number]。" + message: " [name] 偏移量設定為 [number]。" island: limits: description: 顯示您的島嶼限制 - max-color: "&c" - regular-color: "&a" + max-color: "" + regular-color: "" block-limit-syntax: "[number]/[limit]" - no-limits: "&c 這個世界沒有限制" + no-limits: " 這個世界沒有限制" panel: title-syntax: "[title] [sort]" entity-group-name-syntax: "[name]" entity-name-syntax: "[name]" block-name-syntax: "[name]" + env-overworld: "主世界" + env-nether: "下界" + env-end: "終界" A2Z: a > z Z2A: z > a errors: - no-owner: "&c 那島沒有主人" - not-on-island: "&c 該位置沒有設定限制。" + no-owner: " 那島沒有主人" + not-on-island: " 該位置沒有設定限制。" recount: description: 詳細說明您的島嶼的限制 - now-recounting: "&b 現在重述。這可能需要一段時間,請稍候..." - in-progress: "&c 島回收正在進行中。請稍等..." - time-out: "&c 重新計數時逾時。島真的很大嗎?" + now-recounting: " 現在重述。這可能需要一段時間,請稍候..." + in-progress: " 島回收正在進行中。請稍等..." + time-out: " 重新計數時逾時。島真的很大嗎?"