
GPExpansion
The ultimate add-on for GriefPrevention 3D Subdivisions
Список изменений
GPExpansion v1.1.16
Adds the feature asked for on Discord: a chat message when GriefPrevention hands a player their accrued claim blocks. GriefPrevention fires AccrueClaimBlocksEvent six times an hour and GPExpansion already listens to it to apply per-profile rates, world multipliers, and caps — the delivery amount was simply never surfaced to the player. It is now, behind accruals.notify-on-accrue.
The same build moves snapshot creation and legacy .nbt restore onto per-region scheduling so both work on Folia.
Read the Behaviour Changes section before updating. notify-on-accrue defaults to true, so every online player starts receiving a chat line every 10 minutes as soon as the server restarts.
Features
Players are notified when accrued claim blocks are delivered
AccrualListener.onAccrueClaimBlocks() now sends commands.accruals-received at the end of its handler, after the profile has been resolved and the world multiplier applied — so the number shown is the amount GPExpansion actually put on the event, not the raw server default.
commands:
accruals-received: "&aYou received {amount} bonus claim blocks from your {profile} profile."
{amount} is the per-delivery amount, not the hourly rate. GriefPrevention's event carries blocksToAccruePerHour / 6 internally, because delivery happens six times an hour, and setBlocksToAccruePerHour() divides on the way in. A profile configured at 120 blocks/hour reports 20 every 10 minutes. Two consequences of that integer division:
- A profile below 6 blocks/hour truncates to 0 per delivery. The message is guarded by
getBlocksToAccrue() > 0, so those players get silence rather than a stream of "you received 0 blocks". - Rates that do not divide evenly by 6 lose the remainder. That is GriefPrevention's arithmetic, unchanged here — the message just makes it visible for the first time. A profile set to 100/hour delivers 16 six times, i.e. 96/hour.
The message does not fire when:
- the player is idle — GriefPrevention constructs the event pre-cancelled for idle players and the handler is
ignoreCancelled = true; - accruals are disabled, the world is blacklisted, or the player is AFK/vanished/out of survival under the existing pause settings — those paths set the rate to 0 and return before the notification;
- the player lacks
griefprevention.accruals, in which case GriefPrevention never fires the event at all.
No new permission node. If you want the message for some ranks only, give those ranks an accrual profile and leave the others on a rate that resolves to 0.
Behaviour Changes
- The notification is on by default. Existing servers get it on the first restart after updating, at roughly one line per player per 10 minutes. Set
accruals.notify-on-accrue: falseto turn it off, or blankcommands.accruals-receivedinlang.yml. - The message is sent before GriefPrevention delivers the blocks, which is unavoidable from inside the event but has two visible edges:
- A player sitting at their accrual limit is still told they received blocks.
PlayerDataclamps the total withMath.min(newTotal, accruedLimit), so the blocks are discarded on arrival. The existingnotify-on-capmessage only fires when a profile's cap drops below what the player already has, not in this steady state. - Any plugin listening at
HIGHESTor later can still change the amount or cancel the event after GPExpansion has spoken. GPExpansion listens atHIGH; nothing in a default GriefPrevention + GPExpansion install sits behind it.
- A player sitting at their accrual limit is still told they received blocks.
Folia
Snapshot creation reads each chunk on its owning region thread
createSnapshotSnap() scanned the whole claim volume in one world.getBlockAt() loop from the calling thread. That is valid on Paper and Purpur, where one thread owns the world, and invalid on Folia, where each region owns its own chunks and cross-region access is rejected.
On Folia the scan is now split per chunk:
- the claim's block range is divided into chunk-aligned slices;
- each slice loads its chunk through
getChunkAtAsync(reflected, with a 3-arg then 4-arg lookup and a direct-schedule fallback) and then runs on that chunk's region thread viarunAtLocation; - an
AtomicIntegercounts completions, and the last slice to finish writes the.snapfile and logs[Snapshot] Async snapshot <id> saved (<n> blocks).
The non-Folia path is byte-for-byte the same loop as before.
Two things to know about the Folia path:
- It reports success on scheduling, not on completion.
createSnapshotSnap()returnstrueonce the per-chunk tasks are queued, so the index entry is written and/claim snapshotconfirms before the file exists on disk. Watch for the "Async snapshot saved" log line to know the write landed. - A failed chunk read is logged and skipped, not fatal.
Snapshot chunk read failed at <x>,<z>means the resulting.snapis missing that chunk's blocks while still being listed as a valid snapshot.
Block order within the file is now per-chunk-completion rather than x/y/z on Folia. Restore addresses every entry by its stored relative coordinates, so this makes no difference to what gets rebuilt.
Legacy .nbt restore is chunk-scheduled instead of synchronous
.snap restore already had a Folia path; the legacy .nbt path did not, and set blocks directly from one thread. It now collects the palette into a BlockToPlace list, groups it by chunk, and on Folia hands each group to loadChunkAndRestoreBlocks() from a global-scheduler task. The synchronous path was refactored to place from the same list — same blocks, same order, one extra list allocation.
The restoringClaims guard changed with it. It used to be cleared immediately after .nbt restore, on the grounds that the restore was synchronous; on Folia the restore no longer is, so clearing is deferred into the scheduling task. The flag still lifts when the per-chunk tasks have been submitted, not when the last block lands — a restore-in-progress check can come back clean while writes are still queued. That matches how the .snap Folia path already behaved.
.snap remains the format for new snapshots. .nbt is read-only migration support for snapshots taken by older versions.
Configuration
Two new keys. Neither needs a migration step, and version.config-version stays at 1.1.2.
config.yml
accruals:
notify-on-accrue: true
lang.yml
commands:
accruals-received: "&aYou received {amount} bonus claim blocks from your {profile} profile."
Why there is no migration code for these
Both files are additive-merged from in-code default maps on every startup, independent of config-version:
Config.DEFAULTSholdsaccruals.notify-on-accrue.addMissingDefaults()runs duringload(), writes any key the admin's file does notcontain(), saves, and logsAdded missing config option: accruals.notify-on-accrue = true.Messages.DEFAULTSholdscommands.accruals-received.addMissingKeys()injects it, andensureDefaultsInFile()additionally walks the jar's bundledlang.ymland copies across anything still missing — so a key added to the resource file alone is also picked up.
Existing values are never overwritten; only absent keys are added. This is why the version-gated migration blocks in VersionManager were not touched, and why future accrual keys will not need them either.
One caveat on timing: injection happens in Config.load() at startup, not in reload() — /gpx reload is deliberately read-only about keys. If you drop the new jar in and reload without restarting, the key will not appear in config.yml yet, but the feature is still active, because shouldNotifyOnAccrue() falls back to true when the path is absent. The key materialises on the next full restart.
Compatibility
AccrueClaimBlocksEvent is identical between GriefPrevention3D (18.2.7, the build target) and upstream GriefPrevention — same class, same getBlocksToAccrue() / setBlocksToAccruePerHour() contract, fired from the same DeliverClaimBlocksTask six times an hour. The notification needs no reflection, no version branch, and no fork-specific handling; it works against either jar as-is.
Notes
- No API, command, permission, or placeholder changes.
- Verified by a clean
mvn compileagainst GriefPrevention3D 18.2.7 and by tracing the event path throughDeliverClaimBlocksTaskandPlayerData.accrueBlocks(). The notification has not been observed on a live server; the Folia snapshot paths have not been run on a Folia server. - Still outstanding, carried over from earlier releases:
BanEnforcementListenerand/claim ban's ejection path fall back togetHighestBlockYAt(), so ejecting from a nether claim can still deposit a player on the roof.getSafeDestination()uses-1as its "no ground found" sentinel, which collides with a genuine ground block at y=-1 in 1.18+ worlds.
