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
12 changes: 8 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@ jobs:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Set up JDK 21
uses: actions/setup-java@v3
# BentoBox 3.18.0+ is compiled for Java 25 (Minecraft 26.x), so its class files
# cannot be read by a JDK 21 javac at all - the build fails with
# "class file has wrong version 69.0, should be 65.0" before reaching our code.
# The addon itself still targets 21 via <release> in the pom.
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
distribution: 'adopt'
java-version: 21
distribution: 'temurin'
java-version: 25
- name: Cache SonarCloud packages
uses: actions/cache@v3
with:
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/modrinth-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4

# 2. Set up Java 21 (required by ChunkBlock' build)
- name: Set up Java 21
# 2. Set up Java 25 - required to read BentoBox 3.18.0+ class files, which are
# compiled for Java 25. The addon itself still targets 21 via <release> in the pom.
- name: Set up Java 25
uses: actions/setup-java@v4
with:
java-version: '21'
java-version: '25'
distribution: 'temurin'

# 3. Cache Maven dependencies to speed up builds
Expand Down
9 changes: 7 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,13 @@ jobs:
uses: bentoboxworld/.github/.github/workflows/publish-platforms.yml@1f91a0edf72e8c86d671b3b8fdd3121ac6fb88e1 # master
with:
use_release_asset: "true" # publish the jar attached to the release; do not rebuild
hangar_slug: "ChunkBlock" # blank = skip Hangar
curseforge_id: "1512493"
# TODO: ChunkBlock is not set up on Hangar or CurseForge yet. When the projects
# exist, set hangar_slug to the Hangar slug and curseforge_id to ChunkBlock's own
# CurseForge project id (1512493 was AOneBlock's — never reuse it), add the
# HANGAR_API_KEY / CURSEFORGE_TOKEN secrets, and re-enable this workflow
# (gh workflow enable publish.yml).
hangar_slug: "" # blank = skip Hangar
curseforge_id: "" # blank = skip CurseForge
game_versions: "26.2,26.1.2,26.1.1,26.1,1.21.11,1.21.10,1.21.9,1.21.8,1.21.7,1.21.6,1.21.5"
version: ${{ inputs.version }} # empty on release events -> falls back to the release tag
secrets:
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>4.110.0</mock-bukkit.version>
<!-- More visible way how to change dependency versions -->
<bentobox.version>3.15.0-SNAPSHOT</bentobox.version>
<bentobox.version>3.22.0</bentobox.version>
<items-adder.version>4.0.10</items-adder.version>
<nexo.version>1.8.0</nexo.version>
<craftengine.version>0.0.67</craftengine.version>
Expand All @@ -67,7 +67,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.0.1</build.version>
<build.version>1.1.0</build.version>
<!-- SonarCloud -->
<sonar.projectKey>BentoBoxWorld_ChunkBlock</sonar.projectKey>
<sonar.organization>bentobox-world</sonar.organization>
Expand Down
16 changes: 14 additions & 2 deletions src/main/java/world/bentobox/chunkblock/ChunkBlock.java
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,15 @@ public class ChunkBlock extends GameModeAddon {
.type(Type.PROTECTION)
.defaultRank(RanksManager.COOP_RANK)
.build();
/**
* Flag to set who can spend the island's level credit on new chunks. Defaults to the
* owner alone, because a claim is irreversible until the levels are earned back.
*/
public final Flag CHUNKBLOCK_CLAIM_CHUNKS = new Flag.Builder("CHUNKBLOCK_CLAIM_CHUNKS", Material.OAK_FENCE_GATE)
.mode(Mode.BASIC)
.type(Type.PROTECTION)
.defaultRank(RanksManager.OWNER_RANK)
.build();

@Override
public void onLoad() {
Expand Down Expand Up @@ -177,6 +186,8 @@ public void onLoad() {
}
// Magic Block protection
getPlugin().getFlagsManager().registerFlag(this, this.MAGIC_BLOCK);
// Who may spend level credit on chunks
getPlugin().getFlagsManager().registerFlag(this, this.CHUNKBLOCK_CLAIM_CHUNKS);
}
}

Expand Down Expand Up @@ -277,9 +288,10 @@ public boolean loadData() {

@Override
public void onDisable() {
// save cache
// Save cache. This must be a direct write, not a queued one: the server disables this
// Pladdon before BentoBox, so anything queued here depends on BentoBox draining it later.
if (blockListener != null) {
blockListener.saveCache();
blockListener.saveCacheNow();
}

// Stop border rendering and restore client-side blocks
Expand Down
66 changes: 66 additions & 0 deletions src/main/java/world/bentobox/chunkblock/Settings.java
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,18 @@ public class Settings implements WorldSettings {
@ConfigEntry(path = "chunkblock.max-chunks")
private int maxChunks = 441;

@ConfigComment("Require confirmation before level credit is spent on a chunk. When true, the")
@ConfigComment("first hit on the border previews the target chunk and its cost; the player")
@ConfigComment("must then sneak and hit the border again to actually claim it. This stops a")
@ConfigComment("stray swing near the border from spending levels by accident.")
@ConfigEntry(path = "chunkblock.claim.require-confirmation")
private boolean requireClaimConfirmation = true;

@ConfigComment("How long, in seconds, a previewed chunk stays confirmable. After this the")
@ConfigComment("player has to hit the border again to preview it afresh. Minimum 1.")
@ConfigEntry(path = "chunkblock.claim.confirmation-timeout")
private int claimConfirmationTimeout = 15;

@ConfigComment("If true, losing island levels below what has been spent re-locks chunks in")
@ConfigComment("reverse claim order (the most recently claimed chunks are lost first). Builds")
@ConfigComment("inside re-locked chunks are untouched but cannot be reached until the levels")
Expand Down Expand Up @@ -452,6 +464,13 @@ public class Settings implements WorldSettings {
@ConfigEntry(path = "island.water-mob-protection")
private boolean waterMobProtection = true;

@ConfigComment("How often island progress is written to the database, in blocks broken")
@ConfigComment("Progress is also saved whenever a phase changes, a player logs out and the server shuts down,")
@ConfigComment("so this only decides how much is lost if the server dies without shutting down cleanly.")
@ConfigComment("Lower is safer but writes more often. Minimum is 1 (save every block)")
@ConfigEntry(path = "island.save-every")
private int saveEvery = 10;

@ConfigComment("Default max team size")
@ConfigComment("Permission size cannot be less than the default below. ")
@ConfigEntry(path = "island.max-team-size")
Expand Down Expand Up @@ -1933,6 +1952,25 @@ public void setMobWarning(int mobWarning) {
this.mobWarning = mobWarning;
}

/**
* How many blocks are broken between periodic saves of island progress.
* A value below 1 would make the modulo check throw, so it is clamped.
* @return the saveEvery value, never less than 1
*/
public int getSaveEvery() {
if (saveEvery < 1) {
saveEvery = 1;
}
return saveEvery;
}

/**
* @param saveEvery the saveEvery to set
*/
public void setSaveEvery(int saveEvery) {
this.saveEvery = saveEvery;
}

/**
* @return the waterMobProtection
*/
Expand Down Expand Up @@ -2569,6 +2607,34 @@ public void setMaxChunks(int maxChunks) {
this.maxChunks = maxChunks;
}

/**
* @return true if a chunk must be previewed and confirmed before credit is spent
*/
public boolean isRequireClaimConfirmation() {
return requireClaimConfirmation;
}

/**
* @param requireClaimConfirmation the requireClaimConfirmation to set
*/
public void setRequireClaimConfirmation(boolean requireClaimConfirmation) {
this.requireClaimConfirmation = requireClaimConfirmation;
}

/**
* @return how long a previewed chunk stays confirmable in seconds, never less than 1
*/
public int getClaimConfirmationTimeout() {
return Math.max(1, claimConfirmationTimeout);
}

/**
* @param claimConfirmationTimeout the claimConfirmationTimeout to set
*/
public void setClaimConfirmationTimeout(int claimConfirmationTimeout) {
this.claimConfirmationTimeout = claimConfirmationTimeout;
}

/**
* @return true if chunks re-lock when island level drops
*/
Expand Down
69 changes: 69 additions & 0 deletions src/main/java/world/bentobox/chunkblock/chunks/BorderDisplay.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,25 @@ public class BorderDisplay implements Listener {
private static final int OUT_OF_WORLD_DEPTH = 16;
/** Dust color when the curtain is beyond the world height limits */
private static final Color OUT_OF_WORLD_COLOR = Color.ORANGE;
/** Dust color for a chunk a player has lined up to claim but not yet confirmed */
private static final Color PREVIEW_COLOR = Color.YELLOW;
/** Heights above the viewer's feet at which the preview outline is drawn */
private static final int[] PREVIEW_HEIGHTS = { 0, 3, 6 };

private final ChunkBlock addon;
/** Client-side barrier blocks sent per player, with the original data for restore */
private final Map<UUID, Set<BarrierBlock>> barrierBlocks = new HashMap<>();
/** Chunks outlined for a player pending claim confirmation */
private final Map<UUID, Preview> previews = new HashMap<>();
private BukkitTask task;

private record BarrierBlock(Location location, BlockData oldData) {
}

/** A pending claim outline: world chunk coordinates and when to stop drawing it */
private record Preview(int chunkX, int chunkZ, long expiry) {
}

public BorderDisplay(ChunkBlock addon) {
this.addon = addon;
}
Expand All @@ -80,13 +90,15 @@ public void stop() {
barrierBlocks.keySet().stream().map(Bukkit::getPlayer).filter(java.util.Objects::nonNull)
.toList().forEach(this::hideBorder);
barrierBlocks.clear();
previews.clear();
}

private void redrawAll() {
for (World world : Bukkit.getWorlds()) {
if (addon.inWorld(world)) {
for (Player player : world.getPlayers()) {
showBorder(player);
drawPreview(player);
}
}
}
Expand Down Expand Up @@ -227,6 +239,61 @@ public void hideBorder(Player player) {
}
}

/**
* Outlines a chunk in yellow for one player until the given time, marking it as lined
* up for claiming but not yet paid for. Only that player sees it, and the outline
* disappears by itself when the confirmation window closes.
*
* @param player the player about to spend credit
* @param chunkX world chunk x coordinate of the previewed chunk
* @param chunkZ world chunk z coordinate of the previewed chunk
* @param expiry when to stop drawing, in {@link System#currentTimeMillis()} terms
*/
public void showPreview(Player player, int chunkX, int chunkZ, long expiry) {
previews.put(player.getUniqueId(), new Preview(chunkX, chunkZ, expiry));
drawPreview(player);
}

/**
* Stops outlining whatever chunk this player had lined up.
*
* @param uuid the player's UUID
*/
public void clearPreview(UUID uuid) {
previews.remove(uuid);
}

/**
* Draws the pending claim outline for one player, if they have one that has not run out
* of time. The box is drawn around the player's own height so it reads as a wall even
* when the terrain beyond the border is far above or below them.
*/
private void drawPreview(Player player) {
Preview preview = previews.get(player.getUniqueId());
if (preview == null) {
return;
}
if (System.currentTimeMillis() > preview.expiry()) {
previews.remove(player.getUniqueId());
return;
}
// The outline traces the chunk's boundary planes, so the corners meet exactly where
// the four walls of the locked-chunk curtain would
double minX = preview.chunkX() << 4;
double minZ = preview.chunkZ() << 4;
double baseY = player.getLocation().getBlockY() + 0.5D;
Particle.DustOptions dust = new Particle.DustOptions(PREVIEW_COLOR, 1.5F);
for (int height : PREVIEW_HEIGHTS) {
double y = baseY + height;
for (int i = 0; i <= 16; i += 2) {
player.spawnParticle(Particle.DUST, minX + i, y, minZ, 1, 0, 0, 0, 0, dust);
player.spawnParticle(Particle.DUST, minX + i, y, minZ + 16, 1, 0, 0, 0, 0, dust);
player.spawnParticle(Particle.DUST, minX, y, minZ + i, 1, 0, 0, 0, 0, dust);
player.spawnParticle(Particle.DUST, minX + 16, y, minZ + i, 1, 0, 0, 0, 0, dust);
}
}
}

/**
* One-shot green particle celebration along freshly unlocked chunks, visible to
* everyone in the world near the island.
Expand Down Expand Up @@ -264,6 +331,7 @@ public void celebrate(Island island, List<Vector> gained) {
@EventHandler(priority = EventPriority.MONITOR)
public void onQuit(PlayerQuitEvent e) {
barrierBlocks.remove(e.getPlayer().getUniqueId());
previews.remove(e.getPlayer().getUniqueId());
}

@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
Expand All @@ -279,5 +347,6 @@ public void onRespawn(PlayerRespawnEvent e) {
@EventHandler(priority = EventPriority.MONITOR)
public void onChangedWorld(PlayerChangedWorldEvent e) {
barrierBlocks.remove(e.getPlayer().getUniqueId());
previews.remove(e.getPlayer().getUniqueId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,6 @@ private record BrushSession(BukkitTask task, Block block) {}
*/
public static final int MAX_LOOK_AHEAD = 5;

/**
* How often island data is saved to the database (in blocks broken).
*/
public static final int SAVE_EVERY = 50;

/*
* Loot tables for suspicious blocks
*/
Expand Down Expand Up @@ -161,11 +156,25 @@ public BlockListener(@NonNull ChunkBlock addon) {

/**
* Saves all island data from the cache to the database asynchronously.
* <p>
* Only safe while the server is running. On shutdown use {@link #saveCacheNow()}.
*/
public void saveCache() {
cache.values().forEach(handler::saveObjectAsync);
}

/**
* Saves all island data from the cache to the database on the calling thread.
* <p>
* Used on shutdown, where an asynchronous save cannot be retried if it does not complete.
* BentoBox drains writes queued by addons as they are disabled, but this addon is a Pladdon,
* so the server disables it before BentoBox and that drain is the only thing standing between
* a queued block count and a rolled-back island. Writing directly removes the dependency.
*/
public void saveCacheNow() {
cache.values().forEach(handler::saveObjectNow);
}

// ---------------------------------------------------------------------
// Section: Listeners
// ---------------------------------------------------------------------
Expand Down Expand Up @@ -448,7 +457,7 @@ private ProcessPhaseResult processPhase(Cancellable e, Island i, OneBlockIslands
return new ProcessPhaseResult(phase, true, 0);
}
handleNewPhase(player, i, is, phase, block, prevPhaseName);
} else if (is.getBlockNumber() % SAVE_EVERY == 0) {
} else if (is.getBlockNumber() % addon.getSettings().getSaveEvery() == 0) {
// Periodically save the island's progress.
saveIsland(i);
}
Expand Down
Loading
Loading