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
697 changes: 363 additions & 334 deletions pom.xml

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions src/main/java/world/bentobox/limits/Limits.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ public void onEnable() {
registerListener(joinListener);
EntityLimitListener entityLimitListener = new EntityLimitListener(this);
registerListener(entityLimitListener);
if (org.bukkit.Bukkit.getPluginManager().getPlugin("ItemsAdder") != null) {
registerListener(new world.bentobox.limits.listeners.ItemsAdderListener(this));
log("ItemsAdder detected: custom block limits active. Use ItemsAdder ids as blocklimits keys.");
}
if (org.bukkit.Bukkit.getPluginManager().getPlugin("Oraxen") != null) {
registerListener(new world.bentobox.limits.listeners.OraxenListener(this));
log("Oraxen detected: custom block limits active. Use \"oraxen:<itemid>\" blocklimits keys.");
}
try {
Class.forName("io.papermc.paper.event.entity.ShulkerDuplicateEvent");
registerListener(new PaperShulkerLimitListener(this, entityLimitListener));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,9 +233,24 @@ private void scanEntities() {

public void tidyUp() {
ibc = bll.getIsland(island);
// Custom-namespace counts (ItemsAdder/Oraxen blocks) are event-tracked and
// invisible to the chunk scan, so carry them across the reset untouched.
Map<Environment, Map<NamespacedKey, Integer>> customCounts = new EnumMap<>(Environment.class);
for (Environment env : List.of(Environment.NORMAL, Environment.NETHER, Environment.THE_END)) {
ibc.getBlockCounts(env).forEach((key, count) -> {
if (!NamespacedKey.MINECRAFT.equals(key.getNamespace())) {
customCounts.computeIfAbsent(env, e -> new java.util.HashMap<>()).put(key, count);
}
});
}
// Reset and write per-env block counts
ibc.clearAllBlockCounts();
results.getEnvBlockCount().forEach((env, multiset) -> multiset.forEach(key -> ibc.add(env, key)));
customCounts.forEach((env, m) -> m.forEach((key, count) -> {
for (int i = 0; i < count; i++) {
ibc.add(env, key);
}
}));

// Recount entities (loaded only) and write per-env
scanEntities();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.util.Objects;

import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
Expand Down Expand Up @@ -209,6 +210,13 @@ private NamespacedKey parseConfigKey(String key) {

/** Resolve a key to a material or block tag and store its limit, warning on any mismatch. */
private void registerLimit(Map<NamespacedKey, Integer> limits, NamespacedKey nsKey, String key, int limit) {
if (!NamespacedKey.MINECRAFT.equals(nsKey.getNamespace())) {
// Custom-namespace key, e.g. an ItemsAdder block id ("iafestivities:christmas/tree")
// or an Oraxen id written as "oraxen:<itemid>". Enforced by the custom block
// listeners when the owning plugin is installed.
limits.put(nsKey, limit);
return;
}
Material mat = Registry.MATERIAL.get(nsKey);
if (mat != null) {
if (!mat.isBlock()) {
Expand Down Expand Up @@ -445,7 +453,7 @@ private int process(Block b, boolean add) {
* @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())) {
if (DO_NOT_COUNT.contains(fixMaterial(blockData))) {
return -1;
}
// Stacked-plants-as-one: a segment sitting on the same plant is not counted,
Expand All @@ -454,21 +462,34 @@ private int process(Block b, BlockData blockData, boolean add) {
&& isSamePlant(b.getRelative(BlockFace.DOWN).getType(), fixMaterial(blockData))) {
return -1;
}
Environment env = envOf(b.getWorld());
return addon.getIslands().getIslandAt(b.getLocation()).map(i -> {
return processKey(b.getWorld(), b.getLocation(), fixMaterial(blockData), add);
}

/**
* Count and limit-check a namespaced key at a location. Public so custom-block
* listeners (ItemsAdder, Oraxen) can run their block ids through the same
* counting and limit machinery as vanilla blocks.
*
* @return limit amount if at/over the limit (nothing was counted), or -1 on success
*/
public int processKey(World world, Location location, NamespacedKey key, boolean add) {
if (!addon.inGameModeWorld(world)) {
return -1;
}
Environment env = envOf(world);
return addon.getIslands().getIslandAt(location).map(i -> {
String id = i.getUniqueId();
String gameMode = addon.getGameModeName(b.getWorld());
String gameMode = addon.getGameModeName(world);
if (gameMode.isEmpty()) {
return -1;
}
if (addon.getConfig().getBoolean("ignore-center-block", true)
&& i.getCenter().equals(b.getLocation())) {
&& i.getCenter().equals(location)) {
return -1;
}
islandCountMap.putIfAbsent(id, new IslandBlockCount(id, gameMode));
NamespacedKey key = fixMaterial(blockData);
if (add) {
int limit = checkLimit(b.getWorld(), env, key, id);
int limit = checkLimit(world, env, key, id);
if (limit > -1) {
return limit;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package world.bentobox.limits.listeners;

import java.util.Locale;

import org.bukkit.NamespacedKey;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;

import world.bentobox.bentobox.api.localization.TextVariables;
import world.bentobox.bentobox.api.user.User;
import world.bentobox.bentobox.util.Util;
import world.bentobox.limits.Limits;

/**
* Shared plumbing for custom-block plugin listeners (ItemsAdder, Oraxen). Runs custom
* block ids through the same counting and limit machinery as vanilla blocks via
* {@link BlockLimitsListener#processKey}.
*/
final class CustomBlockHandler {

private CustomBlockHandler() {
}

/**
* Limit-check and count a custom block placement; cancels the event and notifies
* the player when the limit is hit.
*/
static void place(Limits addon, Cancellable event, Player player, Block block, NamespacedKey key) {
if (key == null) {
return;
}
int limit = addon.getBlockLimitListener().processKey(block.getWorld(), block.getLocation(), key, true);
if (limit > -1) {
event.setCancelled(true);
if (player != null) {
User.getInstance(player).notify("block-limits.hit-limit",
"[material]", Util.prettifyText(key.getKey()),
TextVariables.NUMBER, String.valueOf(limit));
}
}
}

/**
* Decrement the count for a removed custom block.
*/
static void remove(Limits addon, Block block, NamespacedKey key) {
if (key != null) {
addon.getBlockLimitListener().processKey(block.getWorld(), block.getLocation(), key, false);
}
}

/**
* Parse a custom block id into a namespaced key. ItemsAdder ids already carry a
* namespace; plain ids get the supplied default namespace (e.g. "oraxen").
*/
static NamespacedKey toKey(String id, String defaultNamespace) {
if (id == null) {
return null;
}
String full = id.contains(":") ? id : defaultNamespace + ":" + id;
return NamespacedKey.fromString(full.toLowerCase(Locale.ROOT));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package world.bentobox.limits.listeners;

import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;

import dev.lone.itemsadder.api.Events.CustomBlockBreakEvent;
import dev.lone.itemsadder.api.Events.CustomBlockPlaceEvent;
import world.bentobox.limits.Limits;

/**
* Applies block limits to ItemsAdder custom blocks. Registered only when the
* ItemsAdder plugin is present. Config keys use the ItemsAdder namespaced id, e.g.
* {@code "iafestivities:christmas/christmas_tree/green_orb": 5} under blocklimits.
*/
public class ItemsAdderListener implements Listener {

private final Limits addon;

public ItemsAdderListener(Limits addon) {
this.addon = addon;
}

@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onPlace(CustomBlockPlaceEvent e) {
CustomBlockHandler.place(addon, e, e.getPlayer(), e.getBlock(),
CustomBlockHandler.toKey(e.getNamespacedID(), "itemsadder"));
}

@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onBreak(CustomBlockBreakEvent e) {
CustomBlockHandler.remove(addon, e.getBlock(),
CustomBlockHandler.toKey(e.getNamespacedID(), "itemsadder"));
}
}
51 changes: 51 additions & 0 deletions src/main/java/world/bentobox/limits/listeners/OraxenListener.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package world.bentobox.limits.listeners;

import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;

import io.th0rgal.oraxen.api.events.noteblock.OraxenNoteBlockBreakEvent;
import io.th0rgal.oraxen.api.events.noteblock.OraxenNoteBlockPlaceEvent;
import io.th0rgal.oraxen.api.events.stringblock.OraxenStringBlockBreakEvent;
import io.th0rgal.oraxen.api.events.stringblock.OraxenStringBlockPlaceEvent;
import world.bentobox.limits.Limits;

/**
* Applies block limits to Oraxen custom blocks (note-block and string-block
* mechanics). Registered only when the Oraxen plugin is present. Config keys use the
* {@code oraxen:} namespace, e.g. {@code "oraxen:caveblock": 10} under blocklimits.
*/
public class OraxenListener implements Listener {

private static final String NAMESPACE = "oraxen";

private final Limits addon;

public OraxenListener(Limits addon) {
this.addon = addon;
}

@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onNoteBlockPlace(OraxenNoteBlockPlaceEvent e) {
CustomBlockHandler.place(addon, e, e.getPlayer(), e.getBlock(),
CustomBlockHandler.toKey(e.getMechanic().getItemID(), NAMESPACE));
}

@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onNoteBlockBreak(OraxenNoteBlockBreakEvent e) {
CustomBlockHandler.remove(addon, e.getBlock(),
CustomBlockHandler.toKey(e.getMechanic().getItemID(), NAMESPACE));
}

@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onStringBlockPlace(OraxenStringBlockPlaceEvent e) {
CustomBlockHandler.place(addon, e, e.getPlayer(), e.getBlock(),
CustomBlockHandler.toKey(e.getMechanic().getItemID(), NAMESPACE));
}

@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onStringBlockBreak(OraxenStringBlockBreakEvent e) {
CustomBlockHandler.remove(addon, e.getBlock(),
CustomBlockHandler.toKey(e.getMechanic().getItemID(), NAMESPACE));
}
}
6 changes: 6 additions & 0 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ apply-member-limit-perms: false
# 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
# ItemsAdder / Oraxen custom blocks can be limited too (when the plugin is installed):
# use the ItemsAdder namespaced id, or an Oraxen id prefixed with "oraxen:", as the key.
# Quote keys containing a colon. Custom blocks are tracked from their place/break
# events; a recount preserves their stored counts rather than rescanning them.
# "iafestivities:christmas/christmas_tree/green_orb": 5
# "oraxen:caveblock": 10
blocklimits:
HOPPER: 10

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1097,6 +1097,34 @@ void testIndividualLimitStillAppliesInsideGroup() {
assertTrue(event.isCancelled());
}

// --- Custom block keys (#176) ---

@Test
void testProcessKeyCustomNamespaceLimitAndCount() {
NamespacedKey custom = NamespacedKey.fromString("itemsadder:some_pack/tree");
IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock");
ibc.setBlockLimit(Environment.NORMAL, custom, 1);
listener.setIsland("test-island-id", ibc);

// First add succeeds and counts
assertEquals(-1, listener.processKey(world, blockLocation, custom, true));
assertEquals(1, listener.getIsland("test-island-id").getBlockCount(Environment.NORMAL, custom));
// Second add hits the limit and must not count
assertEquals(1, listener.processKey(world, blockLocation, custom, true));
assertEquals(1, listener.getIsland("test-island-id").getBlockCount(Environment.NORMAL, custom));
// Removal decrements
assertEquals(-1, listener.processKey(world, blockLocation, custom, false));
assertEquals(0, listener.getIsland("test-island-id").getBlockCount(Environment.NORMAL, custom));
}

@Test
void testCustomNamespaceKeyAcceptedInBlockLimitsConfig() {
config.set("blocklimits.oraxen:caveblock", 10);
BlockLimitsListener custom = new BlockLimitsListener(addon);
assertEquals(10,
custom.getMaterialLimits(world, "no-island").get(NamespacedKey.fromString("oraxen:caveblock")));
}

// --- Block cascade tests ---

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package world.bentobox.limits.listeners;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

import org.bukkit.NamespacedKey;
import org.junit.jupiter.api.Test;

class CustomBlockHandlerTest {

@Test
void testToKeyNamespacedIdPassesThrough() {
NamespacedKey key = CustomBlockHandler.toKey("iafestivities:christmas/christmas_tree/green_orb",
"itemsadder");
assertEquals("iafestivities", key.getNamespace());
assertEquals("christmas/christmas_tree/green_orb", key.getKey());
}

@Test
void testToKeyPlainIdGetsDefaultNamespace() {
NamespacedKey key = CustomBlockHandler.toKey("caveblock", "oraxen");
assertEquals("oraxen", key.getNamespace());
assertEquals("caveblock", key.getKey());
}

@Test
void testToKeyUppercaseIsLowercased() {
NamespacedKey key = CustomBlockHandler.toKey("CaveBlock", "oraxen");
assertEquals("caveblock", key.getKey());
}

@Test
void testToKeyInvalidReturnsNull() {
assertNull(CustomBlockHandler.toKey("bad key with spaces", "oraxen"));
assertNull(CustomBlockHandler.toKey(null, "oraxen"));
}
}
Loading