Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions src/main/java/world/bentobox/limits/BlockGroup.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package world.bentobox.limits;

import java.util.Objects;
import java.util.Set;

import org.bukkit.Material;
import org.bukkit.NamespacedKey;

/**
* A named group of block materials sharing a single limit. The counts of every
* member material are summed and checked against the group limit.
*/
public class BlockGroup {
private final String name;
private final Set<NamespacedKey> keys;
private final int limit;
private final Material icon;

public BlockGroup(String name, Set<NamespacedKey> keys, int limit, Material icon) {
this.name = name;
this.keys = keys;
this.limit = limit;
this.icon = icon;
}

public boolean contains(NamespacedKey key) {
return keys.contains(key);
}

public String getName() {
return name;
}

public Set<NamespacedKey> getKeys() {
return keys;
}

public int getLimit() {
return limit;
}

public Material getIcon() {
return icon;
}

@Override
public int hashCode() {
return 83 * 7 + Objects.hashCode(this.name);
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
return Objects.equals(this.name, ((BlockGroup) obj).name);
}
}
121 changes: 109 additions & 12 deletions src/main/java/world/bentobox/limits/Settings.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ enum GeneralGroup {

private static final String SKIPPING = " - skipping...";
private static final String LIMIT_SUFFIX = ".limit";
private static final String LIMIT_LOG_PREFIX = "Limit ";
private static final String GROUP_OVERRIDE_PREFIX = "Group override ";

private final Map<GeneralGroup, Integer> general = new EnumMap<>(GeneralGroup.class);
/** Per-env entity type limits (env defaults from config). */
Expand All @@ -46,6 +48,10 @@ enum GeneralGroup {
private final Map<EntityType, List<EntityGroup>> groupLimits = new EnumMap<>(EntityType.class);
/** Per-env group-limit overrides (env defaults from config). */
private final Map<Environment, Map<String, Integer>> envGroupLimits = new EnumMap<>(Environment.class);
/** Block group definitions and which groups each canonical block key belongs to. */
private final Map<org.bukkit.NamespacedKey, List<BlockGroup>> blockGroups = new java.util.HashMap<>();
/** Per-env block-group limits (defaults from config plus env overrides). */
private final Map<Environment, Map<String, Integer>> envBlockGroupLimits = new EnumMap<>(Environment.class);
private final List<String> gameModes;
private final boolean logLimitsOnJoin;
private final boolean asyncGolums;
Expand Down Expand Up @@ -84,6 +90,7 @@ public Settings(Limits addon) {
for (Environment env : ENVIRONMENTS) {
envLimits.put(env, new EnumMap<>(EntityType.class));
envGroupLimits.put(env, new java.util.HashMap<>());
envBlockGroupLimits.put(env, new java.util.HashMap<>());
}

// Pass 1: parse the unsuffixed entitylimits as the default for every env
Expand All @@ -105,7 +112,7 @@ public Settings(Limits addon) {

addon.log("Entity limits:");
envLimits.forEach((env, m) -> m.entrySet().stream()
.map(e -> "Limit " + e.getKey() + " in " + env + " to " + e.getValue())
.map(e -> LIMIT_LOG_PREFIX + e.getKey() + " in " + env + " to " + e.getValue())
.forEach(addon::log));

// Group definitions live in the unsuffixed entitygrouplimits section. The entire
Expand All @@ -117,10 +124,78 @@ public Settings(Limits addon) {

addon.log("Entity group limits:");
getGroupLimitDefinitions().stream()
.map(e -> "Limit " + e.getName() + " ("
.map(e -> LIMIT_LOG_PREFIX + e.getName() + " ("
+ e.getTypes().stream().map(Enum::name).collect(Collectors.joining(", ")) + ") to "
+ e.getLimit())
.forEach(addon::log);

// Block groups follow the same pattern as entity groups.
loadBlockGroupDefinitions(addon);
loadBlockGroupLimitOverrides(addon, "blockgrouplimits-nether", Environment.NETHER);
loadBlockGroupLimitOverrides(addon, "blockgrouplimits-end", Environment.THE_END);

addon.log("Block group limits:");
getBlockGroupDefinitions().stream()
.map(g -> LIMIT_LOG_PREFIX + g.getName() + " ("
+ g.getKeys().stream().map(org.bukkit.NamespacedKey::getKey)
.collect(Collectors.joining(", "))
+ ") to " + g.getLimit())
.forEach(addon::log);
}

private void loadBlockGroupDefinitions(Limits addon) {
ConfigurationSection el = addon.getConfig().getConfigurationSection("blockgrouplimits");
if (el == null) return;
for (String name : el.getKeys(false)) {
int limit = el.getInt(name + LIMIT_SUFFIX);
Material icon = parseIcon(addon, el.getString(name + ".icon", "BARRIER"));
Set<org.bukkit.NamespacedKey> keys = el.getStringList(name + ".materials").stream().map(m -> {
Material material = Material.matchMaterial(m);
if (material == null || !material.isBlock()) {
addon.logError("Unknown block material in blockgrouplimits." + name + ": " + m + SKIPPING);
return null;
}
return world.bentobox.limits.listeners.BlockLimitsListener.canonicalKey(material);
}).filter(Objects::nonNull).collect(Collectors.toCollection(LinkedHashSet::new));
if (keys.isEmpty()) continue;
BlockGroup group = new BlockGroup(name, keys, limit, icon);
keys.forEach(k -> blockGroups.computeIfAbsent(k, x -> new ArrayList<>()).add(group));
// Default group limit applies to every env unless overridden.
ENVIRONMENTS.forEach(env -> envBlockGroupLimits.get(env).put(name, limit));
}
}

private void loadBlockGroupLimitOverrides(Limits addon, String section, Environment env) {
ConfigurationSection el = addon.getConfig().getConfigurationSection(section);
if (el == null) return;
for (String name : el.getKeys(false)) {
applyBlockGroupOverride(addon, el, section, name, env);
}
}

private void applyBlockGroupOverride(Limits addon, ConfigurationSection el, String section, String name,
Environment env) {
Integer limit = resolveGroupLimit(el, name);
if (limit == null) {
addon.logError(GROUP_OVERRIDE_PREFIX + section + "." + name + " missing limit - skipping.");
return;
}
// Group must already be defined in the base blockgrouplimits section
if (getBlockGroupDefinitions().stream().noneMatch(g -> g.getName().equals(name))) {
addon.logError(GROUP_OVERRIDE_PREFIX + section + "." + name
+ " refers to an undefined group - define it under blockgrouplimits first.");
return;
}
envBlockGroupLimits.get(env).put(name, limit);
}

private Material parseIcon(Limits addon, String iconName) {
try {
return Material.valueOf(iconName.toUpperCase(Locale.ENGLISH));
} catch (Exception e) {
addon.logError("Invalid group icon name: " + iconName + ". Use a Bukkit Material.");
return Material.BARRIER;
}
}

private void loadEntityLimits(Limits addon, String section, List<Environment> targetEnvs) {
Expand Down Expand Up @@ -161,14 +236,7 @@ private void loadGroupDefinitions(Limits addon) {
if (el == null) return;
for (String name : el.getKeys(false)) {
int limit = el.getInt(name + LIMIT_SUFFIX);
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;
}
Material icon = parseIcon(addon, el.getString(name + ".icon", "BARRIER"));
Set<EntityType> entities = el.getStringList(name + ".entities").stream().map(s -> {
EntityType type = getType(s);
if (type == null) {
Expand Down Expand Up @@ -202,13 +270,13 @@ private void applyGroupOverride(Limits addon, ConfigurationSection el, String se
// 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.");
addon.logError(GROUP_OVERRIDE_PREFIX + 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
addon.logError(GROUP_OVERRIDE_PREFIX + section + "." + name
+ " refers to an undefined group - define it under entitygrouplimits first.");
return;
}
Expand Down Expand Up @@ -287,6 +355,35 @@ public boolean isApplyMemberLimitPerms() {
return applyMemberLimitPerms;
}

/**
* @param key canonical block key
* @return block groups containing this key; empty if none
*/
public List<BlockGroup> getBlockGroups(org.bukkit.NamespacedKey key) {
return blockGroups.getOrDefault(key, Collections.emptyList());
}

/**
* @return true if the key belongs to any block group
*/
public boolean isInBlockGroup(org.bukkit.NamespacedKey key) {
return blockGroups.containsKey(key);
}

/**
* @return all defined block groups
*/
public List<BlockGroup> getBlockGroupDefinitions() {
return blockGroups.values().stream().flatMap(Collection::stream).distinct().toList();
}

/**
* @return the block group's limit in this environment, or -1 if not defined
*/
public int getBlockGroupLimit(Environment env, String name) {
return envBlockGroupLimits.getOrDefault(env, Collections.emptyMap()).getOrDefault(name, -1);
}

public Map<GeneralGroup, Integer> getGeneral() {
return general;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,10 @@ public RecountCalculator(Limits addon, Island island, CompletableFuture<Results>

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)) {
// Only count materials that are tracked at any level (env default, world override,
// island, or block group membership).
if (bll.getMaterialLimits(worlds.get(env), island.getUniqueId()).containsKey(md)
|| addon.getSettings().isInBlockGroup(md)) {
results.getBlockCount(env).add(md);
}
}
Expand Down
25 changes: 25 additions & 0 deletions src/main/java/world/bentobox/limits/commands/player/LimitTab.java
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ public LimitTab(Limits addon, IslandBlockCount ibc, Map<NamespacedKey, Integer>
this.env = env;
result = new ArrayList<>();
addMaterialIcons(ibc, matLimits);
addBlockGroupIcons(ibc);
addEntityLimits(ibc);
addEntityGroupLimits(ibc);
result.sort(Comparator.comparing(PanelItem::getName));
Expand Down Expand Up @@ -188,6 +189,30 @@ private void addMaterialIcons(IslandBlockCount ibc, Map<NamespacedKey, Integer>
}
}

private void addBlockGroupIcons(IslandBlockCount ibc) {
for (world.bentobox.limits.BlockGroup g : addon.getSettings().getBlockGroupDefinitions()) {
int limit = addon.getSettings().getBlockGroupLimit(env, g.getName());
if (limit < 0) {
continue;
}
PanelItemBuilder pib = new PanelItemBuilder();
pib.name(user.getTranslation("island.limits.panel.block-group-name-syntax", TextVariables.NAME,
g.getName()));
String description = "(" + g.getKeys().stream().map(k -> Util.prettifyText(k.getKey()))
.collect(Collectors.joining(", ")) + ")\n";
pib.icon(g.getIcon());
int count = ibc == null ? 0
: g.getKeys().stream().mapToInt(k -> ibc.getBlockCount(env, k)).sum();
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_PLACEHOLDER, String.valueOf(limit));
pib.description(description);
result.add(pib.build());
}
}

@Override
public PanelItem getIcon() {
Material icon = switch (env) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,13 @@ private static boolean isSamePlant(Material below, NamespacedKey plantKey) {
return VARIANT_MAP.getOrDefault(below, below).getKey().equals(plantKey);
}

/**
* @return the canonical key for a material after variant normalisation
*/
public static NamespacedKey canonicalKey(Material m) {
return VARIANT_MAP.getOrDefault(m, m).getKey();
}

/**
* Map variant materials to their canonical form.
*/
Expand Down Expand Up @@ -503,6 +510,34 @@ private void updateSaveMap(String id) {
* @return limit if at or over, or -1 if no limit
*/
private int checkLimit(World w, Environment env, NamespacedKey m, String id) {
int single = checkSingleLimit(w, env, m, id);
if (single > -1) {
return single;
}
return checkBlockGroupLimit(env, m, islandCountMap.get(id));
}

/**
* Check the block groups this material belongs to: the counts of all group members
* are summed and compared against the group's limit for this environment.
*
* @return group limit if at or over, or -1 if no group limit is hit
*/
private int checkBlockGroupLimit(Environment env, NamespacedKey m, IslandBlockCount ibc) {
for (world.bentobox.limits.BlockGroup group : addon.getSettings().getBlockGroups(m)) {
int limit = addon.getSettings().getBlockGroupLimit(env, group.getName());
if (limit < 0) {
continue;
}
int sum = group.getKeys().stream().mapToInt(k -> ibc.getBlockCount(env, k)).sum();
if (sum >= limit) {
return limit;
}
}
return -1;
}

private int checkSingleLimit(World w, Environment env, NamespacedKey m, String id) {
IslandBlockCount ibc = islandCountMap.get(id);
if (ibc.isBlockLimited(env, m)) {
int offset = ibc.getBlockLimitOffset(env, m);
Expand Down
30 changes: 30 additions & 0 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,36 @@ stacked-plants-count-as-one: false
#blocklimits-end:
# HOPPER: 5

# Block Groups
# Same idea as entity groups below: one shared limit across a set of block materials.
# The counts of every member are summed and checked against the group limit, so players
# cannot dodge a limit by converting between related blocks (e.g. grass -> dirt) or
# splitting across variants (e.g. pistons and sticky pistons).
# Individual blocklimits still apply on top if both are set.
# Run a recount (/<gamemode> limits recount) after adding a group so existing blocks
# are counted.
#blockgrouplimits:
# Pistons:
# icon: PISTON
# limit: 10
# materials:
# - PISTON
# - STICKY_PISTON
# Soil:
# icon: GRASS_BLOCK
# limit: 200
# materials:
# - GRASS_BLOCK
# - DIRT
# - DIRT_PATH
# - FARMLAND
# Override a block group's limit per environment. The group must already be defined
# in blockgrouplimits; only the numeric limit can be overridden.
#blockgrouplimits-nether:
# Pistons: 5
#blockgrouplimits-end:
# Pistons: 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
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/locales/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ island:
entity-group-name-syntax: '[name]'
entity-name-syntax: '[name]'
block-name-syntax: '[name]'
block-group-name-syntax: '[name]'
env-overworld: "Overworld"
env-nether: "Nether"
env-end: "End"
Expand Down
Loading
Loading