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
87 changes: 87 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,93 @@ 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:20,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. 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:20,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
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
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public class OneBlocksManager {
private static final String CONTENTS = "contents";
private static final String MOBS = "mobs";
private static final String BLOCKS = "blocks";
private static final String CUSTOM_BLOCKS = "custom-blocks";
private static final String PHASES = "phases";
private static final String GOTO_BLOCK = "gotoBlock";
private static final String START_COMMANDS = "start-commands";
Expand Down Expand Up @@ -500,17 +501,30 @@ void addBlocks(OneBlockPhase obPhase, ConfigurationSection phase) {
}
}

int probability = Integer.parseInt(Objects.toString(map.get("probability"), "0"));
Optional<OneBlockCustomBlock> customBlock = OneBlockCustomBlockCreator.create(map);
if (customBlock.isPresent()) {
obPhase.addCustomBlock(customBlock.get(), probability);
} else {
addon.logError("Bad custom block in " + obPhase.getPhaseName() + ": " + map);
}
addCustomBlockFromMap(obPhase, map);
}
}

// Optional sibling list holding only custom entries. Lets admins keep the
// existing map-form `blocks:` section untouched while still registering
// custom block types like `mob-data` or `mythic-mob`.
if (phase.isList(CUSTOM_BLOCKS)) {
for (Map<?, ?> map : phase.getMapList(CUSTOM_BLOCKS)) {
addCustomBlockFromMap(obPhase, map);
}
}
}

private void addCustomBlockFromMap(OneBlockPhase obPhase, Map<?, ?> map) {
int probability = Integer.parseInt(Objects.toString(map.get("probability"), "0"));
Optional<OneBlockCustomBlock> customBlock = OneBlockCustomBlockCreator.create(map);
if (customBlock.isPresent()) {
obPhase.addCustomBlock(customBlock.get(), probability);
} else {
addon.logError("Bad custom block in " + obPhase.getPhaseName() + ": " + map);
}
}

private boolean addMaterial(OneBlockPhase obPhase, String material, String probability) {
int prob;
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package world.bentobox.aoneblock.oneblocks.customblock;

import java.util.Comparator;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.block.Block;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;

import world.bentobox.aoneblock.AOneBlock;
import world.bentobox.aoneblock.listeners.MakeSpace;
import world.bentobox.aoneblock.oneblocks.OneBlockCustomBlock;
import world.bentobox.bentobox.BentoBox;

/**
* A custom block that summons an entity using a Minecraft-format NBT data
* string (i.e. the same syntax as the vanilla {@code /summon} command).
* <p>
* Example YAML entry:
*
* <pre>
* - type: mob-data
* data: breeze{Glowing:1b,attributes:[{id:scale,base:2f}]}
* underlying-block: STONE
* probability: 15
* </pre>
*
* After summoning, the spawned entity's bounding box is measured one tick
* later and {@link MakeSpace} is invoked so that scaled/oversized hitboxes
* clear the surrounding blocks. See BentoBoxWorld/AOneBlock#488.
*/
public class MobDataCustomBlock implements OneBlockCustomBlock {

private final String data;
private final Material underlyingBlock;

public MobDataCustomBlock(@NonNull String data, @Nullable Material underlyingBlock) {
this.data = data;
this.underlyingBlock = underlyingBlock;
}

public static Optional<MobDataCustomBlock> fromMap(Map<?, ?> map) {
String data = Objects.toString(map.get("data"), null);
if (data == null) {
return Optional.empty();
}

String underlyingBlockValue = Objects.toString(map.get("underlying-block"), null);
Material underlyingBlock = underlyingBlockValue == null ? null : Material.getMaterial(underlyingBlockValue);
if (underlyingBlockValue != null && underlyingBlock == null) {
BentoBox.getInstance().logWarning("Underlying block " + underlyingBlockValue
+ " does not exist and will be replaced with STONE.");
}

return Optional.of(new MobDataCustomBlock(data, underlyingBlock));
}

@Override
public void execute(AOneBlock addon, Block block) {
try {
block.setType(Objects.requireNonNullElse(underlyingBlock, Material.STONE));

Location spawnLoc = block.getLocation().add(new Vector(0.5D, 1D, 0.5D));
String world = "minecraft:" + spawnLoc.getWorld().getName();
// Vanilla summon coordinates accept decimals.
String x = String.valueOf(spawnLoc.getX());
String y = String.valueOf(spawnLoc.getY());
String z = String.valueOf(spawnLoc.getZ());

String command = buildSummonCommand(data, world, x, y, z);
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command);

block.getWorld().playSound(block.getLocation(), Sound.ENTITY_ENDERMAN_TELEPORT, 1F, 2F);

// Defer MakeSpace by one tick so NBT-driven attributes (e.g.
// minecraft:scale) have applied before we measure the bounding box.
Bukkit.getScheduler().runTaskLater(addon.getPlugin(), () -> {
Entity spawned = findRecentlySpawned(spawnLoc);
if (spawned != null && addon.getSettings().isClearBlocks()) {
new MakeSpace(addon).makeSpace(spawned, spawnLoc);
}
}, 1L);
} catch (Exception e) {
BentoBox.getInstance().logError("Could not summon mob-data entity '" + data + "': " + e.getMessage()
+ " — check that the entity id, NBT keys, and attribute ids are valid for this Minecraft"
+ " version (1.21 renamed 'minecraft:generic.*' attributes to drop the 'generic.' prefix).");
}
}

/**
* Builds the {@code execute in <world> run summon <entity> <x> <y> <z> [nbt]}
* command for the given data string and coordinates.
* <p>
* Vanilla summon grammar is {@code summon <entity> <x> <y> <z> [nbt]} — the
* NBT must come AFTER the coordinates. Gluing it to the entity id (e.g.
* {@code summon breeze{...} x y z}) makes the command parser throw an
* "Unhandled exception" in {@code VanillaCommandWrapper}, so we split the
* data string at the first NBT/component delimiter (<code>{</code> or
* <code>[</code>) and interleave the coordinates.
* <p>
* Package-private so unit tests can verify the resulting command without
* needing a live server.
*/
static String buildSummonCommand(String data, String world, String x, String y, String z) {
int nbtStart = indexOfFirst(data, '{', '[');
String entityId = nbtStart < 0 ? data : data.substring(0, nbtStart);
String nbt = nbtStart < 0 ? "" : " " + data.substring(nbtStart);
return "execute in " + world + " run summon " + entityId + " " + x + " " + y + " " + z + nbt;
}

/**
* Returns the index of the first occurrence of any of the given characters
* in {@code s}, or -1 if none are present. Used to locate where the NBT
* portion of a {@code /summon}-style data string begins.
*/
private static int indexOfFirst(String s, char a, char b) {
int ai = s.indexOf(a);
int bi = s.indexOf(b);
if (ai < 0) return bi;
if (bi < 0) return ai;
return Math.min(ai, bi);
}

/**
* Finds the most-recently-spawned living entity near the given location.
* Skips players, then picks the entity with the lowest {@code getTicksLived()}
* (i.e. the newest).
*/
@Nullable
private Entity findRecentlySpawned(Location spawnLoc) {
return spawnLoc.getWorld().getNearbyEntities(spawnLoc, 1.5D, 2D, 1.5D).stream()
.filter(e -> !(e instanceof Player))
.filter(LivingEntity.class::isInstance)
.min(Comparator.comparingInt(Entity::getTicksLived))
.orElse(null);
}

public String getData() {
return data;
}

public Material getUnderlyingBlock() {
return underlyingBlock;
}
}
Loading
Loading