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
91 changes: 90 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

Border is a BentoBox addon for Minecraft Paper servers that creates and renders per-player world borders around islands. Players cannot pass the border. It supports two rendering modes: barrier blocks with particles, and vanilla world borders.

**Target:** Paper 1.21+ with BentoBox 3.10+, Java 21
**Target:** Paper 1.21+ with BentoBox 3.12+, Java 21

## Build Commands

Expand Down Expand Up @@ -65,3 +65,92 @@ Uses JUnit 5 + Mockito 5 + MockBukkit. All test classes extend `CommonTestSetup`
- Game modes can be excluded via `Settings.getDisabledGameModes()`
- Localization files live in `src/main/resources/locales/` (20+ languages, YAML format)
- `BorderType` is an enum with byte-based serialization ids (`fromId`/`getId`)

## Dependency Source Lookup

When you need to inspect source code for a dependency (e.g., BentoBox, addons):

1. **Check local Maven repo first**: `~/.m2/repository/` — sources jars are named `*-sources.jar`
2. **Check the workspace**: Look for sibling directories or Git submodules that may contain the dependency as a local project (e.g., `../bentoBox`, `../addon-*`)
3. **Check Maven local cache for already-extracted sources** before downloading anything
4. Only download a jar or fetch from the internet if the above steps yield nothing useful

Prefer reading `.java` source files directly from a local Git clone over decompiling or extracting a jar.

In general, the latest version of BentoBox should be targeted.

## Project Layout

Related projects are checked out as siblings under `~/git/`:

**Core:**
- `bentobox/` — core BentoBox framework

**Game modes:**
- `addon-acidisland/` — AcidIsland game mode
- `addon-bskyblock/` — BSkyBlock game mode
- `Boxed/` — Boxed game mode (expandable box area)
- `CaveBlock/` — CaveBlock game mode
- `OneBlock/` — AOneBlock game mode
- `SkyGrid/` — SkyGrid game mode
- `RaftMode/` — Raft survival game mode
- `StrangerRealms/` — StrangerRealms game mode
- `Brix/` — plot game mode
- `parkour/` — Parkour game mode
- `poseidon/` — Poseidon game mode
- `gg/` — gg game mode

**Addons:**
- `addon-level/` — island level calculation
- `addon-challenges/` — challenges system
- `addon-welcomewarpsigns/` — warp signs
- `addon-limits/` — block/entity limits
- `addon-invSwitcher/` / `invSwitcher/` — inventory switcher
- `addon-biomes/` / `Biomes/` — biomes management
- `Bank/` — island bank
- `Border/` — world border for islands
- `Chat/` — island chat
- `CheckMeOut/` — island submission/voting
- `ControlPanel/` — game mode control panel
- `Converter/` — ASkyBlock to BSkyBlock converter
- `DimensionalTrees/` — dimension-specific trees
- `discordwebhook/` — Discord integration
- `Downloads/` — BentoBox downloads site
- `DragonFights/` — per-island ender dragon fights
- `ExtraMobs/` — additional mob spawning rules
- `FarmersDance/` — twerking crop growth
- `GravityFlux/` — gravity addon
- `Greenhouses-addon/` — greenhouse biomes
- `IslandFly/` — island flight permission
- `IslandRankup/` — island rankup system
- `Likes/` — island likes/dislikes
- `Limits/` — block/entity limits
- `lost-sheep/` — lost sheep adventure
- `MagicCobblestoneGenerator/` — custom cobblestone generator
- `PortalStart/` — portal-based island start
- `pp/` — pp addon
- `Regionerator/` — region management
- `Residence/` — residence addon
- `TopBlock/` — top ten for OneBlock
- `TwerkingForTrees/` — twerking tree growth
- `Upgrades/` — island upgrades (Vault)
- `Visit/` — island visiting
- `weblink/` — web link addon
- `CrowdBound/` — CrowdBound addon

**Data packs:**
- `BoxedDataPack/` — advancement datapack for Boxed

**Documentation & tools:**
- `docs/` — main documentation site
- `docs-chinese/` — Chinese documentation
- `docs-french/` — French documentation
- `BentoBoxWorld.github.io/` — GitHub Pages site
- `website/` — website
- `translation-tool/` — translation tool

Check these for source before any network fetch.

## Key Dependencies (source locations)

- `world.bentobox:bentobox` → `~/git/bentobox/src/`
6 changes: 3 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@
<mockito.version>5.11.0</mockito.version>
<!-- More visible way how to change dependency versions -->
<paper.version>1.21.11-R0.1-SNAPSHOT</paper.version>
<bentobox.version>3.10.0</bentobox.version>
<bentobox.version>3.12.0</bentobox.version>
<!-- Revision variable removes warning about dynamic version -->
<revision>${build.version}-SNAPSHOT</revision>
<!-- This allows to change between versions and snapshots. -->
<build.version>4.8.2</build.version>
<build.version>4.8.3</build.version>
<build.number>-LOCAL</build.number>
<!-- Sonar Cloud -->
<sonar.projectKey>BentoBoxWorld_Border</sonar.projectKey>
Expand Down Expand Up @@ -114,7 +114,7 @@
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
<snapshots>
<enabled>false</enabled>
<enabled>true</enabled>
Comment thread
tastybento marked this conversation as resolved.
</snapshots>
</repository>
<repository>
Expand Down
91 changes: 43 additions & 48 deletions src/main/java/world/bentobox/border/listeners/PlayerListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
import org.bukkit.util.RayTraceResult;
import org.bukkit.util.Vector;

import world.bentobox.bentobox.BentoBox;
import world.bentobox.bentobox.api.addons.Addon;
import world.bentobox.bentobox.api.events.island.IslandProtectionRangeChangeEvent;
import world.bentobox.bentobox.api.flags.Flag;
Expand Down Expand Up @@ -308,47 +307,46 @@ public void onPlayerLeaveIsland(PlayerMoveEvent e) {
return;
}
// Backtrack - try to find island at current location, or fall back to the player's own island
Optional<Island> optionalIsland = addon.getIslands().getIslandAt(p.getLocation());
Optional<Island> optionalIsland = addon.getIslands().getIslandAt(Objects.requireNonNull(p.getLocation()));
if (optionalIsland.isEmpty()) {
optionalIsland = Optional
.ofNullable(addon.getIslands().getIsland(p.getWorld(), User.getInstance(p)));
}
optionalIsland.ifPresent(i -> {
Vector unitVector = i.getProtectionCenter().toVector().subtract(p.getLocation().toVector()).normalize()
.multiply(new Vector(1, 0, 1));
if (unitVector.lengthSquared() <= 0D) {
// Direction is zero, so nothing to do; cannot move.
return;
}
RayTraceResult r = i.getProtectionBoundingBox().rayTrace(p.getLocation().toVector(), unitVector, i.getRange());
if (r == null || !checkFinite(r.getHitPosition())) {
// Ray trace failed, so just teleport the player back to that island
inTeleport.add(p.getUniqueId());
Util.teleportAsync(p, i.getHome("")).thenRun(() -> inTeleport.remove(p.getUniqueId()));
return;
}
optionalIsland.ifPresent(i -> backtrackToIsland(p, i));
}

private void backtrackToIsland(Player p, Island i) {
Vector unitVector = i.getProtectionCenter().toVector().subtract(p.getLocation().toVector()).normalize()
.multiply(new Vector(1, 0, 1));
if (unitVector.lengthSquared() <= 0D) {
// Direction is zero, so nothing to do; cannot move.
return;
}
RayTraceResult r = i.getProtectionBoundingBox().rayTrace(p.getLocation().toVector(), unitVector, i.getRange());
if (r == null || !checkFinite(r.getHitPosition())) {
// Ray trace failed, so just teleport the player back to that island
inTeleport.add(p.getUniqueId());
Location targetPos = r.getHitPosition().toLocation(p.getWorld(), p.getLocation().getYaw(), p.getLocation().getPitch());

if (!addon.getIslands().isSafeLocation(targetPos)) {
switch (targetPos.getWorld().getEnvironment()) {
case NETHER:
targetPos.getBlock().getRelative(BlockFace.DOWN).setType(Material.NETHERRACK);
break;
case THE_END:
targetPos.getBlock().getRelative(BlockFace.DOWN).setType(Material.END_STONE);
break;
default:
targetPos.getBlock().getRelative(BlockFace.DOWN).setType(Material.STONE);
break;
}
Util.teleportAsync(p, targetPos).thenRun(() -> inTeleport.remove(p.getUniqueId()));
} else {
Util.teleportAsync(p, i.getHome("")).thenRun(() -> inTeleport.remove(p.getUniqueId()));
}
Util.teleportAsync(p, i.getHome("")).thenRun(() -> inTeleport.remove(p.getUniqueId()));
return;
}

});
inTeleport.add(p.getUniqueId());
Location targetPos = r.getHitPosition().toLocation(p.getWorld(), p.getLocation().getYaw(), p.getLocation().getPitch());

if (addon.getIslands().isSafeLocation(targetPos)) {
Util.teleportAsync(p, i.getHome("")).thenRun(() -> inTeleport.remove(p.getUniqueId()));
return;
}
targetPos.getBlock().getRelative(BlockFace.DOWN).setType(safeLandingMaterial(targetPos));
Util.teleportAsync(p, targetPos).thenRun(() -> inTeleport.remove(p.getUniqueId()));
}

private Material safeLandingMaterial(Location targetPos) {
return switch (targetPos.getWorld().getEnvironment()) {
case NETHER -> Material.NETHERRACK;
case THE_END -> Material.END_STONE;
default -> Material.STONE;
};
}

/**
Expand Down Expand Up @@ -406,13 +404,10 @@ public void onEntityMount(EntityMountEvent event) {
if (!addon.inGameWorld(loc.getWorld())) {
return;
}
// Eject from mount if outside the protection range
if (addon.getIslands().getProtectedIslandAt(loc).isEmpty()) {
// Force the dismount event for custom entities
if (!event.getMount().eject()) {
var dismountEvent = new EntityDismountEvent(player, event.getMount());
Bukkit.getPluginManager().callEvent(dismountEvent);
}
// Eject from mount if outside the protection range; force dismount for custom entities
if (addon.getIslands().getProtectedIslandAt(loc).isEmpty() && !event.getMount().eject()) {
var dismountEvent = new EntityDismountEvent(player, event.getMount());
Bukkit.getPluginManager().callEvent(dismountEvent);
}
}, 1, 20));
}
Expand Down Expand Up @@ -452,7 +447,7 @@ public void onPlayerMove(PlayerMoveEvent e) {
// Remove head movement
if (isOn(player) && !e.getFrom().toVector().equals(e.getTo().toVector())) {
addon.getIslands()
.getIslandAt(e.getPlayer().getLocation())
.getIslandAt(Objects.requireNonNull(e.getPlayer().getLocation()))
.ifPresent(i -> show.refreshView(User.getInstance(e.getPlayer()), i));
}
}
Expand Down Expand Up @@ -499,8 +494,8 @@ public void onItemDrop(PlayerDropItemEvent event) {
&& addon.inGameWorld(event.getPlayer().getWorld())
&& isOn(event.getPlayer())
) {
// Get this island
addon.getIslands().getIslandAt(event.getPlayer().getLocation()).ifPresent(is -> trackItem(event.getItemDrop(), is));
Location loc = event.getPlayer().getLocation();
addon.getIslands().getIslandAt(Objects.requireNonNull(loc)).ifPresent(is -> trackItem(event.getItemDrop(), is));
}
}

Expand All @@ -514,9 +509,9 @@ public void onPlayerDeath(PlayerDeathEvent event) {
if (addon.getSettings().isBounceBack()
&& addon.inGameWorld(event.getPlayer().getWorld())
&& isOn(event.getPlayer())) {
// Get this island
addon.getIslands().getIslandAt(event.getPlayer().getLocation()).ifPresent(is -> {
event.getDrops().forEach(item -> trackItem(event.getPlayer().getWorld().dropItemNaturally(event.getPlayer().getLocation(), item), is));
Location loc = event.getPlayer().getLocation();
addon.getIslands().getIslandAt(Objects.requireNonNull(loc)).ifPresent(is -> {
event.getDrops().forEach(item -> trackItem(event.getPlayer().getWorld().dropItemNaturally(loc, item), is));
event.getDrops().clear(); // We handled them
});
}
Expand Down
77 changes: 36 additions & 41 deletions src/main/java/world/bentobox/border/listeners/ShowBarrier.java
Original file line number Diff line number Diff line change
Expand Up @@ -86,42 +86,33 @@ public void showBorder(Player player, Island island) {

private void showWalls(Player player, Location loc, int xMin, int xMax, int zMin, int zMax, boolean max) {
if (loc.getBlockX() - xMin < BARRIER_RADIUS) {

// Close to min x
for (int z = Math.max(loc.getBlockZ() - BARRIER_RADIUS, zMin); z < loc.getBlockZ() + BARRIER_RADIUS && z < zMax; z++) {
for (int y = -BARRIER_RADIUS; y < BARRIER_RADIUS; y++) {
showPlayer(player, xMin-1, loc.getBlockY() + y, z, max);
}
}
showWallAlongZ(player, loc, xMin - 1, zMin, zMax, max);
}
if (xMax - loc.getBlockX() < BARRIER_RADIUS) {
showWallAlongZ(player, loc, xMax, zMin, zMax, max); // not xMax+1, that's outside the region
}
if (loc.getBlockZ() - zMin < BARRIER_RADIUS) {

// Close to min z
for (int x = Math.max(loc.getBlockX() - BARRIER_RADIUS, xMin); x < loc.getBlockX() + BARRIER_RADIUS && x < xMax; x++) {
for (int y = -BARRIER_RADIUS; y < BARRIER_RADIUS; y++) {
showPlayer(player, x, loc.getBlockY() + y, zMin-1, max);
}
}
showWallAlongX(player, loc, xMin, xMax, zMin - 1, max);
}
if (xMax - loc.getBlockX() < BARRIER_RADIUS) {
if (zMax - loc.getBlockZ() < BARRIER_RADIUS) {
showWallAlongX(player, loc, xMin, xMax, zMax, max); // not zMax+1, that's outside the region
}
}

// Close to max x
for (int z = Math.max(loc.getBlockZ() - BARRIER_RADIUS, zMin); z < loc.getBlockZ() + BARRIER_RADIUS && z < zMax; z++) {
for (int y = -BARRIER_RADIUS; y < BARRIER_RADIUS; y++) {
showPlayer(player, xMax, loc.getBlockY() + y, z, max); // not xMax+1, that's outside the region
}
private void showWallAlongZ(Player player, Location loc, int x, int zMin, int zMax, boolean max) {
for (int z = Math.max(loc.getBlockZ() - BARRIER_RADIUS, zMin); z < loc.getBlockZ() + BARRIER_RADIUS && z < zMax; z++) {
for (int y = -BARRIER_RADIUS; y < BARRIER_RADIUS; y++) {
showPlayer(player, x, loc.getBlockY() + y, z, max);
}
}
if (zMax - loc.getBlockZ() < BARRIER_RADIUS) {
}

// Close to max z
for (int x = Math.max(loc.getBlockX() - BARRIER_RADIUS, xMin); x < loc.getBlockX() + BARRIER_RADIUS && x < xMax; x++) {
for (int y = -BARRIER_RADIUS; y < BARRIER_RADIUS; y++) {
showPlayer(player, x, loc.getBlockY() + y, zMax, max); // not zMax+1, that's outside the region
}
private void showWallAlongX(Player player, Location loc, int xMin, int xMax, int z, boolean max) {
for (int x = Math.max(loc.getBlockX() - BARRIER_RADIUS, xMin); x < loc.getBlockX() + BARRIER_RADIUS && x < xMax; x++) {
for (int y = -BARRIER_RADIUS; y < BARRIER_RADIUS; y++) {
showPlayer(player, x, loc.getBlockY() + y, z, max);
}
}

}

/**
Expand All @@ -138,21 +129,25 @@ private void showPlayer(Player player, int i, int j, int k, boolean max) {
&& player.getLocation().getBlockZ() == k) {
teleportEntity(player);
}

Location l = new Location(player.getWorld(), i, j, k);
Util.getChunkAtAsync(l).thenAccept(c -> {
if (addon.getSettings().isShowParticles()) {
if (j < player.getWorld().getMinHeight() || j > player.getWorld().getMaxHeight()) {
User.getInstance(player).spawnParticle(max ? MAX_PARTICLE : PARTICLE, PARTICLE_DUST_RED, i + 0.5D, j + 0.0D, k + 0.5D);
} else {
User.getInstance(player).spawnParticle(max ? MAX_PARTICLE : PARTICLE, PARTICLE_DUST_BLUE, i + 0.5D, j + 0.0D, k + 0.5D);
}
}
if (addon.getSettings().isUseBarrierBlocks() && (l.getBlock().isEmpty() || l.getBlock().isLiquid())) {
player.sendBlockChange(l, Material.BARRIER.createBlockData());
barrierBlocks.computeIfAbsent(player.getUniqueId(), u -> new HashSet<>()).add(new BarrierBlock(l, l.getBlock().getBlockData()));
}
});
Util.getChunkAtAsync(l).thenAccept(c -> renderBorderBlock(player, l, i, j, k, max));
}

private void renderBorderBlock(Player player, Location l, int i, int j, int k, boolean max) {
if (addon.getSettings().isShowParticles()) {
spawnBorderParticle(player, i, j, k, max);
}
if (addon.getSettings().isUseBarrierBlocks() && (l.getBlock().isEmpty() || l.getBlock().isLiquid())) {
player.sendBlockChange(l, Material.BARRIER.createBlockData());
barrierBlocks.computeIfAbsent(player.getUniqueId(), u -> new HashSet<>()).add(new BarrierBlock(l, l.getBlock().getBlockData()));
}
}

private void spawnBorderParticle(Player player, int i, int j, int k, boolean max) {
boolean outOfWorld = j < player.getWorld().getMinHeight() || j > player.getWorld().getMaxHeight();
Particle.DustOptions dust = outOfWorld ? PARTICLE_DUST_RED : PARTICLE_DUST_BLUE;
User.getInstance(player).spawnParticle(max ? MAX_PARTICLE : PARTICLE, dust, i + 0.5D, j + 0.0D, k + 0.5D);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/addon.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Border
main: world.bentobox.border.Border
version: ${version}${build.number}
api-version: 1.17
api-version: 3.12.0
icon: BEDROCK

prefix: Border
Expand Down
12 changes: 8 additions & 4 deletions src/main/resources/locales/cs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
border:
toggle:
description: přepíná hranice ostrova zapnuto/vypnuto
border-on: "&a Hranice zapnuta."
border-off: "&a Hranice vypnuta."
border-on: '<green>Hranice zapnuta.</green>'
border-off: '<green>Hranice vypnuta.</green>'
set-type:
description: změní typ ohraničení
changed: "&a Typ ohraničení změněn na &b[type]&a."
error-unavailable-type: "&c Tento typ je nedostupný nebo neexistuje."
changed: '<green>Typ ohraničení změněn na <aqua>[type]</aqua>.</green>'
error-unavailable-type: '<red>Tento typ je nedostupný nebo neexistuje.</red>'
set-color:
description: mění barvu ohraničení
changed: '<green>Barva ohraničení změněna na <aqua>[color]</aqua>.</green>'
error-invalid-color: '<red>Tato barva je neplatná. Platné barvy: red, green, blue.</red>'
Loading
Loading