Skip to content

Commit 20fdc22

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 7255a2f commit 20fdc22

10 files changed

Lines changed: 635 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
@@ -66,6 +66,14 @@ public void onEnable() {
6666
registerListener(joinListener);
6767
EntityLimitListener entityLimitListener = new EntityLimitListener(this);
6868
registerListener(entityLimitListener);
69+
if (org.bukkit.Bukkit.getPluginManager().getPlugin("ItemsAdder") != null) {
70+
registerListener(new world.bentobox.limits.listeners.ItemsAdderListener(this));
71+
log("ItemsAdder detected: custom block limits active. Use ItemsAdder ids as blocklimits keys.");
72+
}
73+
if (org.bukkit.Bukkit.getPluginManager().getPlugin("Oraxen") != null) {
74+
registerListener(new world.bentobox.limits.listeners.OraxenListener(this));
75+
log("Oraxen detected: custom block limits active. Use \"oraxen:<itemid>\" blocklimits keys.");
76+
}
6977
try {
7078
Class.forName("io.papermc.paper.event.entity.ShulkerDuplicateEvent");
7179
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
@@ -231,9 +231,24 @@ private void scanEntities() {
231231

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

238253
// Recount entities (loaded only) and write per-env
239254
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;
@@ -208,6 +209,13 @@ private NamespacedKey parseConfigKey(String key) {
208209

209210
/** Resolve a key to a material or block tag and store its limit, warning on any mismatch. */
210211
private void registerLimit(Map<NamespacedKey, Integer> limits, NamespacedKey nsKey, String key, int limit) {
212+
if (!NamespacedKey.MINECRAFT.equals(nsKey.getNamespace())) {
213+
// Custom-namespace key, e.g. an ItemsAdder block id ("iafestivities:christmas/tree")
214+
// or an Oraxen id written as "oraxen:<itemid>". Enforced by the custom block
215+
// listeners when the owning plugin is installed.
216+
limits.put(nsKey, limit);
217+
return;
218+
}
211219
Material mat = Registry.MATERIAL.get(nsKey);
212220
if (mat != null) {
213221
if (!mat.isBlock()) {
@@ -437,7 +445,7 @@ private int process(Block b, boolean add) {
437445
* @return limit amount if over limit, or -1 if no limitation
438446
*/
439447
private int process(Block b, BlockData blockData, boolean add) {
440-
if (DO_NOT_COUNT.contains(fixMaterial(blockData)) || !addon.inGameModeWorld(b.getWorld())) {
448+
if (DO_NOT_COUNT.contains(fixMaterial(blockData))) {
441449
return -1;
442450
}
443451
// Stacked-plants-as-one: a segment sitting on the same plant is not counted,
@@ -446,21 +454,34 @@ private int process(Block b, BlockData blockData, boolean add) {
446454
&& isSamePlant(b.getRelative(BlockFace.DOWN).getType(), fixMaterial(blockData))) {
447455
return -1;
448456
}
449-
Environment env = envOf(b.getWorld());
450-
return addon.getIslands().getIslandAt(b.getLocation()).map(i -> {
457+
return processKey(b.getWorld(), b.getLocation(), fixMaterial(blockData), add);
458+
}
459+
460+
/**
461+
* Count and limit-check a namespaced key at a location. Public so custom-block
462+
* listeners (ItemsAdder, Oraxen) can run their block ids through the same
463+
* counting and limit machinery as vanilla blocks.
464+
*
465+
* @return limit amount if at/over the limit (nothing was counted), or -1 on success
466+
*/
467+
public int processKey(World world, Location location, NamespacedKey key, boolean add) {
468+
if (!addon.inGameModeWorld(world)) {
469+
return -1;
470+
}
471+
Environment env = envOf(world);
472+
return addon.getIslands().getIslandAt(location).map(i -> {
451473
String id = i.getUniqueId();
452-
String gameMode = addon.getGameModeName(b.getWorld());
474+
String gameMode = addon.getGameModeName(world);
453475
if (gameMode.isEmpty()) {
454476
return -1;
455477
}
456478
if (addon.getConfig().getBoolean("ignore-center-block", true)
457-
&& i.getCenter().equals(b.getLocation())) {
479+
&& i.getCenter().equals(location)) {
458480
return -1;
459481
}
460482
islandCountMap.putIfAbsent(id, new IslandBlockCount(id, gameMode));
461-
NamespacedKey key = fixMaterial(blockData);
462483
if (add) {
463-
int limit = checkLimit(b.getWorld(), env, key, id);
484+
int limit = checkLimit(world, env, key, id);
464485
if (limit > -1) {
465486
return limit;
466487
}
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
@@ -43,6 +43,12 @@ show-limit-messages: true
4343
# Use blocklimits-nether / blocklimits-end below to override per environment.
4444
# The following general terms can be used: STAIRS, DOORS, SIGNS, LOGS, SNOW, LEAVES,
4545
# SHULKER_BOXES, BANNERS, RAILS, ICE, BUTTONS, CANDLE_CAKES, CAMPFIRES, SLABS, STONE_BRICKS
46+
# ItemsAdder / Oraxen custom blocks can be limited too (when the plugin is installed):
47+
# use the ItemsAdder namespaced id, or an Oraxen id prefixed with "oraxen:", as the key.
48+
# Quote keys containing a colon. Custom blocks are tracked from their place/break
49+
# events; a recount preserves their stored counts rather than rescanning them.
50+
# "iafestivities:christmas/christmas_tree/green_orb": 5
51+
# "oraxen:caveblock": 10
4652
blocklimits:
4753
HOPPER: 10
4854

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,6 +1022,34 @@ void testStackedPlantBreakBaseDecrementsOnlyOneWhenEnabled() {
10221022
assertEquals(0, listener.getIsland("test-island-id").getBlockCount(Material.SUGAR_CANE.getKey()));
10231023
}
10241024

1025+
// --- Custom block keys (#176) ---
1026+
1027+
@Test
1028+
void testProcessKeyCustomNamespaceLimitAndCount() {
1029+
NamespacedKey custom = NamespacedKey.fromString("itemsadder:some_pack/tree");
1030+
IslandBlockCount ibc = new IslandBlockCount("test-island-id", "BSkyBlock");
1031+
ibc.setBlockLimit(Environment.NORMAL, custom, 1);
1032+
listener.setIsland("test-island-id", ibc);
1033+
1034+
// First add succeeds and counts
1035+
assertEquals(-1, listener.processKey(world, blockLocation, custom, true));
1036+
assertEquals(1, listener.getIsland("test-island-id").getBlockCount(Environment.NORMAL, custom));
1037+
// Second add hits the limit and must not count
1038+
assertEquals(1, listener.processKey(world, blockLocation, custom, true));
1039+
assertEquals(1, listener.getIsland("test-island-id").getBlockCount(Environment.NORMAL, custom));
1040+
// Removal decrements
1041+
assertEquals(-1, listener.processKey(world, blockLocation, custom, false));
1042+
assertEquals(0, listener.getIsland("test-island-id").getBlockCount(Environment.NORMAL, custom));
1043+
}
1044+
1045+
@Test
1046+
void testCustomNamespaceKeyAcceptedInBlockLimitsConfig() {
1047+
config.set("blocklimits.oraxen:caveblock", 10);
1048+
BlockLimitsListener custom = new BlockLimitsListener(addon);
1049+
assertEquals(10,
1050+
custom.getMaterialLimits(world, "no-island").get(NamespacedKey.fromString("oraxen:caveblock")));
1051+
}
1052+
10251053
// --- Block cascade tests ---
10261054

10271055
@Test
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)