Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
0c55d53
Version 1.23.0
tastybento Apr 5, 2026
c824533
Update BlockListenerTest2 to assert no exceptions are thrown during e…
tastybento Apr 6, 2026
955381b
Refactor OneBlocksManager and PhasesPanel for improved readability an…
tastybento Apr 6, 2026
f0d8bc7
Fix description formatting in PhasesPanel to correctly handle pipe ch…
tastybento Apr 6, 2026
b1e6985
Add no-op implementations for setDifficulty and setResetEpoch in test…
tastybento Apr 6, 2026
2b0ad28
Update API version to 3.14.0 in addon.yml
tastybento Apr 6, 2026
8c007f6
Implement translation key handling using Adventure Components in Chec…
tastybento Apr 6, 2026
f6f120c
Downgrade API version to 3.13.0 in addon.yml
tastybento Apr 6, 2026
59fc0a2
Initial plan
Copilot Apr 6, 2026
ef97465
Initial plan
Copilot Apr 6, 2026
cc1b5e1
Revert CheckPhase.java to use user.sendMessage() directly instead of …
Copilot Apr 6, 2026
7f19977
chore: update BentoBox Maven dependency to 3.13.0 for MiniMessage sup…
Copilot Apr 6, 2026
af015f4
Merge pull request #486 from BentoBoxWorld/copilot/sub-pr-484-again
tastybento Apr 6, 2026
5123896
Fix broken tests: restore showTitle() Adventure API in CheckPhase (no…
Copilot Apr 6, 2026
06a5bee
Fix InfoListenerTest: verify via lm.get() instead of deprecated spigo…
Copilot Apr 6, 2026
7d14d50
Remove obsolete testOnInfo from InfoListenerTest
tastybento Apr 7, 2026
976f3af
Merge branch 'develop' into copilot/sub-pr-484
tastybento Apr 7, 2026
b69a5f7
Merge pull request #485 from BentoBoxWorld/copilot/sub-pr-484
tastybento Apr 7, 2026
878e732
Update src/main/java/world/bentobox/aoneblock/panels/PhasesPanel.java
tastybento Apr 7, 2026
0dad719
Initial plan
Copilot Apr 7, 2026
8e5c2af
Improve Title assertions in CheckPhaseTest using ArgumentCaptor
Copilot Apr 7, 2026
8bac957
Rename numbered titleCaptor variables for consistency
Copilot Apr 7, 2026
ee60d7c
Merge pull request #487 from BentoBoxWorld/copilot/sub-pr-484
tastybento Apr 7, 2026
bdcc1ed
Add mob-data and mythic-mob custom block types
tastybento Apr 8, 2026
dd8d7e8
Fix mob-data /summon ordering and add custom-blocks sibling section
tastybento Apr 8, 2026
f61d3fb
Add `block` custom-blocks alias for /setblock with NBT
tastybento Apr 8, 2026
848071d
Add custom-blocks examples to 0_plains.yml
tastybento Apr 8, 2026
13e0914
Add spawner timing fields to block example NBT
tastybento Apr 8, 2026
5363387
Merge pull request #489 from BentoBoxWorld/feature/mob-data-and-mythi…
tastybento Apr 8, 2026
3d15adf
Refactor Russian localization in ru.yml
tastybento Apr 8, 2026
8c8a42a
Merge pull request #492 from BentoBoxWorld/tastybento-patch-1
tastybento Apr 8, 2026
97f88dd
Spawn custom-blocks bosses and spawners on the current tick
tastybento Apr 8, 2026
c568185
Merge pull request #493 from BentoBoxWorld/feature/faster-custom-spawns
tastybento Apr 8, 2026
9214438
Add continuous brushing for placed suspicious blocks
tastybento Apr 9, 2026
1261851
Merge pull request #494 from BentoBoxWorld/fix/continuous-suspicious-…
tastybento Apr 9, 2026
78f8deb
Fix formatting issues in Russian locale file
tastybento Apr 9, 2026
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
89 changes: 89 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,95 @@ If CHEST is listed in the blocks section, then it will be randomly filled accord

Be very careful when editing the chest items and check that the material is a true Bukkit material and spelled correctly.

### Custom block entries

Phase `blocks:` sections can also be written as a YAML list, which unlocks
custom entry types:

```yaml
blocks:
- type: block-data
data: redstone_wire[power=15]
probability: 20

# `type: block` runs the vanilla /setblock command at the magic-block
# position. The data string may include block states `[…]`, NBT `{…}`,
# and a trailing destroy|keep|replace mode — anything valid after
# `setblock <x> <y> <z>`. Use single quotes around the data so any
# double quotes inside the NBT don't clash with YAML string delimiters.
- type: block
data: 'spawner{Delay:0,MinSpawnDelay:200,MaxSpawnDelay:800,SpawnCount:1,SpawnRange:4,MaxNearbyEntities:6,RequiredPlayerRange:16,SpawnData:{entity:{id:breeze,CustomName:[{text:"Breezy Generator",color:"#f90606"}],CustomNameVisible:1b,Glowing:1b,active_effects:[{id:unluck,duration:200,ambient:1b,show_particles:1b}],attributes:[{id:scale,base:2f}]}}} replace'
probability: 10

# #488: summon an entity with vanilla NBT/component data, same syntax as /summon.
# After spawning, blocks inside the mob's (scaled) bounding box are cleared.
- type: mob-data
data: breeze{CustomName:[{text:Breezy,color:"#f90606"}],CustomNameVisible:1b,Glowing:1b,attributes:[{id:scale,base:2f}]}
underlying-block: STONE
probability: 15

# #303: spawn a MythicMob via BentoBox's MythicMobs hook. Requires the MythicMobs
# plugin to be installed; otherwise the entry is logged and skipped at runtime.
- type: mythic-mob
mob: SkeletalKnight
level: 3
power: 1.0
display-name: "Boss"
underlying-block: STONE
probability: 5
```

`type: block` is an alias for `type: block-data` — both route to the same
handler. Use `block-data` when you only need simple block states
(`redstone_wire[power=15]`); use `block` when the data contains NBT or a
setblock mode flag so the intent is obvious at a glance.

> **Spawner gotcha:** placing a `spawner` via `/setblock` only sets the fields
> you provide — everything else defaults to `0`/`-1`, which leaves the spawner
> **inactive** (`Delay:-1` means "never tick"). If you want it to actually spawn
> mobs, set `Delay`, `MinSpawnDelay`, `MaxSpawnDelay`, `SpawnCount`, `SpawnRange`,
> `MaxNearbyEntities`, and `RequiredPlayerRange` explicitly, as shown in the
> example above. `Delay:0` spawns on the very next tick (visually instant),
> `Delay:N` waits N ticks before the first spawn, and `Delay:-1` never ticks.
> Test your full data string in-game with `/setblock ~ ~ ~ <data>` first — if
> the spawner ticks there, it will tick from `custom-blocks:` too.

> **Note:** the `mob-data` string is passed straight to the vanilla `/summon`
> command, so it must be valid NBT for your server version. A few 1.21 gotchas:
> attribute ids no longer use the `generic.`/`player.` prefix (`scale`, not
> `generic.scale`), numeric attribute bases need a float suffix (`base:2f`),
> and `CustomName` must be a text-component list (`[{text:Breezy}]`), not a
> plain string. Test the command in-game first with `/summon <your data>` —
> if it works there it will work here. Bad NBT is logged and the spawn is
> skipped.

If you'd rather leave your existing `blocks:` map-form section untouched, you
can put custom entries in a sibling `custom-blocks:` list. Both sections are
read and their entries merged into the same weighted pool, so probabilities in
the two sections are directly comparable:

```yaml
blocks:
PODZOL: 40
DIRT: 1000
OAK_LOG: 2000

custom-blocks:
- type: mob-data
data: breeze{CustomName:[{text:Breezy,color:"#f90606"}],CustomNameVisible:1b,Glowing:1b,attributes:[{id:scale,base:2f}]}
underlying-block: STONE
probability: 50

- type: block
data: 'spawner{Delay:0,MinSpawnDelay:200,MaxSpawnDelay:800,SpawnCount:1,SpawnRange:4,MaxNearbyEntities:6,RequiredPlayerRange:16,SpawnData:{entity:{id:breeze,CustomName:[{text:"Breezy Generator",color:"#f90606"}],CustomNameVisible:1b,Glowing:1b,active_effects:[{id:unluck,duration:200,ambient:1b,show_particles:1b}],attributes:[{id:scale,base:2f}]}}} replace'
probability: 10

- type: mythic-mob
mob: SkeletalKnight
level: 3
probability: 5
```

### Other Add-ons

OneBlock is an add-on that uses the BentoBox API. Here are some other ones that you may be interested in:
Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
<mockito.version>5.11.0</mockito.version>
<mock-bukkit.version>v1.21-SNAPSHOT</mock-bukkit.version>
<!-- More visible way how to change dependency versions -->
<bentobox.version>3.10.0</bentobox.version>
<bentobox.version>3.13.0</bentobox.version>
<items-adder.version>4.0.10</items-adder.version>
<nexo.version>1.8.0</nexo.version>
Comment thread
tastybento marked this conversation as resolved.
<level.version>2.6.2</level.version>
Expand All @@ -66,7 +66,7 @@
<!-- Do not change unless you want different name for local builds. -->
<build.number>-LOCAL</build.number>
<!-- This allows to change between versions. -->
<build.version>1.22.1</build.version>
<build.version>1.23.0</build.version>
<!-- SonarCloud -->
<sonar.projectKey>BentoBoxWorld_AOneBlock</sonar.projectKey>
<sonar.organization>bentobox-world</sonar.organization>
Expand Down
119 changes: 111 additions & 8 deletions src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.util.Optional;
import java.util.Random;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;

import org.bukkit.Bukkit;
Expand Down Expand Up @@ -46,10 +47,12 @@
import org.bukkit.event.entity.ItemSpawnEvent;
import org.bukkit.event.player.PlayerBucketFillEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.loot.LootContext;
import org.bukkit.loot.LootTable;
import org.bukkit.scheduler.BukkitTask;
import org.bukkit.util.Vector;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
Expand Down Expand Up @@ -97,6 +100,17 @@ public class BlockListener extends FlagListener implements Listener {
*/
private final Map<String, OneBlockIslands> cache;

/**
* Active continuous-brushing sessions, keyed by player UUID. Each session
* holds the repeating task driving dust progression and the block being brushed.
*/
private final Map<UUID, BrushSession> brushSessions = new HashMap<>();

/**
* Per-player brushing session state.
*/
private record BrushSession(BukkitTask task, Block block) {}

/**
* Helper class to check phase requirements.
*/
Expand Down Expand Up @@ -711,29 +725,118 @@ public void onPlayerInteract(PlayerInteractEvent e) {
if (block.getBlockData() instanceof Brushable bb) {
int dusted = bb.getDusted() + 1;
if (dusted > bb.getMaximumDusted()) {
completeBrush(e, block);
} else {
completeBrush(e.getPlayer(), block);
return;
}
bb.setDusted(dusted);
block.setBlockData(bb);
playBrushFeedback(block);
// Kick off a continuous-brush session so the player can hold right-click
// and have dusting advance automatically (vanilla feel). The kickoff click
// above already advances one stage; the timer picks up from there.
Player player = e.getPlayer();
UUID uuid = player.getUniqueId();
BrushSession existing = brushSessions.get(uuid);
if (existing != null && !existing.block().equals(block)) {
cancelBrushSession(uuid);
existing = null;
}
if (existing == null) {
brushSessions.put(uuid, startContinuousBrush(player, block));
}
}
}

/**
* Schedules a repeating task that advances brushing on the given block while the
* player keeps holding right-click with the brush. Period of 10 ticks per dust stage
* matches the vanilla brush cadence.
* @param player The brushing player.
* @param block The suspicious block being brushed.
* @return A new BrushSession holding the scheduled task.
*/
private BrushSession startContinuousBrush(Player player, Block block) {
UUID uuid = player.getUniqueId();
BukkitTask task = Bukkit.getScheduler().runTaskTimer(addon.getPlugin(), new Runnable() {
@Override
public void run() {
// Validate that the player is still actively brushing this block.
if (!player.isOnline()
|| player.getInventory().getItemInMainHand().getType() != Material.BRUSH
|| !player.isHandRaised()
|| !block.equals(player.getTargetBlockExact(5))
|| (block.getType() != Material.SUSPICIOUS_GRAVEL
&& block.getType() != Material.SUSPICIOUS_SAND)
|| !(block.getBlockData() instanceof Brushable bb)) {
cancelBrushSession(uuid);
return;
}
int dusted = bb.getDusted() + 1;
if (dusted > bb.getMaximumDusted()) {
completeBrush(player, block);
cancelBrushSession(uuid);
return;
}
bb.setDusted(dusted);
block.setBlockData(bb);
playBrushFeedback(block);
}
}, 10L, 10L);
return new BrushSession(task, block);
}

/**
* Cancels any active brushing session for the given player UUID.
* @param uuid The player's UUID.
*/
private void cancelBrushSession(UUID uuid) {
BrushSession session = brushSessions.remove(uuid);
if (session != null) {
session.task().cancel();
}
}

/**
* Clean up any brushing session when a player disconnects.
* @param e The PlayerQuitEvent.
*/
@EventHandler
public void onPlayerQuit(PlayerQuitEvent e) {
cancelBrushSession(e.getPlayer().getUniqueId());
}

/**
* Plays brushing particles and sound at a suspicious block to give visible/audible
* progress feedback. Needed because the block is placed programmatically rather than
* spawning naturally, so the vanilla brush animation is not triggered on clients.
* @param block The suspicious block being brushed.
*/
private void playBrushFeedback(Block block) {
World world = block.getWorld();
Location center = block.getLocation().add(0.5, 0.5, 0.5);
// Dust particles using the block's own data so they match sand/gravel colour.
world.spawnParticle(Particle.BLOCK, center, 10, 0.25, 0.25, 0.25, 0.0, block.getBlockData());
Sound brushSound = (block.getType() == Material.SUSPICIOUS_GRAVEL)
? Sound.ITEM_BRUSH_BRUSHING_GRAVEL
: Sound.ITEM_BRUSH_BRUSHING_SAND;
world.playSound(center, brushSound, 0.8f, 1.0f);
}

/**
* Completes the brushing of a suspicious block: drops loot (if available), plays the
* break sound, removes the block, fires a BlockBreakEvent, and damages the brush.
* @param e The originating PlayerInteractEvent.
* @param block The suspicious block being brushed.
* @param player The brushing player.
* @param block The suspicious block being brushed.
*/
private void completeBrush(PlayerInteractEvent e, Block block) {
private void completeBrush(Player player, Block block) {
Location loc = block.getLocation().add(0.5, 0.5, 0.5);
World world = block.getWorld();

if (block.getState() instanceof BrushableBlock suspiciousBlock) {
LootTable lootTable = suspiciousBlock.getLootTable();
if (lootTable != null) {
LootContext context = new LootContext.Builder(loc)
.lootedEntity(e.getPlayer()).killer(e.getPlayer()).build();
.lootedEntity(player).killer(player).build();
Collection<ItemStack> items = lootTable.populateLoot(new Random(), context);
for (ItemStack item : items) {
world.dropItemNaturally(loc, item);
Expand All @@ -746,8 +849,8 @@ private void completeBrush(PlayerInteractEvent e, Block block) {
: Sound.BLOCK_SUSPICIOUS_SAND_BREAK;
world.playSound(loc, breakSound, 1.0f, 1.0f);
block.setType(Material.AIR);
Bukkit.getPluginManager().callEvent(new BlockBreakEvent(block, e.getPlayer()));
e.getPlayer().getInventory().getItemInMainHand().damage(1, e.getPlayer());
Bukkit.getPluginManager().callEvent(new BlockBreakEvent(block, player));
player.getInventory().getItemInMainHand().damage(1, player);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;

import net.kyori.adventure.text.Component;
import net.kyori.adventure.title.Title;

import world.bentobox.aoneblock.AOneBlock;
import world.bentobox.aoneblock.dataobjects.OneBlockIslands;
import world.bentobox.aoneblock.oneblocks.OneBlockPhase;
Expand Down Expand Up @@ -82,7 +85,7 @@ void setNewPhase(@Nullable Player player, @NonNull Island i, @NonNull OneBlockIs
// Set the phase name
is.setPhaseName(newPhaseName);
if (user.isPlayer() && user.isOnline() && addon.inWorld(user.getWorld())) {
user.getPlayer().sendTitle(newPhaseName, null, -1, -1, -1);
user.getPlayer().showTitle(Title.title(Component.text(newPhaseName), Component.empty()));
}
// Run phase start commands
Util.runCommands(user,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

import world.bentobox.aoneblock.oneblocks.customblock.BlockDataCustomBlock;
import world.bentobox.aoneblock.oneblocks.customblock.MobCustomBlock;
import world.bentobox.aoneblock.oneblocks.customblock.MobDataCustomBlock;
import world.bentobox.aoneblock.oneblocks.customblock.MythicMobCustomBlock;

/**
* A creator for {@link OneBlockCustomBlock}
Expand All @@ -22,7 +24,14 @@ public final class OneBlockCustomBlockCreator {

static {
register("block-data", BlockDataCustomBlock::fromMap);
// Alias: `block` routes to the same handler as `block-data`. Both accept
// anything valid after `setblock <x> <y> <z>` — a block id, optional
// states `[…]`, optional NBT `{…}`, and an optional
// destroy|keep|replace mode.
register("block", BlockDataCustomBlock::fromMap);
register("mob", MobCustomBlock::fromMap);
register("mob-data", MobDataCustomBlock::fromMap);
register("mythic-mob", MythicMobCustomBlock::fromMap);
register("short", map -> {
String type = Objects.toString(map.get("data"), null);
if (type == null) {
Expand Down
Loading
Loading