Skip to content

Commit 886f8a5

Browse files
tastybentoclaude
andcommitted
Add ItemsAdder and Oraxen custom block limits via BentoBox hooks
Custom block ids can now be used directly as blocklimits keys (all sections: blocklimits, -nether, -end, worlds): ItemsAdder ids as-is (namespace:id), Oraxen ids prefixed "oraxen:". The config parser already produced NamespacedKeys; registerLimit now accepts non-minecraft namespaces instead of rejecting them. Enforcement listens to the plugins' own place/break events (fixing the 2023 event-ordering problem where the alert fired but the block still placed) through new soft-registered listeners that only load when the respective plugin is present. Both feed processKey, a public extraction of the existing count/limit core, so island lookup, per-env limits, offsets, batching, and the GUI all work unchanged - custom keys render with a paper icon and prettified name. Custom counts are event-tracked and invisible to the recount chunk scan, so the recount now carries non-minecraft counts across the reset instead of zeroing them. Fixes #176 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015dbJyrv2uxHfTmiWkqGUJB
1 parent 6cbca12 commit 886f8a5

10 files changed

Lines changed: 633 additions & 341 deletions

File tree

pom.xml

Lines changed: 363 additions & 334 deletions
Large diffs are not rendered by default.

src/main/java/world/bentobox/limits/Limits.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,14 @@ public void onEnable() {
6969
registerListener(joinListener);
7070
EntityLimitListener entityLimitListener = new EntityLimitListener(this);
7171
registerListener(entityLimitListener);
72+
if (org.bukkit.Bukkit.getPluginManager().getPlugin("ItemsAdder") != null) {
73+
registerListener(new world.bentobox.limits.listeners.ItemsAdderListener(this));
74+
log("ItemsAdder detected: custom block limits active. Use ItemsAdder ids as blocklimits keys.");
75+
}
76+
if (org.bukkit.Bukkit.getPluginManager().getPlugin("Oraxen") != null) {
77+
registerListener(new world.bentobox.limits.listeners.OraxenListener(this));
78+
log("Oraxen detected: custom block limits active. Use \"oraxen:<itemid>\" blocklimits keys.");
79+
}
7280
try {
7381
Class.forName("io.papermc.paper.event.entity.ShulkerDuplicateEvent");
7482
registerListener(new PaperShulkerLimitListener(this, entityLimitListener));

src/main/java/world/bentobox/limits/calculators/RecountCalculator.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,9 +233,24 @@ private void scanEntities() {
233233

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

240255
// Recount entities (loaded only) and write per-env
241256
scanEntities();

src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import java.util.Objects;
1010

1111
import org.bukkit.Bukkit;
12+
import org.bukkit.Location;
1213
import org.bukkit.Material;
1314
import org.bukkit.NamespacedKey;
1415
import org.bukkit.Registry;
@@ -209,6 +210,13 @@ private NamespacedKey parseConfigKey(String key) {
209210

210211
/** Resolve a key to a material or block tag and store its limit, warning on any mismatch. */
211212
private void registerLimit(Map<NamespacedKey, Integer> limits, NamespacedKey nsKey, String key, int limit) {
213+
if (!NamespacedKey.MINECRAFT.equals(nsKey.getNamespace())) {
214+
// Custom-namespace key, e.g. an ItemsAdder block id ("iafestivities:christmas/tree")
215+
// or an Oraxen id written as "oraxen:<itemid>". Enforced by the custom block
216+
// listeners when the owning plugin is installed.
217+
limits.put(nsKey, limit);
218+
return;
219+
}
212220
Material mat = Registry.MATERIAL.get(nsKey);
213221
if (mat != null) {
214222
if (!mat.isBlock()) {
@@ -445,7 +453,7 @@ private int process(Block b, boolean add) {
445453
* @return limit amount if over limit, or -1 if no limitation
446454
*/
447455
private int process(Block b, BlockData blockData, boolean add) {
448-
if (DO_NOT_COUNT.contains(fixMaterial(blockData)) || !addon.inGameModeWorld(b.getWorld())) {
456+
if (DO_NOT_COUNT.contains(fixMaterial(blockData))) {
449457
return -1;
450458
}
451459
// Stacked-plants-as-one: a segment sitting on the same plant is not counted,
@@ -454,21 +462,34 @@ private int process(Block b, BlockData blockData, boolean add) {
454462
&& isSamePlant(b.getRelative(BlockFace.DOWN).getType(), fixMaterial(blockData))) {
455463
return -1;
456464
}
457-
Environment env = envOf(b.getWorld());
458-
return addon.getIslands().getIslandAt(b.getLocation()).map(i -> {
465+
return processKey(b.getWorld(), b.getLocation(), fixMaterial(blockData), add);
466+
}
467+
468+
/**
469+
* Count and limit-check a namespaced key at a location. Public so custom-block
470+
* listeners (ItemsAdder, Oraxen) can run their block ids through the same
471+
* counting and limit machinery as vanilla blocks.
472+
*
473+
* @return limit amount if at/over the limit (nothing was counted), or -1 on success
474+
*/
475+
public int processKey(World world, Location location, NamespacedKey key, boolean add) {
476+
if (!addon.inGameModeWorld(world)) {
477+
return -1;
478+
}
479+
Environment env = envOf(world);
480+
return addon.getIslands().getIslandAt(location).map(i -> {
459481
String id = i.getUniqueId();
460-
String gameMode = addon.getGameModeName(b.getWorld());
482+
String gameMode = addon.getGameModeName(world);
461483
if (gameMode.isEmpty()) {
462484
return -1;
463485
}
464486
if (addon.getConfig().getBoolean("ignore-center-block", true)
465-
&& i.getCenter().equals(b.getLocation())) {
487+
&& i.getCenter().equals(location)) {
466488
return -1;
467489
}
468490
islandCountMap.putIfAbsent(id, new IslandBlockCount(id, gameMode));
469-
NamespacedKey key = fixMaterial(blockData);
470491
if (add) {
471-
int limit = checkLimit(b.getWorld(), env, key, id);
492+
int limit = checkLimit(world, env, key, id);
472493
if (limit > -1) {
473494
return limit;
474495
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package world.bentobox.limits.listeners;
2+
3+
import java.util.Locale;
4+
5+
import org.bukkit.NamespacedKey;
6+
import org.bukkit.block.Block;
7+
import org.bukkit.entity.Player;
8+
import org.bukkit.event.Cancellable;
9+
10+
import world.bentobox.bentobox.api.localization.TextVariables;
11+
import world.bentobox.bentobox.api.user.User;
12+
import world.bentobox.bentobox.util.Util;
13+
import world.bentobox.limits.Limits;
14+
15+
/**
16+
* Shared plumbing for custom-block plugin listeners (ItemsAdder, Oraxen). Runs custom
17+
* block ids through the same counting and limit machinery as vanilla blocks via
18+
* {@link BlockLimitsListener#processKey}.
19+
*/
20+
final class CustomBlockHandler {
21+
22+
private CustomBlockHandler() {
23+
}
24+
25+
/**
26+
* Limit-check and count a custom block placement; cancels the event and notifies
27+
* the player when the limit is hit.
28+
*/
29+
static void place(Limits addon, Cancellable event, Player player, Block block, NamespacedKey key) {
30+
if (key == null) {
31+
return;
32+
}
33+
int limit = addon.getBlockLimitListener().processKey(block.getWorld(), block.getLocation(), key, true);
34+
if (limit > -1) {
35+
event.setCancelled(true);
36+
if (player != null) {
37+
User.getInstance(player).notify("block-limits.hit-limit",
38+
"[material]", Util.prettifyText(key.getKey()),
39+
TextVariables.NUMBER, String.valueOf(limit));
40+
}
41+
}
42+
}
43+
44+
/**
45+
* Decrement the count for a removed custom block.
46+
*/
47+
static void remove(Limits addon, Block block, NamespacedKey key) {
48+
if (key != null) {
49+
addon.getBlockLimitListener().processKey(block.getWorld(), block.getLocation(), key, false);
50+
}
51+
}
52+
53+
/**
54+
* Parse a custom block id into a namespaced key. ItemsAdder ids already carry a
55+
* namespace; plain ids get the supplied default namespace (e.g. "oraxen").
56+
*/
57+
static NamespacedKey toKey(String id, String defaultNamespace) {
58+
if (id == null) {
59+
return null;
60+
}
61+
String full = id.contains(":") ? id : defaultNamespace + ":" + id;
62+
return NamespacedKey.fromString(full.toLowerCase(Locale.ROOT));
63+
}
64+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package world.bentobox.limits.listeners;
2+
3+
import org.bukkit.event.EventHandler;
4+
import org.bukkit.event.EventPriority;
5+
import org.bukkit.event.Listener;
6+
7+
import dev.lone.itemsadder.api.Events.CustomBlockBreakEvent;
8+
import dev.lone.itemsadder.api.Events.CustomBlockPlaceEvent;
9+
import world.bentobox.limits.Limits;
10+
11+
/**
12+
* Applies block limits to ItemsAdder custom blocks. Registered only when the
13+
* ItemsAdder plugin is present. Config keys use the ItemsAdder namespaced id, e.g.
14+
* {@code "iafestivities:christmas/christmas_tree/green_orb": 5} under blocklimits.
15+
*/
16+
public class ItemsAdderListener implements Listener {
17+
18+
private final Limits addon;
19+
20+
public ItemsAdderListener(Limits addon) {
21+
this.addon = addon;
22+
}
23+
24+
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
25+
public void onPlace(CustomBlockPlaceEvent e) {
26+
CustomBlockHandler.place(addon, e, e.getPlayer(), e.getBlock(),
27+
CustomBlockHandler.toKey(e.getNamespacedID(), "itemsadder"));
28+
}
29+
30+
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
31+
public void onBreak(CustomBlockBreakEvent e) {
32+
CustomBlockHandler.remove(addon, e.getBlock(),
33+
CustomBlockHandler.toKey(e.getNamespacedID(), "itemsadder"));
34+
}
35+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package world.bentobox.limits.listeners;
2+
3+
import org.bukkit.event.EventHandler;
4+
import org.bukkit.event.EventPriority;
5+
import org.bukkit.event.Listener;
6+
7+
import io.th0rgal.oraxen.api.events.noteblock.OraxenNoteBlockBreakEvent;
8+
import io.th0rgal.oraxen.api.events.noteblock.OraxenNoteBlockPlaceEvent;
9+
import io.th0rgal.oraxen.api.events.stringblock.OraxenStringBlockBreakEvent;
10+
import io.th0rgal.oraxen.api.events.stringblock.OraxenStringBlockPlaceEvent;
11+
import world.bentobox.limits.Limits;
12+
13+
/**
14+
* Applies block limits to Oraxen custom blocks (note-block and string-block
15+
* mechanics). Registered only when the Oraxen plugin is present. Config keys use the
16+
* {@code oraxen:} namespace, e.g. {@code "oraxen:caveblock": 10} under blocklimits.
17+
*/
18+
public class OraxenListener implements Listener {
19+
20+
private static final String NAMESPACE = "oraxen";
21+
22+
private final Limits addon;
23+
24+
public OraxenListener(Limits addon) {
25+
this.addon = addon;
26+
}
27+
28+
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
29+
public void onNoteBlockPlace(OraxenNoteBlockPlaceEvent e) {
30+
CustomBlockHandler.place(addon, e, e.getPlayer(), e.getBlock(),
31+
CustomBlockHandler.toKey(e.getMechanic().getItemID(), NAMESPACE));
32+
}
33+
34+
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
35+
public void onNoteBlockBreak(OraxenNoteBlockBreakEvent e) {
36+
CustomBlockHandler.remove(addon, e.getBlock(),
37+
CustomBlockHandler.toKey(e.getMechanic().getItemID(), NAMESPACE));
38+
}
39+
40+
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
41+
public void onStringBlockPlace(OraxenStringBlockPlaceEvent e) {
42+
CustomBlockHandler.place(addon, e, e.getPlayer(), e.getBlock(),
43+
CustomBlockHandler.toKey(e.getMechanic().getItemID(), NAMESPACE));
44+
}
45+
46+
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
47+
public void onStringBlockBreak(OraxenStringBlockBreakEvent e) {
48+
CustomBlockHandler.remove(addon, e.getBlock(),
49+
CustomBlockHandler.toKey(e.getMechanic().getItemID(), NAMESPACE));
50+
}
51+
}

src/main/resources/config.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ apply-member-limit-perms: false
5353
# Use blocklimits-nether / blocklimits-end below to override per environment.
5454
# The following general terms can be used: STAIRS, DOORS, SIGNS, LOGS, SNOW, LEAVES,
5555
# SHULKER_BOXES, BANNERS, RAILS, ICE, BUTTONS, CANDLE_CAKES, CAMPFIRES, SLABS, STONE_BRICKS
56+
# ItemsAdder / Oraxen custom blocks can be limited too (when the plugin is installed):
57+
# use the ItemsAdder namespaced id, or an Oraxen id prefixed with "oraxen:", as the key.
58+
# Quote keys containing a colon. Custom blocks are tracked from their place/break
59+
# events; a recount preserves their stored counts rather than rescanning them.
60+
# "iafestivities:christmas/christmas_tree/green_orb": 5
61+
# "oraxen:caveblock": 10
5662
blocklimits:
5763
HOPPER: 10
5864

src/test/java/world/bentobox/limits/listeners/BlockLimitsListenerTest.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,6 +1095,32 @@ void testIndividualLimitStillAppliesInsideGroup() {
10951095
listener.onBlock(event);
10961096

10971097
assertTrue(event.isCancelled());
1098+
// --- Custom block keys (#176) ---
1099+
1100+
@Test
1101+
void testProcessKeyCustomNamespaceLimitAndCount() {
1102+
NamespacedKey custom = NamespacedKey.fromString("itemsadder:some_pack/tree");
1103+
IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock");
1104+
ibc.setBlockLimit(Environment.NORMAL, custom, 1);
1105+
listener.setIsland("test-island-id", ibc);
1106+
1107+
// First add succeeds and counts
1108+
assertEquals(-1, listener.processKey(world, blockLocation, custom, true));
1109+
assertEquals(1, listener.getIsland("test-island-id").getBlockCount(Environment.NORMAL, custom));
1110+
// Second add hits the limit and must not count
1111+
assertEquals(1, listener.processKey(world, blockLocation, custom, true));
1112+
assertEquals(1, listener.getIsland("test-island-id").getBlockCount(Environment.NORMAL, custom));
1113+
// Removal decrements
1114+
assertEquals(-1, listener.processKey(world, blockLocation, custom, false));
1115+
assertEquals(0, listener.getIsland("test-island-id").getBlockCount(Environment.NORMAL, custom));
1116+
}
1117+
1118+
@Test
1119+
void testCustomNamespaceKeyAcceptedInBlockLimitsConfig() {
1120+
config.set("blocklimits.oraxen:caveblock", 10);
1121+
BlockLimitsListener custom = new BlockLimitsListener(addon);
1122+
assertEquals(10,
1123+
custom.getMaterialLimits(world, "no-island").get(NamespacedKey.fromString("oraxen:caveblock")));
10981124
}
10991125

11001126
// --- Block cascade tests ---
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package world.bentobox.limits.listeners;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertNull;
5+
6+
import org.bukkit.NamespacedKey;
7+
import org.junit.jupiter.api.Test;
8+
9+
class CustomBlockHandlerTest {
10+
11+
@Test
12+
void testToKeyNamespacedIdPassesThrough() {
13+
NamespacedKey key = CustomBlockHandler.toKey("iafestivities:christmas/christmas_tree/green_orb",
14+
"itemsadder");
15+
assertEquals("iafestivities", key.getNamespace());
16+
assertEquals("christmas/christmas_tree/green_orb", key.getKey());
17+
}
18+
19+
@Test
20+
void testToKeyPlainIdGetsDefaultNamespace() {
21+
NamespacedKey key = CustomBlockHandler.toKey("caveblock", "oraxen");
22+
assertEquals("oraxen", key.getNamespace());
23+
assertEquals("caveblock", key.getKey());
24+
}
25+
26+
@Test
27+
void testToKeyUppercaseIsLowercased() {
28+
NamespacedKey key = CustomBlockHandler.toKey("CaveBlock", "oraxen");
29+
assertEquals("caveblock", key.getKey());
30+
}
31+
32+
@Test
33+
void testToKeyInvalidReturnsNull() {
34+
assertNull(CustomBlockHandler.toKey("bad key with spaces", "oraxen"));
35+
assertNull(CustomBlockHandler.toKey(null, "oraxen"));
36+
}
37+
}

0 commit comments

Comments
 (0)