Skip to content
Open
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
7ab75a9
fix: zero new island on large protection ranges via incremental capture
tastybento May 15, 2026
caff259
Initial plan
Copilot May 15, 2026
10d7c2f
Initial plan
Copilot May 15, 2026
5d88f5b
fix: recalculate donated points from donatedBlocks map using current …
Copilot May 16, 2026
e1fc2d5
Changes before error encountered
Copilot May 16, 2026
ee568ba
Merge pull request #438 from BentoBoxWorld/copilot/fix-donation-calcu…
tastybento May 16, 2026
663f429
refactor(listener): address Sonar findings in NewChunkListener
tastybento May 16, 2026
bec4ec1
Merge pull request #435 from BentoBoxWorld/fix/zero-island-large-prot…
tastybento May 16, 2026
821a79b
Merge remote-tracking branch 'origin/master' into copilot/fix-blockco…
Copilot May 16, 2026
58def10
Merge branch 'develop' into copilot/fix-blockconfig-limits
tastybento May 16, 2026
45f32b7
Merge pull request #439 from BentoBoxWorld/copilot/fix-blockconfig-li…
tastybento May 16, 2026
f11ccce
fix: enforce blockconfig donation limits in GUI panel and confirm pro…
tastybento May 16, 2026
c210d66
perf: parallelize chunk fetches and add levelstatus diagnostics
tastybento May 17, 2026
0a1e6ea
fix: dedup chunks and apply limits in NewChunkListener
tastybento May 17, 2026
57c8a2e
fix: stop fresh islands reading level 1 from missed handicap
tastybento May 17, 2026
0c75e7b
Merge pull request #440 from BentoBoxWorld/feat/lazy-zero-and-paralle…
tastybento May 17, 2026
89f86a1
Initial plan
Copilot May 20, 2026
4110dde
fix: cap donated block counts to current block limits during level ca…
Copilot May 20, 2026
0548a0a
Merge pull request #443 from BentoBoxWorld/copilot/fix-donate-blocks-…
tastybento May 20, 2026
22f20bb
fix: self-healing per-chunk handicap and address Copilot review
tastybento May 24, 2026
f254f04
fix: refresh initialCount in tidyUp to close zero-scan/regular-scan race
tastybento May 24, 2026
7715a12
debug: delay zero scan and log handicap drift for diagnosis
tastybento May 25, 2026
178d85a
fix: first /level after zero scan adopts live total as canonical base…
tastybento May 25, 2026
ce6e7a2
Revert "fix: first /level after zero scan adopts live total as canoni…
tastybento May 25, 2026
f562e96
Pin MockBukkit to Maven Central 4.110.0 instead of jitpack snapshot
tastybento May 30, 2026
a7f183d
Add MC 26.1.2 to Modrinth game-versions (#444)
tastybento Jun 3, 2026
4cbe568
Add files via upload
tastybento Jun 28, 2026
fe19d63
ci: add CurseForge publish workflow (publishes the release asset)
tastybento Jun 28, 2026
a6e5e1b
ci: pin reusable workflow to SHA and enable Hangar publish
tastybento Jun 28, 2026
a08946b
ci: bump pinned reusable workflow to 71bf927bce32586216baa6995f21852d…
tastybento Jun 28, 2026
bf4a75c
Merge pull request #445 from BentoBoxWorld/ci/curseforge-publish
tastybento Jun 28, 2026
c3a786f
ci: bump pinned reusable workflow to fe4b1f0
tastybento Jul 1, 2026
d04146a
Merge pull request #446 from BentoBoxWorld/ci/bump-publish-workflow-f…
tastybento Jul 1, 2026
4793a79
ci: bump pinned publish-platforms.yml to ca2dcd1
tastybento Jul 3, 2026
08298a4
Merge pull request #447 from BentoBoxWorld/ci/bump-publish-platforms-…
tastybento Jul 3, 2026
aeee20f
Remove experimental per-chunk zeroing engine from develop (#448)
tastybento Jul 16, 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
1 change: 1 addition & 0 deletions .github/workflows/modrinth-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ jobs:
1.21.11
26.1
26.1.1
26.1.2

# Path to the built JAR — Maven produces Level-{version}.jar in target/
files: target/Level-${{ github.event.release.tag_name || inputs.tag }}.jar
30 changes: 30 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Level — .github/workflows/publish.yml
# Publishes the jar attached to a GitHub release to CurseForge and Hangar via the
# shared BentoBoxWorld/.github reusable workflow. Downloads the release asset instead of
# rebuilding from source. The reusable workflow is pinned to a commit SHA (Sonar
# githubactions:S7637). workflow_dispatch lets you (re)publish a given version.

name: Publish release to CurseForge and Hangar

on:
release:
types: [published]
workflow_dispatch:
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
inputs:
version:
description: "Version to publish (e.g. 1.2.3)"
required: true
type: string

jobs:
publish:
uses: bentoboxworld/.github/.github/workflows/publish-platforms.yml@ca2dcd167e8db4e0f671a976080744dda43801a6 # master
with:
use_release_asset: "true" # publish the jar attached to the release; do not rebuild
hangar_slug: "Level" # blank = skip Hangar
curseforge_id: "1514824"
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:
HANGAR_API_KEY: ${{ secrets.HANGAR_API_KEY }}
CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }}
8 changes: 4 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
<!-- Non-minecraft related dependencies -->
<junit.version>5.10.2</junit.version>
<mockito.version>5.11.0</mockito.version>
<mock-bukkit.version>v1.21-SNAPSHOT</mock-bukkit.version>
<mock-bukkit.version>4.110.0</mock-bukkit.version>
<!-- More visible way how to change dependency versions -->
<paper.version>1.21.11-R0.1-SNAPSHOT</paper.version>
<bentobox.version>3.15.1-SNAPSHOT</bentobox.version>
Expand All @@ -69,7 +69,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>2.27.0</build.version>
<build.version>2.28.0</build.version>
<sonar.projectKey>BentoBoxWorld_Level</sonar.projectKey>
<sonar.organization>bentobox-world</sonar.organization>
<sonar.host.url>https://sonarcloud.io</sonar.host.url>
Expand Down Expand Up @@ -182,8 +182,8 @@
<dependencies>
<!-- Mock Bukkit - used to mock the server in tests -->
<dependency>
<groupId>com.github.MockBukkit</groupId>
<artifactId>MockBukkit</artifactId>
<groupId>org.mockbukkit.mockbukkit</groupId>
<artifactId>mockbukkit-v1.21</artifactId>
<version>${mock-bukkit.version}</version>
<scope>test</scope>
</dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ private List<String> getReport() {
donatedBlocks.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.forEach(entry -> {
Integer value = addon.getBlockConfig().getBlockValues().getOrDefault(entry.getKey().toLowerCase(java.util.Locale.ENGLISH), 0);
Integer value = Objects.requireNonNullElse(addon.getBlockConfig().getValue(island.getWorld(), entry.getKey().toLowerCase(java.util.Locale.ENGLISH)), 0);
long totalValue = (long) value * entry.getValue();
reportLines.add(" " + Util.prettifyText(entry.getKey()) + " x "
+ String.format("%,d", entry.getValue())
Expand Down Expand Up @@ -730,8 +730,24 @@ public void tidyUp() {
results.rawBlockCount
.addAndGet((long) (results.underWaterBlockCount.get() * addon.getSettings().getUnderWaterMultiplier()));

// Add donated block points (permanent contributions that persist across recalculations)
long donatedPoints = addon.getManager().getDonatedPoints(island);
// Add donated block points (permanent contributions that persist across recalculations).
// Recalculate from the donated blocks map using current block config values so the
// level always reflects the current configuration, even if block values changed since donation.
// Also apply the current block limit: if the limit was lowered after donation, only count
// up to the current limit (donated blocks over the limit are silently ignored).
Map<String, Integer> donatedBlocksMap = addon.getManager().getDonatedBlocks(island);
long donatedPoints = donatedBlocksMap.entrySet().stream()
.mapToLong(entry -> {
String key = entry.getKey().toLowerCase(java.util.Locale.ENGLISH);
Integer value = addon.getBlockConfig().getValue(island.getWorld(), key);
int count = entry.getValue();
Integer limit = addon.getBlockConfig().getLimit(key);
if (limit != null) {
count = Math.min(count, limit);
}
return (long) Objects.requireNonNullElse(value, 0) * count;
})
.sum();
results.rawBlockCount.addAndGet(donatedPoints);
results.donatedPoints.set(donatedPoints);

Expand Down
104 changes: 95 additions & 9 deletions src/main/java/world/bentobox/level/commands/IslandDonateCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -124,15 +124,41 @@ private boolean handleHandDonation(User user, Island island, List<String> args)
}
}

final int previewAmount = Math.min(requested, hand.getAmount());
final long previewPoints = (long) previewAmount * blockValue;
int previewAmount = Math.min(requested, hand.getAmount());
final int finalRequested = requested;

// Apply blockconfig limit to the preview so the confirm prompt shows the
// amount that will actually be destroyed, not the raw request.
Object displayKey = customId != null ? customId : material;
String donationId = customId != null ? customId : material.name();
Object blockObj = customId != null ? (Object) customId : material;
Integer limit = addon.getBlockConfig().getLimit(blockObj);
boolean limited = false;
if (limit != null) {
int already = addon.getManager().getDonatedBlocks(island).getOrDefault(donationId, 0);
int remaining = Math.max(0, limit - already);
if (remaining == 0) {
user.sendMessage("island.donate.limit-reached",
MATERIAL_PLACEHOLDER, Utils.prettifyObject(displayKey, user));
return false;
}
if (previewAmount > remaining) {
previewAmount = remaining;
limited = true;
}
}

long previewPoints = (long) previewAmount * blockValue;
String prompt = user.getTranslation("island.donate.hand.confirm-prompt",
TextVariables.NUMBER, String.valueOf(previewAmount),
MATERIAL_PLACEHOLDER, Utils.prettifyObject(displayKey, user),
POINTS_PLACEHOLDER, Utils.formatNumber(user, previewPoints));
if (limited) {
// The limit-notice locale uses '|' as a lore line-break for the GUI;
// translate to '\n' here so the chat confirmation prompt wraps cleanly.
prompt = prompt + "\n"
+ user.getTranslation("island.donate.limit-notice").replace('|', '\n');
}

askConfirmation(user, prompt, () -> performHandDonation(user, island, material, customId, blockValue, finalRequested));
return true;
Expand All @@ -151,6 +177,23 @@ private void performHandDonation(User user, Island island, Material material, St
return;
}
int amount = Math.min(requested, currentHand.getAmount());

// Apply blockconfig donation limit
String donationId = customId != null ? customId : material.name();
Object blockObj = customId != null ? (Object) customId : material;
Object displayKey = customId != null ? customId : material;
Integer limit = addon.getBlockConfig().getLimit(blockObj);
if (limit != null) {
int currentDonated = addon.getManager().getDonatedBlocks(island).getOrDefault(donationId, 0);
int remaining = Math.max(0, limit - currentDonated);
if (remaining == 0) {
user.sendMessage("island.donate.limit-reached",
MATERIAL_PLACEHOLDER, Utils.prettifyObject(displayKey, user));
return;
}
amount = Math.min(amount, remaining);
}

long points = (long) amount * blockValue;

if (amount >= currentHand.getAmount()) {
Expand All @@ -159,11 +202,9 @@ private void performHandDonation(User user, Island island, Material material, St
currentHand.setAmount(currentHand.getAmount() - amount);
}

String donationId = customId != null ? customId : material.name();
addon.getManager().donateBlocks(island, user.getUniqueId(), donationId, amount, points);
addon.getManager().recalculateAfterDonation(island);

Object displayKey = customId != null ? customId : material;
user.sendMessage("island.donate.hand.success",
TextVariables.NUMBER, String.valueOf(amount),
MATERIAL_PLACEHOLDER, Utils.prettifyObject(displayKey, user),
Expand All @@ -184,7 +225,9 @@ private boolean handleInvDonation(User user, Island island) {
return false;
}

Map<String, Integer> alreadyDonated = addon.getManager().getDonatedBlocks(island);
long totalPoints = 0L;
boolean limited = false;
StringBuilder prompt = new StringBuilder(
user.getTranslation("island.donate.inv.confirm-header"));
for (Map.Entry<String, Integer> e : totals.entrySet()) {
Expand All @@ -197,13 +240,35 @@ private boolean handleInvDonation(User user, Island island) {
Object displayKey = mat != null ? mat : e.getKey();
Integer rawValue = addon.getBlockConfig().getValue(getWorld(), displayKey);
if (rawValue == null) continue;
long points = (long) rawValue * e.getValue();

String donationId = mat != null ? mat.name() : e.getKey();
Object blockObj = mat != null ? mat : e.getKey();
int amount = e.getValue();
Integer limit = addon.getBlockConfig().getLimit(blockObj);
if (limit != null) {
int remaining = Math.max(0, limit - alreadyDonated.getOrDefault(donationId, 0));
int accepted = Math.min(amount, remaining);
if (accepted < amount) {
limited = true;
}
amount = accepted;
}
if (amount <= 0) {
continue;
}
long points = (long) rawValue * amount;
totalPoints += points;
prompt.append('\n').append(user.getTranslation("island.donate.inv.confirm-line",
TextVariables.NUMBER, String.valueOf(e.getValue()),
TextVariables.NUMBER, String.valueOf(amount),
MATERIAL_PLACEHOLDER, Utils.prettifyObject(displayKey, user),
POINTS_PLACEHOLDER, Utils.formatNumber(user, points)));
}
if (limited) {
// The limit-notice locale uses '|' as a lore line-break for the GUI;
// translate to '\n' here so the chat confirmation prompt wraps cleanly.
prompt.append('\n').append(
user.getTranslation("island.donate.limit-notice").replace('|', '\n'));
}
prompt.append('\n').append(user.getTranslation("island.donate.inv.confirm-total",
POINTS_PLACEHOLDER, Utils.formatNumber(user, totalPoints)));

Expand All @@ -223,14 +288,35 @@ private void performInvDonation(User user, Island island) {
if (value == null) {
continue;
}
int amount = item.getAmount();
long points = (long) value * amount;
String customId = addon.getCustomBlockId(item);
String donationId = customId != null ? customId : item.getType().name();
Object blockObj = customId != null ? (Object) customId : item.getType();

// Apply blockconfig donation limit: only take up to the remaining capacity
int amount = item.getAmount();
Integer limit = addon.getBlockConfig().getLimit(blockObj);
if (limit != null) {
// getDonatedBlocks returns the live cached map, already updated by earlier donateBlocks calls
int alreadyDonated = addon.getManager().getDonatedBlocks(island).getOrDefault(donationId, 0);
int remaining = Math.max(0, limit - alreadyDonated);
if (remaining == 0) {
// Limit already reached for this material — leave item in inventory
continue;
}
amount = Math.min(amount, remaining);
}

// Remove accepted amount from inventory slot
if (amount >= item.getAmount()) {
contents[i] = null;
} else {
item.setAmount(item.getAmount() - amount);
}

long points = (long) value * amount;
donated.merge(donationId, amount, Integer::sum);
totalPoints += points;
addon.getManager().donateBlocks(island, user.getUniqueId(), donationId, amount, points);
contents[i] = null;
}
pInv.setStorageContents(contents);

Expand Down
Loading
Loading