Перейти к содержимому
Mineforgian

GPExpansion

The ultimate add-on for GriefPrevention 3D Subdivisions

Загрузки
640
Подписчики
4
Обновлён
14 августа 2026 г.
Лицензия
GPL-3.0-only

Опубликован 2 января 2026 г.

Extend GriefPrevention with rental signs, mailboxes, and more

GPExpansion adds powerful features to GriefPrevention including rental signs, claim mailboxes, global claims, rental snapshots, claim GUIs, sign protection, and more — all while maintaining the self-service philosophy.

Haven't heard of GriefPrevention 3D Subdivisions? Get it here to enable mailbox support. This is a fork that replaces the GriefPrevention jar.

Supported Platforms

Spigot, Paper, Purpur, and Folia

Requires GriefPrevention and optionally Vault for economy features.

Optionally, replace GriefPrevention with GriefPrevention3D for seamless mailboxes (1x1x1 subdivisions and public container trust). Mailboxes will work with regular GP and there is a config setting called mailbox-protocol: virtual that helps if you want to be able to stack mailboxes on top of each other.

Features

Rental Signs

Allow claim owners to rent out their claims to other players using signs.

  • Set rental prices and durations
  • Automatic trust/untrust on rental start/expiry
  • Supports Vault economy, experience, claim blocks, and item-based payments
  • Interactive setup wizard with /rentclaim command
  • Rental Snapshots – Save and restore claim state between renters
Setting up a rental sign manually

Sell Signs

Allow claim owners to sell their claims to other players using signs.

  • Set claim prices
  • Automatic transfer of ownership of claim
  • Supports Vault economy, experience, claim blocks, and item-based payments
  • Interactive setup wizard with /sellclaim command
Setting up a sell sign manually

Claim Mailboxes

Give each claim a mailbox where other players can deposit items.

  • Owners have full access to retrieve items
  • Non-owners see a virtual inventory (snapshot at open time)
    • Changes save only when the menu closes
    • Can take back items before closing (reversible deposits)
    • Items returned if mailbox fills up while viewing
  • Chest opening sound/animation on sign click
  • Storage warnings when mailbox is nearly full
  • Purchasable via signs with configurable prices
  • Interactive setup wizard with /mailbox command
Creating a self mailbox
Selling a mailbox to other players
Sharing a mailbox with other players

Self Mailboxes (Instant Creation)

  • Owners and renters can place [Mailbox] wall signs directly on containers in claims they own or rent
  • Configurable limit via defaults.max-self-mailboxes-per-claim
Protocol Type Subdivision Created Non-Owner View Live Updates Stackable
Real Protocol 1x1x1 (GP3D) or 1x1 2D (regular GP) Real chest with public container trust Yes No
Virtual Protocol None Virtual snapshot inventory No (snapshot at open time) Yes

Global Claims

Allow claims to be listed in a global claim list, viewable to all.

  • Set global settings like icon, description, name, spawn point, and more
  • Allows users to teleport to global claims via GUI by default
  • Allows [global] claim signs to instantly set spawn and list as global
  • Simple /claim global <true|false> command to manage global listing status

Sign Protection

Protect your rental and mailbox signs from unauthorized modification.

  • Admin-only sign breaking for active rentals
  • Automatic cleanup on sign removal

Rental Restoration Snapshots

Admins with griefprevention.restoresnapshot can save and restore claim block state for rentals. When a rental or eviction ends, the claim can be restored to a saved snapshot so the next renter sees the original build.

  • Create – Save the current claim blocks as a snapshot (e.g., before listing for rent)
  • List – View snapshots for a claim or for all claims
  • Restore – On eviction or rental end, the latest snapshot is applied automatically

Commands

  • /claim snapshot create [claimId] – Create a snapshot
  • /claim snapshot list [claimId|all] – List snapshots
  • /claim snapshot remove <snapshotId> – Delete a snapshot

Claim Management & GUIs

Interactive claim management with fully configurable GUIs:

  • /claim name <name> - Set claim name (supports color codes with permissions)
  • /claim desc <description> - Set claim description (uses same color permissions as name)
  • /claim icon <material> - Set claim icon for GUI display
  • /claim spawn - Set the teleport spawn point for your claim
  • /claim tp [claimId] - Teleport to a claim's spawn point
  • /claim flags [claimId] - Open the claim flags GUI (GPFlags)
  • /claim options [claimId] - Open the claim options GUI
  • /claim ban <player> - Ban players from your claims
  • /claim unban <player> - Unban players from your claims
  • /claim info [claimId] - View detailed claim information
  • /mailbox - Manage your mailboxes
  • /gpx reload - Reload configuration and language files
  • /gpx max - Manage player sign creation limits

Claim GUI Features

  • Visual claim map editor – Resize, subdivide, and manage claims via interactive GUI
  • Customizable layouts, icons, and permissions
  • Teleport to claims, view claim info, and manage settings from one interface
  • Integration with GPFlags for claim flag management

Configurable Tax System

Optional tax system for claim maintenance:

  • Configurable tax rates per claim block
  • Multiple payment methods (money, claim blocks, experience)
  • Grace periods before claim deletion
  • Tax exemptions via permissions

CrowBar Locator Bar Integration

When CrowBar, a Fabric client mod, is installed alongside GPExpansion, claim waypoints display the claim name and a bowtie marker on the locator bar to the claim owner and trusted players. CrowBar reads the same locator data GPExpansion sends and renders readable name tags above each waypoint.

GPX-CrowBar

  • Works automatically when both GPExpansion and CrowBar are installed — no extra configuration needed.
  • Claim names appear above the bowtie waypoint marker on the locator bar.
  • Compatible with Minecraft 1.21.6–1.21.11, 26.1.x, and 26.2.

Download CrowBar

Documentation

Sign formats, configuration, permissions, and detailed guides All here

Support

Building from Source

git clone https://github.com/castledking/GPExpansion.git
cd GPExpansion
mvn clean install

The compiled jar will be in target/.

Built to extend GriefPrevention with love ❤️

Ченджлог

1.2.2Релиз26.1.1, 26.1.2, 26.2 · 14 августа 2026 г.

GPExpansion v1.2.2

This point release closes three ways a banned player could still act inside a claim they were banned from. The reported one is the most serious: riding any mount across the border bypassed the ban completely, and dismounting left the animal behind inside the claim.

A ban also did not reliably outrank trust, and enforcement only ever considered where the player was standing, never what they were reaching for. Because banning does not remove trust, GriefPrevention still authorized a trusted-but-banned player to interact through the boundary.

Read the Behaviour Changes section before updating. A ban is now enforced against the target of an action rather than only the player's position, so a banned player can no longer break, place, interact with, mount, or attack anything inside the claim from outside it. Public bans are unchanged in that trusted players still pass them.

No configuration keys or permission nodes were added. Two language keys were added, both with bundled defaults. The Maven project version is now 1.2.2; version.config-version remains unchanged.

Mounted Entry

Riding across the border no longer bypasses the ban

A banned player who rode a horse, pig, strider, camel, boat, or minecart into a claim received the claim.ban-blocked-enter chat message and was then free to roam. Nothing else happened.

The move handler was firing correctly. Its two enforcement tools simply do not work on a passenger:

  • cancelling PlayerMoveEvent returns the player to the previous position, but the vehicle owns that position and keeps walking in;
  • Player#setVelocity is ignored while the player is a passenger, so the boundary knockback did nothing.

The deep-ejection path could not help either, because a passenger cannot be teleported away from its vehicle. That is why the player was only ejected once they dismounted, and why the mount stayed inside the claim.

A mounted boundary crossing now detaches the rider and returns both the rider and the mount to the position the move came from. The knockback path is unchanged for players on foot.

Vehicle movement is checked independently

VehicleMoveEvent is now handled as a second net for rides that move a passenger without a vetoable PlayerMoveEvent. The event is not cancellable, so the mount is put back by hand.

The handler walks the full passenger stack, so a player riding a horse that is itself in a boat is still found. It uses the same block-coordinate change filter and the same flat ban-box index as the move handler, so vehicles that are nowhere near a claim with bans cost one axis-aligned box test.

If the ride began inside the claim, there is no safe previous position to roll back to and the existing safe-ejection search runs instead.

Mounts leave with their rider

Ejecting a rider on its own left the animal parked inside the claim, which is the grief the ban is meant to prevent. Three paths now move the mount as well:

  • a rejected boundary crossing returns the mount to the position outside that the ride came from;
  • ejection from inside the claim dismounts the player first, then teleports the player and the mount to the same computed exit point;
  • a banned rider who dismounts voluntarily inside the claim has their mount relocated to the nearest point outside the claim footprint.

A mount that still carries another player is left alone. Only the banned rider is removed from it, and someone else's boat continues on its way.

The dismount relocation is deferred by one entity tick. EntityDismountEvent fires before the player is actually off, and teleporting a mount that still has a passenger would drag the player along with it. Dismounts that GPExpansion performs itself are flagged and skipped, so the ejection paths are not processed twice.

Mounting across the boundary is denied

Player reach is longer than a claim border is thick. A banned player could stand outside and right-click an animal standing inside to ride it in, which produced no chat message at all because their own position was never in the claim.

EntityMountEvent is now checked against the mount's location. Mounting an entity inside a claim the player is banned from is cancelled and reported with the new claim.ban-blocked-mount message.

Bans and Trust

An explicit ban now outranks trust

The ban check evaluated the public ban and the player's trust before it considered the player's own ban:

if (!pubBanned && !playerBanned) return null;
if (pubBanned && isTrusted(main, player)) return null;

A player who was individually banned from a claim that was also public-banned therefore came back as not banned whenever they held trust. The individual ban was discarded.

A ban naming the player now short-circuits the check before trust is consulted. A public ban still yields to trust, which remains the documented way to public-ban a claim while keeping specific players allowed in.

Bans still do not remove trust

Banning a player deliberately leaves their trust entries untouched, so an unban restores exactly what they had before. GPExpansion enforces the ban above trust instead of destroying trust data that could not be restored.

The consequence is that GriefPrevention, which knows nothing about GPExpansion bans, will still authorize a trusted-but-banned player for anything GPExpansion does not intercept. That is what the next section covers.

Cross-Border Enforcement

Enforcement previously asked only whether the player was standing inside a claim they were banned from. A banned player standing just outside the border could still reach in.

Every one of these is now checked against the location of the thing being acted on, regardless of where the player stands:

Event Target checked
PlayerInteractEntityEvent The clicked entity
EntityMountEvent The mount
EntityDamageByEntityEvent The victim, for melee and for projectiles resolved back to their shooter
BlockBreakEvent The broken block
BlockPlaceEvent The placed block
PlayerInteractEvent The clicked block, in addition to the existing player-position check

PlayerInteractAtEntityEvent shares its handler list with PlayerInteractEntityEvent and is covered by the same handler.

Denials are reported with the new claim.ban-blocked-interact message on the existing ten-second ban-message cooldown. The claim boundary visualization is now throttled with the message rather than shown per event, because a held left click produces one event per tick.

The existing bypass permissions are unaffected. claim-customization.bans.admin-bypass-permission and claim-customization.bans.bypass-permission are still checked first and exempt their holders from all of the above.

Performance

Every new handler runs behind the flat ban-box index introduced for move enforcement. A location that is not inside the X/Z footprint of any claim with bans is rejected with an axis-aligned box test before any GriefPrevention lookup, config read, or claim resolution happens. Servers with no claim bans configured pay one empty-list check per event.

Behaviour Changes

  • A banned player riding any mount is dismounted at the border and returned to the position they came from, together with the mount.
  • Mounts are moved out of the claim with the banned rider, including when the rider dismounts inside the claim voluntarily.
  • A mount carrying other players is not relocated; only the banned rider is removed from it.
  • A player named in a claim's ban list is now banned even when they hold trust and the claim is also public-banned. Previously that combination let them through.
  • A banned player can no longer mount, interact with entities, damage entities, shoot entities, break blocks, place blocks, or use blocks inside the claim while standing outside it.
  • A banned player who holds trust is affected by all of the above. Bans still do not delete trust entries.
  • The ban boundary visualization is now rate limited with the ban message instead of shown on every blocked event.

Configuration and Language

  • No new configuration keys. All new enforcement is governed by the existing claim-customization.bans.prevent-entry setting and is skipped entirely when it is false.
  • Two new language keys, both with bundled defaults so existing lang.yml files need no edits:
claim:
  ban-blocked-mount: "&cYou cannot ride entities in a claim you are banned from!"
  ban-blocked-interact: "&cYou cannot touch anything in a claim you are banned from!"
  • New internal helper: SchedulerFacade#teleportEntity(Entity, Location), the existing player teleport generalized to any entity so mounts follow the same Paper and Folia paths.
  • Maven artifact version: 1.2.2.

Compatibility

Built against GriefPrevention3D 18.2.7 and Paper 26.2. EntityMountEvent and EntityDismountEvent are used from org.bukkit.event.entity, where Paper has exposed them since 1.20.6.

VehicleMoveEvent covers everything Bukkit models as a Vehicle: horses, donkeys, mules, camels, llamas, pigs, striders, boats, and minecarts. A mob made rideable by another plugin that does not implement Vehicle is still covered by the player move handler.

No storage migration is required. Existing ban, rental, sale, mailbox, and claim data, configuration, and language customizations retain their formats.

Verification

  • mvn clean package completed successfully and produced target/GPExpansion.jar for version 1.2.2.
  • The existing suite of 10 unit tests passes.
  • Live-server verification is still needed for: riding each vehicle type across a border while banned, a stacked passenger arrangement, a shared boat with one banned passenger, dismounting inside the claim, mounting an animal from outside the border, and each cross-border action as a player who is both trusted and banned.
1.2.1Релиз26.1.1, 26.1.2, 26.2 · 14 августа 2026 г.

GPExpansion v1.2.1

This point release lets holders of GriefPrevention's existing griefprevention.adminclaims permission create GPExpansion-managed signs inside admin claims, restores automatic mailbox-protocol selection on fresh installations, reports malformed sign input in chat without replacing the player's sign text, and prevents generic BlueMap sign-marker plugins from claiming GPExpansion signs.

Admin claims deliberately have no owner UUID. GPExpansion's sign setup paths were still treating Claim#getOwnerID() as the only ownership answer, so an authorized admin-claim manager could build in the area but rent, sell, global, and mailbox setup would reject the claim as unowned. v1.2.1 treats griefprevention.adminclaims as the ownership equivalent only when the target is an admin claim or one of its subdivisions.

The Maven project version is now 1.2.1. One compatibility configuration key was added. No language keys, storage migrations, commands, or permission nodes were added.

Sign Validation Feedback

Malformed rent and sell sign input no longer rewrites the physical sign as [Invalid Rental] or [Invalid Listing]. GPExpansion now leaves every line as the player entered it and reports the validation failure through the existing localized chat messages.

An empty [sell] sign now receives a useful chat explanation that a payment amount is required, instead of displaying an invalid-listing marker on the sign. This also avoids modifying signs that reuse a GPExpansion header for another plugin, including bracket-based BlueMap marker plugins.

Valid GPExpansion sign creation and formatting are unchanged.

BlueMap sign-marker compatibility

Generic sign-marker plugins can treat every bracketed first line as a BlueMap icon. Headers such as [sell], [rent], and [mailbox] could therefore be consumed as map markers before GPExpansion handled them.

The new signs.compatibility.prevent-bluemap-markers option defaults to true. GPExpansion temporarily masks an enabled, configured GPExpansion input header while normal-priority marker listeners run, then restores the exact original component before GPExpansion parses and formats the sign.

The filter is deliberately narrow. It applies only to the configured input headers for enabled GPExpansion rent, sell, mailbox, and global signs. Other marker headers such as [bank], [star], and [shop] remain visible to BlueMap marker plugins.

When protection is enabled, a shared header such as [sell] belongs to GPExpansion and cannot also be used as a BlueMap marker header. Servers that intentionally assign the same header to another plugin can restore the old shared-header behavior:

signs:
  compatibility:
    prevent-bluemap-markers: false

Mailbox Protocol Detection

Fresh GP3D installations default to real again

GPExpansion historically selected the mailbox protocol on first startup:

  • GriefPrevention3D: signs.mailbox.protocol: real;
  • upstream GriefPrevention: signs.mailbox.protocol: virtual.

The 1.1.2 configuration restructure moved the old top-level mailbox-protocol setting to signs.mailbox.protocol. The bundled configuration and the generic Java defaults both supplied virtual at the new path. VersionManager's GP/GP3D detection only ran when the path was missing, so the bundled value made fresh GP3D installations look like an explicit user choice and short-circuited detection.

GPExpansion now records whether config.yml was created during the current startup. On that first load, it replaces the bundled placeholder with the detected server default even though the nested path already exists. A fresh GP3D server persists real; a fresh upstream GP server persists virtual.

The mailbox protocol is no longer injected by the generic static-default pass. If the nested setting is genuinely missing from an existing configuration, VersionManager can again migrate the old mailbox-protocol value or select a default from the installed GriefPrevention implementation.

After VersionManager writes a detected value or migration, GPExpansion reloads its dedicated configuration wrapper immediately. The selected protocol therefore governs mailbox creation during the same first server session instead of taking effect only after a restart.

Existing choices are not overwritten

The first-install override applies only when that startup created config.yml. Once the file already exists, either explicit value remains authoritative:

signs:
  mailbox:
    protocol: real

or:

signs:
  mailbox:
    protocol: virtual

This means GPExpansion does not silently change a server intentionally using the virtual protocol on GP3D, and it does not flip an established configuration merely because the GriefPrevention implementation changes later.

Upgrade note: if an affected GP3D installation has already completed its first startup and saved virtual, GPExpansion cannot distinguish that generated value from an administrator's intentional choice. Change signs.mailbox.protocol to real once. New installations created by v1.2.1 select and save real automatically.

Admin-Claim Sign Creation

Supported sign types

The admin-claim exception applies to the ownership checks used by:

  • rent signs;
  • sell signs;
  • global claim signs;
  • purchasable mailbox signs;
  • self-mailbox signs.

Both manual sign entry and the setup-wizard paths recognize the exception. This includes /rentclaim, /sellclaim, /mailbox, explicit claim-ID validation, location-based claim selection, and the wizard's sign auto-paste authorization.

/rentclaim and /sellclaim tab completion now also offers top-level admin claim IDs to players with griefprevention.adminclaims.

Permission scope remains narrow

griefprevention.adminclaims replaces only the missing owner check for an admin-claim context. It does not grant control over another player's ordinary claim.

The player must still satisfy the existing requirements for the requested sign type, including its creation and economy permissions. Sign limits, enabled/disabled sign settings, mailbox container attachment, nested-subdivision rules, safe global spawn checks, global claim limits and approval, and .anywhere behavior are unchanged.

The existing broad griefprevention.admin bypass also remains unchanged where a sign workflow already supported it.

Admin subdivisions are detected through their parent chain

GriefPrevention forks do not all expose administrative status at the same level. Some report it directly on an ownerless subdivision, while others expose it only on the top-level claim.

GPExpansion now walks the claim's parent chain when determining whether a sign target belongs to an admin claim. The authorization therefore works inside nested admin-claim subdivisions without treating similarly shaped subdivisions in ordinary player claims as administrative.

Real mailboxes preserve admin-claim ownership

Under the real mailbox protocol, placing an instant mailbox creates a 1x1 child claim around its container. That child previously received the sign creator's UUID because ordinary mailboxes are created by the parent claim owner.

When the parent belongs to an admin-claim hierarchy, the generated mailbox child now inherits the immediate parent's owner UUID. For a normal ownerless admin claim, that value remains null; creating the mailbox no longer turns part of the admin claim into the administrator's personal claim.

The mailbox record and sign creator metadata still identify the player who created the mailbox. Existing mailbox purchase and ownership-transfer behavior is unchanged.

Behaviour Changes

  • A newly created GP3D configuration persists signs.mailbox.protocol: real; a newly created upstream GP configuration persists virtual.
  • Existing explicit real and virtual values are preserved.
  • A detected or migrated protocol is active immediately on the same startup.
  • A player with griefprevention.adminclaims passes GPExpansion's sign ownership gate inside an admin claim or any descendant subdivision.
  • The same permission does not bypass ownership in ordinary player claims.
  • Type-specific sign permissions and every non-ownership validation still apply.
  • Manual creation and wizard-assisted creation now use the same admin-claim decision.
  • Rent and sell tab completion includes manageable top-level admin claims.
  • Real-protocol mailbox child claims remain ownerless when created directly beneath an ownerless admin claim.
  • Malformed rent and sell signs retain their original text and report the error in chat.
  • An empty [sell] sign explains that a payment amount is required.
  • Enabled GPExpansion input headers bypass normal-priority bracket-based BlueMap marker handlers by default.
  • Unrelated BlueMap marker headers are not filtered.

Compatibility

Built against GriefPrevention3D 18.2.7 and Paper 26.2. Parent-chain inspection uses GPExpansion's existing reflective GriefPrevention bridge and is compatible with both top-level-only and subdivision-level admin status reporting.

No data conversion is required. Existing signs, claims, mailboxes, rentals, permission assignments, configuration, and language customizations retain their formats. Existing [Invalid Rental] and [Invalid Listing] signs are not migrated; players can edit or replace them normally.

Configuration and Permissions

  • New configuration key: signs.compatibility.prevent-bluemap-markers (default: true).
  • The existing signs.mailbox.protocol first-install default is dynamic again: real for GP3D and virtual for upstream GP.
  • No new language keys.
  • No new commands or aliases.
  • No new permission nodes.
  • Existing permission used: griefprevention.adminclaims.
  • Maven artifact version: 1.2.1.

Verification

  • mvn clean package completed successfully and produced target/GPExpansion.jar for version 1.2.1.
  • Maven compiled all 105 main source files successfully.
  • Ten regression tests pass: five cover fresh and migrated mailbox-protocol selection, one prevents physical invalid-sign rewrites, and four cover managed-header masking, unrelated marker preservation, listener ordering, and configuration defaults.
  • Diff whitespace validation completed successfully.
  • Live-server verification is still needed for a complete first boot with GP3D and upstream GP, every sign type in a top-level admin claim and nested admin subdivision, denial in an ordinary claim the administrator does not own, setup-wizard auto-paste, real-mailbox child ownership before and after purchase, protected GPExpansion headers with Easy-BMapMarkers installed, and an unrelated BlueMap marker header while protection is enabled.
1.2.0Релиз26.1.1, 26.1.2, 26.2 · 14 августа 2026 г.

GPExpansion v1.2.0

This release lets the people responsible for a managed sign relocate it without destroying and rebuilding its GPExpansion state. Claim owners, active renters, sellers, buyers, global-sign creators, and unshared self-mailbox creators receive type-specific access, while staff can be granted separate permissions for moving somebody else's sign.

Subdivision handling is also more consistent. Active tenants can name the subdivision they rent, unnamed subdivisions inherit their nearest named parent's display name in PlaceholderAPI, and administrators can start evictions for rented subdivisions inside ownerless admin claims. Claim bans now eject an affected online player promptly instead of waiting for that player to move or interact.

Read the Behaviour Changes and Permissions sections before updating. The five controller sign-move permissions default to true, but every .other permission defaults to false and must be granted explicitly when staff should move signs they do not control.

No configuration keys or storage migrations were added. The Maven project version is now 1.2.0; version.config-version remains unchanged.

Managed Sign Moving

New two-step move workflow

Managed signs can now be moved with either command:

/claim movesign
/moveclaimsign

The workflow is deliberately interactive:

  1. Run either command.
  2. Right-click the existing GPExpansion sign.
  3. Place another sign at the desired destination.
  4. GPExpansion copies the managed sign onto the newly placed block and clears the old sign.

The placed destination sign is consumed normally from the player's inventory. Its material and orientation come from the sign the player placed; GPExpansion does not physically move or refund the original sign block. The old block remains in place with its front and back text cleared and its GPExpansion metadata removed.

The copied state includes:

  • all four lines on both sign sides;
  • text dye colour and glowing-text state on both sides;
  • the waxed state;
  • all persistent metadata owned by GPExpansion, including price, rental, item, mailbox, and claim associations.

Metadata owned by other plugins is not copied. Normal block-placement protection still applies to the destination, so the move workflow does not bypass GriefPrevention or another plugin cancelling BlockPlaceEvent.

Use /moveclaimsign cancel or /claim movesign cancel to stop an active move. A move also expires after two minutes and is discarded when the player disconnects.

Supported sign types and controllers

The source must be one of GPExpansion's managed rent, sell, mailbox, global, or self-mailbox signs. GPExpansion first determines whether the player controls that particular sign, then checks the corresponding permission.

Sign type A controller is... Controller permission Move-another permission
Rent The current claim owner or the active tenant griefprevention.sign.move.rent griefprevention.sign.move.rent.other
Sell The current claim owner: the seller before purchase or buyer after transfer griefprevention.sign.move.sell griefprevention.sign.move.sell.other
Mailbox The mailbox creator/seller, recorded owner/buyer, mailbox-claim owner, or parent-claim owner griefprevention.sign.move.mailbox griefprevention.sign.move.mailbox.other
Global The player who created the global sign griefprevention.sign.move.global griefprevention.sign.move.global.other
Self mailbox The recorded creator/owner, provided the mailbox has no shared users griefprevention.sign.move.self-mailbox griefprevention.sign.move.self-mailbox.other

Controller permissions default to true. The five .other permissions default to false.

Authorization is checked when the source is selected and again when the destination is processed. In particular, a renter must still have an active rental at completion time. An expired or former tenant does not keep control merely because they selected the sign earlier.

A shared self mailbox intentionally has no individual controller for moving purposes. Moving one requires the explicit griefprevention.sign.move.self-mailbox.other permission.

Stored locations remain synchronized

Completing a move updates GPExpansion's stored location where that sign type has one:

  • rent signs update the active rental's sign location;
  • mailbox and self-mailbox signs update the mailbox sign location;
  • a global claim spawn moves only when that spawn was the old sign block;
  • sell signs retain their claim and sale data directly on the destination sign and have no separate stored sign location to update.

Source clearing is scheduled at the source location and destination completion at the destination location. This keeps cross-region moves compatible with Folia's region scheduler.

Creator identity for new signs

New rent, sell, mailbox, self-mailbox, and global signs now store the creator UUID in GPExpansion-owned persistent data. This retains the seller or creator identity needed by the move authorization rules even when a mailbox or claim later changes hands.

Existing rent, sell, and mailbox signs continue to derive control from their active rental, claim, parent claim, and mailbox records. Legacy global signs predate creator metadata; on their first authorized move, GPExpansion treats the current claim owner as the safely inferable creator and adds the missing metadata.

Commands

/claim info aliases /claiminfo

/claim info [claimId] now runs the existing /claiminfo [claimId] implementation. It has the same output and permission checks:

  • griefprevention.claiminfo is required to use it;
  • griefprevention.claiminfo.other is still required to inspect a claim the player does not own.

The alias and its argument completion are available through GPExpansion's own /claim command and through the GP3D command addon/interceptor when GP3D owns the root command.

Sign-move command routing

movesign is similarly registered as a /claim subcommand and exposed through GP3D's command integration. The standalone /moveclaimsign command provides the same workflow for servers where another plugin's /claim routing makes a direct command more convenient.

Subdivision Fixes

Active tenants can name their rented subdivision

Running /claim name <name> while inside a rented subdivision no longer fails with the generic ownership message when the player is that subdivision's active tenant.

The resolver now walks to the deepest subdivision containing the player's full X/Y/Z location, checks the rental stored against that exact claim ID, and permits the rename when the renter UUID matches and the rental has not expired. griefprevention.claim.name is still required.

This exception applies only to naming. Renting a subdivision does not make the tenant its GriefPrevention owner and does not grant the other ownership-gated claim customization or management commands.

Unnamed subdivisions inherit a parent claim name

%gpx_currentclaim_claimname% now starts at the deepest subdivision containing the player. If that subdivision has no nonblank custom name, the placeholder walks upward and returns the nearest named ancestor. An explicit subdivision name always wins; an empty result is returned only when neither the subdivision nor any parent has a name.

%gpx_currentclaim_claimid% now uses the same deepest-claim resolution. Players inside a subdivision therefore receive that subdivision's ID rather than a parent ID returned by a broader GriefPrevention lookup.

Together, these changes let an unnamed rented area display the main claim's established label until its tenant gives the subdivision a more specific name.

Admin-claim subdivision evictions can start

Administrative claims deliberately have no owner UUID. /claim evict <player> previously passed the admin-claim authorization check, then failed later with Unable to determine claim owner when the eviction record needed an actor UUID.

When a player has griefprevention.adminclaims, GPExpansion now records that authorized administrator as the eviction actor for an ownerless admin claim. The existing griefprevention.evict requirement, standing/location validation, notice period, and renter notification behavior are unchanged.

Trust removal at eviction start and trust restoration after cancellation now target the actual rented subdivision instead of its top-level parent claim. This prevents an eviction in one subdivision from altering trust at the wrong claim level.

Claim Ban Enforcement

Adding a GPExpansion claim ban now schedules an enforcement check for the affected online player one entity tick later. If the player is currently inside the claim from which they were banned, the existing safe-ejection logic runs without waiting for movement, teleportation, interaction, or another incidental event.

Unbans do not eject anyone. The one-tick deferral lets the command or datastore operation finish its dismount and release work first and remains safe on both Paper and Folia.

Behaviour Changes

  • Claim owners and the eligible tenant, buyer, seller, or creator can move supported managed signs when they have the new type-specific permission. Those permissions are granted by default.
  • Players who do not control a sign require its exact .other permission. These permissions default to false; grant them explicitly to staff roles that should move other players' signs.
  • Moving consumes the destination sign and leaves the source sign block blank and unmanaged. It does not transfer the source block itself.
  • A rent-sign tenant is revalidated when the destination is processed and cannot complete a move after the rental expires.
  • Shared self-mailboxes are excluded from creator-level moving and require the .other permission.
  • New managed signs retain creator UUID metadata. A legacy global sign adopts the current claim owner as its creator on the first authorized move.
  • /claim name grants an active tenant a narrow exception for the exact rented subdivision; it does not broaden tenant ownership elsewhere.
  • %gpx_currentclaim_claimname% can now return a parent name inside an unnamed subdivision, and %gpx_currentclaim_claimid% now returns the deepest containing subdivision ID.
  • Starting an eviction in an ownerless admin claim records the authorized administrator as the actor instead of rejecting the operation.
  • Newly added claim bans can eject stationary online players after one entity tick.

Permissions

This release adds ten permission nodes:

griefprevention.sign.move.rent
griefprevention.sign.move.rent.other
griefprevention.sign.move.sell
griefprevention.sign.move.sell.other
griefprevention.sign.move.mailbox
griefprevention.sign.move.mailbox.other
griefprevention.sign.move.global
griefprevention.sign.move.global.other
griefprevention.sign.move.self-mailbox
griefprevention.sign.move.self-mailbox.other

Each base node defaults to true. Each .other node defaults to false. Existing permissions continue to govern claim info, naming, eviction, and admin-claim management.

Configuration and Language

  • No new configuration keys.
  • Twelve sign-move.* language keys provide selection, permission, success, cancellation, failure, and timeout messages.
  • New standalone command: /moveclaimsign [cancel].
  • New /claim subcommands: /claim info [claimId] and /claim movesign [cancel].
  • New persistent sign field: sign.creator. It is added as signs are created, with compatibility handling for older signs.
  • Maven artifact version: 1.2.0.

Compatibility

Built against GriefPrevention3D 18.2.7 and Paper 26.2. The move workflow uses Paper's sign-open event so copied text can replace the normal sign editor safely. Location-bound completion and clearing use GPExpansion's scheduler abstraction for Paper and Folia compatibility.

No storage migration is required. Existing rental, sale, mailbox, claim metadata, configuration, and language customizations retain their formats. Missing bundled language defaults continue to be available without overwriting customized values.

Verification

  • mvn clean package completed successfully and produced target/GPExpansion.jar.
  • The filtered paper-plugin.yml and bundled lang.yml were parsed successfully as YAML.
  • The project currently has no automated test sources, so Maven reported no tests to run.
  • The new commands, sign interaction workflow, cross-region movement, active-rental expiry check, admin-claim eviction path, tenant rename path, placeholder inheritance, and immediate online ban ejection still need live-server verification.
1.1.19Релиз26.1.1, 26.1.2, 26.2 · 11 августа 2026 г.

GPExpansion v1.1.19

This release fixes several places where GPExpansion had the right data but used it at the wrong level or delivered it at the wrong time.

The most visible example was CrowBar claim waypoints. A live report showed only 102 claims taking roughly 23 minutes to appear. Those claims were already loaded in memory and could be prepared in milliseconds; the delay came from sending one snapshot shortly after join, before the client had reliably advertised its custom payload channel, then waiting for an unrelated claim event to cause another send. GPExpansion now refreshes when CrowBar registers the channel and has a short delayed fallback.

The other headline changes are in claim administration: the flags GUI now shows the effective state GPFlags actually enforces, subdivisions keep their own flags and snapshots, admin claims can be managed by administrators, and rent/sell/mailbox signs accept compact prices such as 1k, 2.5m, and 1b.

Read the Behaviour Changes section before updating. The money-payment cap changed from 9,999,999.99 to 2,000,000,000. Existing signs above the old cap can therefore charge substantially more than they did before.

No configuration keys or permission nodes were added. The Maven project version is now 1.1.19; version.config-version remains unchanged.

Bug Fixes

CrowBar claim snapshots no longer depend on winning the join race

The old join path waited one tick and called rebuildAll(). That had two separate problems:

  • the payload could be sent before CrowBar's crowbar:claim_data receiver had been registered with the server;
  • one joining player caused the claim set for every online player to be recalculated and serialized.

If the first payload was missed, GPExpansion had no acknowledgement or retry. CrowBar correctly continued to show Collecting claims... Please wait., and the player only recovered when a trust, claim, colour, name, spawn, or other waypoint event happened to trigger a global rebuild. That explains why a 102-claim server could appear to spend 23 minutes "caching": it was waiting for a resend, not processing claims for 23 minutes.

Delivery now has three opportunities:

  1. A targeted refresh is still scheduled one tick after join for the fast path.
  2. PlayerRegisterChannelEvent triggers another targeted refresh as soon as Paper reports that the player registered crowbar:claim_data. This is the first delivery point known to have a listening CrowBar client.
  3. A final targeted refresh runs after 60 ticks, roughly three seconds, as a fallback for unusual registration timing.

This is a server-side protocol fix. The JSON format and channel name are unchanged, so existing CrowBar clients can receive the more reliable delivery without a matching client update.

World changes now refresh only the player who changed worlds. Quits only discard that player's vanilla waypoint state; they no longer rebuild anyone else, because another player going offline cannot change claim ownership or trust visibility.

Claim flags show what GPFlags actually enforces

The flags GUI previously read only the raw flags stored directly on a claim. GPFlags resolves flags through a wider effective chain: the claim, its parent, defaults, world settings, and server settings. As a result, the GUI could display a flag as disabled while GPFlags was actively enforcing an inherited value.

GPExpansion now reflects into GPFlags's effective lookup and uses that result for every flag shown in the GUI. An inherited value is marked with:

(inherited from world/server, not set on this claim)

Toggling also starts from the effective state and writes an explicit opposite override to the claim. It no longer removes a raw value and accidentally allows an inherited parent, default, world, or server value to take over again.

/claim flags targets the subdivision the player is standing in

Some GriefPrevention-compatible lookup paths return a parent claim even when the player is inside a subdivision. Since GPFlags stores values by claim ID, that opened the parent's flags instead of the subdivision's.

When /claim flags is used without an explicit claim ID, GPExpansion now checks the resolved parent's children and narrows the target to the subdivision containing the player's full X/Y/Z position.

Snapshots preserve subdivision identity

The shared claim context used to retain only the top-level claim for snapshot operations. Taking a snapshot while standing in a rented or independently managed subdivision could therefore snapshot the parent and store the result under the parent's ID.

The context now carries both the top-level claim and the claim actually resolved at the player's position. Snapshot creation, listing, restoration, and deletion use the resolved claim ID, so subdivisions get their own snapshot history and bounds.

Administrators can manage ownerless admin claims

Ownership-gated claim actions treated an administrative claim as unmanageable because admin claims deliberately have no owner UUID. A player with griefprevention.adminclaims is now treated as the manager of an admin claim for GPExpansion's shared claim-management checks.

This does not grant access to ordinary player claims and introduces no new permission. It makes GPExpansion agree with GriefPrevention's existing administrative-claim permission model.

Sign Prices

Compact k, m, and b amounts

Rent, sell, and mailbox sign parsing now accepts case-insensitive shorthand:

  • 1k = 1,000
  • 2.5m = 2,500,000
  • 1b = 1,000,000,000
  • an L suffix remains available for XP levels where that economy type supports it

Shorthand is expanded to plain digits before the existing payment and rental code sees it. Fractional shorthand is multiplied and rounded down to a whole unit; for example, 1.2345k becomes 1,234.

The setup wizard accepts the same syntax. When it generates sign lines from a whole numeric price, it automatically compacts exact multiples of a billion, million, or thousand so large prices fit more comfortably on a sign. Values that are not exact multiples are left unchanged rather than approximated.

Maximum amount is 2 billion

New shorthand or numeric sign amounts above 2,000,000,000 are rejected. The limit keeps item, XP, and claim-block paths inside their integer-backed ranges.

The money withdrawal path previously clamped the amount to 9,999,999.99. A sign displaying a larger stored price could therefore charge only the old cap. It now uses the same 2-billion ceiling as parsing.

Upgrade consequence: audit existing money-based rent, sell, and mailbox signs whose stored price exceeds 9,999,999.99. They will now charge the intended amount, up to 2 billion, instead of silently undercharging at the old cap.

Performance

Waypoint refreshes do less work

Join and world-change refreshes now scan claims for only the affected player. With P online players and C claims, a join's visibility work changes from approximately P × C checks to C. The improvement scales directly with online player count; a join with 50 players online performs roughly 50 times fewer visibility checks.

Global rebuilds are still required when claim visibility or metadata genuinely changes, but they now:

  • walk the GP3D in-memory claim collection once;
  • prepare each visible claim's anchor, display name, colour, and deterministic waypoint UUID once;
  • pass each viewer's final claim list directly to JSON serialization and vanilla waypoint syncing instead of repeatedly scanning the union of every viewer's claims.

Claim-event bursts are coalesced with one queued rebuild per tick. Bulk operations that fire many create, resize, trust, or metadata events no longer enqueue an identical full refresh for every event in that burst.

GP3D itself required no change for this work. DataStore#getClaims() and direct UUID trust checks are already in-memory; the observed multi-minute wait was delivery timing, not disk access or GP3D claim lookup time.

Internal Command Routing

GUI buttons, command interception, the GP3D command addon, and nested /claim fly dispatch used to construct anonymous Bukkit Command objects solely to call ClaimCommand#onCommand or its tab completer. The command implementation only needed a command name.

ClaimCommand and ClaimFlyCommand now expose direct name-based entry points, and tab completion has the same path. All internal callers use those methods, removing the synthetic command objects while preserving Bukkit's normal executor entry points for registered commands.

This is intended as an internal cleanup. It does not change command names, aliases, syntax, permissions, or scheduling behavior.

Behaviour Changes

  • CrowBar claim data should arrive immediately when channel registration is observed, with a roughly three-second fallback instead of waiting indefinitely for another claim event.
  • The flags GUI reports effective inherited values. A flag may now display as enabled even when there is no raw flag entry on that claim; that is the state GPFlags was already enforcing.
  • Toggling an inherited flag creates an explicit claim override. It no longer means "remove this claim's entry and fall back to inheritance."
  • /claim flags and snapshot operations preserve subdivisions instead of silently targeting the parent claim.
  • Holders of griefprevention.adminclaims pass GPExpansion's ownership gate for ownerless admin claims.
  • Existing money signs above 9,999,999.99 can charge more after updating, up to the new 2-billion ceiling.
  • The setup wizard's default invalid-price message now mentions 1k, 1m, and 1b. Existing customized lang.yml values are not overwritten merely because the bundled wording changed.

Compatibility

Built against GriefPrevention3D 18.2.7 and Paper 26.2. The waypoint delivery change uses Bukkit/Paper's standard plugin messaging and PlayerRegisterChannelEvent; it does not require a new CrowBar payload format.

GPFlags remains an optional reflective integration with no compile-time dependency. Effective-state display expects a GPFlags build exposing FlagManager#getEffectiveFlag(String, Claim, World). If that method is absent or incompatible, GPExpansion logs the reflective failure rather than preventing the plugin from enabling.

No storage migration is needed. Existing claims, flags, snapshots, signs, and configuration files retain their formats.

Configuration and Language

  • No new configuration keys.
  • No new permission nodes.
  • No new commands or aliases.
  • No new language keys.
  • The existing wizard.invalid-price bundled default now documents shorthand amounts.
  • Maven artifact version: 1.1.19.

Verification

  • mvn test and mvn package completed successfully for GPExpansion.
  • The full GriefPrevention3D Gradle test suite completed successfully; GP3D source was not changed.
  • The pre-fix waypoint failure was observed live as an approximately 23-minute wait with 102 claims, which is what identified the delivery race. The new registration-driven and delayed retry paths have compiled successfully but still need confirmation through a fresh client join on a live server.
1.1.18Релиз26.1.1, 26.1.2, 26.2 · 31 июля 2026 г.

GPExpansion v1.1.18

A single fix, and a short release. v1.1.17 stopped the claim map from calling the code below; this one fixes the code itself.

Shortly after v1.1.17 went out, a watchdog thread dump arrived from a server running an older build. It shows the server thread stalled past ten seconds inside one /claimmap click:

The server has not responded for 10 seconds! Creating thread dump
  java.lang.Throwable.fillInStackTrace(Native Method)
  java.lang.NoSuchMethodException.<init>
  java.lang.Class.getMethod
  GPBridge.bruteForceFindClaim
  GPBridge.getClaimAt
  GPBridge.getDominantClaimInCell
  ClaimMapEditorGUI.createCellItem
  ClaimMapEditorGUI.populateInventory
  ClaimMapEditorGUI.handleClick

The bottom half of that stack is the path v1.1.17 addressed, by sampling map cells less densely and by not calling bruteForceFindClaim at all on GriefPrevention3D and upstream GriefPrevention. The top half is something v1.1.17 did not address: the sweep's dominant cost was never claim geometry. It was the JVM building an exception, complete with stack capture, that the code immediately discarded — once per claim, per probe.

Who needs this. If you are on v1.1.17 with GriefPrevention3D or upstream GriefPrevention, nothing here is observable; the affected method is already unreachable for you. This closes the remaining exposure for legacy forks and fixes the defect at its source rather than routing around it. If you are on v1.1.16 or earlier, the release you need is v1.1.17 — take it first, or take this, which includes it.

No configuration changes, no new permissions, no lang keys. version.config-version stays at 1.1.2.

Bug Fixes

bruteForceFindClaim constructed an exception per claim, per probe

The method resolved its reflective accessors inside the loop over every claim, each behind an if (method == null) guard:

for (Object claim : claims) {
    Class<?> cc = claim.getClass();
    if (is3D == null) {
        try { is3D = cc.getMethod("is3D"); } catch (NoSuchMethodException ignored) {}
    }
    if (containsY == null) {
        try { containsY = cc.getMethod("containsY", int.class); } catch (NoSuchMethodException ignored) {}
    }
    ...

That guard is a cache that can only ever store a hit. A method the claim class does expose is resolved once and the guard closes. A method it does not expose leaves the guard open forever, so getMethod runs again on the next claim, and the next, and every one after — and each miss constructs a NoSuchMethodException.

Constructing an exception is not cheap. fillInStackTrace walks the live stack and materialises a frame array, which is among the most expensive routine operations a JVM performs, and it happens whether or not anyone ever reads the trace. Here nobody did: the catch block is ignored. The plugin was paying full price for a stack capture, tens of frames deep, to discard it — inside the innermost loop of a lookup whose correct answer was "no claim here."

Multiply it out. A miss swept every claim on the server, twice. The map editor probed a 20x20 tile at 400 points and drew 45 tiles per repaint. On a server with a few thousand claims, one inventory click is on the order of millions of stack captures. Ten seconds of unresponsive server thread is the expected result, not a surprising one.

Resolution is now hoisted out of the loop and routed through the handle cache introduced in v1.1.17, which stores misses as well as hits. A method that does not exist is looked up once, ever, and the loop body reduces to the invoke calls that were always the point.

Why this only bites under version skew

On GriefPrevention3D 18.2.7 — the current build target — every accessor the sweep probes is public on Claim: is3D(), containsY(int), getArea(), getGreaterBoundaryCorner(), getLesserBoundaryCorner(). Nothing misses, so every guard closes on the first claim and no exception is ever built.

That is what makes this failure mode nasty. Under matched versions the sweep is merely expensive — O(claims) reflective invokes. It degrades into an exception storm only when GPExpansion probes for something the GriefPrevention build in front of it does not expose: an older fork, a newer one that renamed an accessor, or a divergent third-party build. The plugin gets slower in exact proportion to how unfamiliar the fork is, which is the opposite of how graceful degradation is supposed to work.

The reporting server was running a GPExpansion build older than v1.1.17 — its GPBridge line numbers do not match any current source — so the specific accessor that was missing there cannot be identified from the dump. The mechanism is unambiguous regardless: Class.getMethod reached its throw site, and the frame above it is fillInStackTrace.

Behaviour Changes

  • Accessor resolution now samples only the first claim's class. The old code re-attempted resolution per claim, so in a collection mixing claim types it could eventually pick up a method that the first element lacked. Resolution now happens once, against claims.get(0).getClass(). GriefPrevention stores a single Claim type in DataStore.claims, so this is theoretical rather than a change anyone can observe — but it is a real narrowing, and it is the assumption the old guards were already making in the common case.
  • A missing contains method now returns null immediately instead of throwing out of the loop and being caught by the enclosing handler. Same result, one less exception.

Compatibility

Unchanged from v1.1.17. Built and verified against GriefPrevention3D 18.2.7.

The reachability rule set in v1.1.17 still holds: bruteForceFindClaim runs only when the resolved DataStore signature is the legacy two-argument getClaimAt(Location, boolean). GriefPrevention3D resolves to the four-argument form and upstream GriefPrevention to the three- or four-argument form, so neither reaches it. This release is what happens when a fork does.

Notes

  • No API, command, permission, placeholder, or configuration changes.
  • Verified by a clean mvn package against GriefPrevention3D 18.2.7, and by confirming in the GriefPrevention3D source that all five probed accessors are public on Claim — which is why the exception path does not trigger under matched versions.
  • Not run on a live server, and the watchdog trip has not been reproduced or confirmed fixed against a real workload. The reasoning is mechanical: the exception construction is removed because the lookup that caused it is removed. Confirmation would be a /claimmap click at the 20x20 and 200x200 zooms on the reporting server's fork, without a watchdog trip.
  • Still outstanding, carried over:
    • BanEnforcementListener's ejection path and /claim ban still fall back to getHighestBlockYAt(), so ejecting from a nether claim can deposit a player on the roof.
    • getSafeDestination() uses -1 as its "no ground found" sentinel, which collides with a genuine ground block at y=-1 in 1.18+ worlds.
    • ClaimDataStore remains a plain HashMap with no synchronisation.
1.1.17Релиз26.1.1, 26.1.2, 26.2 · 31 июля 2026 г.

GPExpansion v1.1.17

Three fixes, all on the same theme: work that ran on every tick, every player move, or every map repaint without needing to.

The headline is a startup failure. With DiscordSRV and Skript installed alongside GriefPrevention3D, Paper reports an unsatisfiable load order:

[LoadOrderTree] Circular plugin loading detected:
[LoadOrderTree] 1) GPExpansion -> DiscordSRV -> Skript -> GriefPrevention -> GPExpansion

Alongside it, two profiler findings: BanEnforcementListener.onMove() accounting for 4.50% of sampled server-thread CPU time, and the /claimmap editor visibly stalling on every click. Both trace back to the same reflective claim lookup.

No configuration changes, no new permissions, no lang keys. version.config-version stays at 1.1.2.

Bug Fixes

The DiscordSRV load-order cycle

Two of the four edges in that loop are ours, declared in paper-plugin.yml:

  • GriefPrevention: load: AFTER — GriefPrevention loads after GPExpansion. This one stays. GriefPrevention3D declares provides: [GriefPrevention] and registers its own claim command; Bukkit's command map only assigns an unprefixed label to the first plugin that asks for it, so loading first is what gives GPExpansion /claim rather than /gpexpansion:claim.
  • DiscordSRV: load: BEFORE — DiscordSRV loads before GPExpansion. This is the edge that closes the loop, because DiscordSRV soft-depends on Skript, and Skript soft-depends on GriefPrevention.

The DiscordSRV entry is now removed, with the reasoning recorded in the file next to the existing Nexo exclusion, which was omitted for the same class of reason.

Removing it cost nothing, because the integration it existed for was never active. DiscordSRVChatCaptureBridge — added in v1.0.6 to stop setup-wizard replies leaking into the Discord relay — was written, compiled, and shipped with no call site anywhere in the plugin. It has been dead code in every release since.

Rather than delete it, this release wires it up and makes it independent of load order:

  • register() hooks immediately if DiscordSRV is already enabled, and otherwise registers a one-shot PluginEnableEvent listener that hooks when DiscordSRV enables and then unregisters itself.
  • It is called from onEnable() with a predicate covering both capture paths — SetupWizardManager.hasActiveSession() and DescriptionInputManager.hasPending().

The hook was always fully reflective (join-classpath: false, Class.forName against DiscordSRV's own class loader, registerEvent with a hand-built EventExecutor), so nothing about it required a declared dependency in the first place — only the assumption that DiscordSRV would have enabled first.

This is a visible behaviour change. On a server with DiscordSRV, chat typed in response to a /mailbox, /rentclaim, /sellclaim, or /claim desc prompt now stops at the server instead of being relayed to Discord. That was the intended behaviour in v1.0.6; it starts working here.

Performance

getClaimAt() swept the entire claim table on every miss

GPBridge.getClaimAt() is the single hot path shared by ban enforcement, claim flight, the map editor, and most GUIs. On a lookup that found nothing it did four things in sequence:

  1. call GriefPrevention's getClaimAt with ignoreHeight = true;
  2. on null, call it again with ignoreHeight = false;
  3. on null, run bruteForceFindClaim(location, true) — iterate every claim on the server, invoking Claim.contains() reflectively on each;
  4. on null, run bruteForceFindClaim(location, false) — the same sweep again.

Steps 2 and 4 could never find anything steps 1 and 3 had not. Claim.contains(location, ignoreHeight, excludeSubdivisions) only adds a Y-bounds test when ignoreHeight is false, so the true pass matches a strict superset: if it returns null, so will the false pass. Both retries were pure duplicate work.

Step 3 is worse than redundant — it is redundant and unbounded. GriefPrevention3D's DataStore.getClaimAt() resolves through getChunkClaims(), a chunk-indexed lookup; a null from it is authoritative. The sweep re-derived that answer in O(number of claims), with a reflective call per claim, and it ran on every miss — which is to say, every block a player walks in wilderness.

Now:

  • The ignoreHeight = false retry is gone.
  • The sweep runs only when the resolved signature is the legacy two-argument getClaimAt(Location, boolean), which cannot express subclaim selection and is the one case where a fallback is defensible. GriefPrevention3D resolves to the four-argument signature and upstream GriefPrevention to the three- or four-argument one, so neither reaches it.

A wilderness lookup drops from two reflective invokes plus two full table sweeps to one reflective invoke.

v1.1.13 already identified the doubled reflective invoke while fixing claim flight, and worked around it there by not calling the method. This fixes it at the source, for every caller.

BanEnforcementListener.onMove() was ~40% of GPExpansion's server-thread cost

Spark attributed 4.50% of sampled server-thread CPU time to this one handler. Against GPExpansion's total of 11.35% in the same profile, that is roughly 40% of everything the plugin was doing on the server thread — spent in a single movement listener.

Two things that figure does not say. It is not 4.5% of every tick: PlayerMoveEvent fires many times per tick per player, and the profiler aggregates all of those calls across the sampling window, so the cost is spread unevenly across ticks and scales with how many players are moving. And converting it to wall time — 4.5% of a 50 ms budget is 2.25 ms — is arithmetic on an average, not a per-tick measurement; a sampling percentage alone does not support a claim about what any individual tick spent. What it does establish is proportion, and the proportion was the problem.

The handler ran checkBanned() on every block-coordinate change for every player. That call reaches getClaimAt(), so before the fix above, every player walking anywhere in the world was triggering two full claim-table sweeps per block. Enforcement was paying full lookup cost continuously to discover that almost nobody is banned from almost anywhere.

The listener now keeps a footprint index. rebuildBanIndex() collects the world name and X/Z bounds of the top-level claims that actually ban somebody, and nearBannedClaim() rejects a movement with a string compare and four integer comparisons against that list. On a typical server the list is empty or has a handful of entries, so the handler becomes a few comparisons and a return. Only a move that lands inside one of those footprints proceeds to the real check.

The index is a pre-filter, never a decision — checkBanned() still resolves the claim, applies bypass permissions, respects 3D subdivision Y-bounds, and evaluates public-ban trust exactly as before. Bans are stored against the top-level claim ID (/claim ban resolves through mainClaimId), which is why indexing top-level footprints is sufficient, and why a bounding box is a safe over-approximation for shaped claims.

Invalidation is two-part:

  • Ban changes take effect immediately. ClaimDataStore now carries a banRevision counter, bumped by setPublicBanned, addBannedPlayer, removeBannedPlayer, set, remove, initial load, and bans.yml migration. The move handler compares one volatile int; a mismatch rebuilds inline before deciding anything. There is no window in which a freshly banned player can walk in unchallenged.
  • Claim geometry changes are picked up within 5 seconds. A resize, creation, or deletion does not touch ban data, so a 100-tick repeating task rebuilds unconditionally.

If claim data cannot be reached — GriefPrevention still loading, or the bridge unavailable — the index marks itself unusable and every move falls through to the full check. Enforcement degrades to the old cost, never to silence.

Two smaller changes on the same path:

  • onInteract() got the same pre-filter. It ran a full claim lookup on every right- and left-click.
  • The beingEjected check now tests isEmpty() before the set lookup, which is the common case by a wide margin.

ClaimDataStore was creating an entry for every claim a player entered

isPublicBanned() and getBannedPlayers() were implemented on get(claimId), which is computeIfAbsent. Every ban check against a claim with no stored data inserted an empty ClaimData — so simply walking around the server grew the in-memory map by one entry per distinct claim visited, on a hot path, in a plain HashMap.

Both are now plain reads returning the default when absent. getBans() still uses get(), because its two callers (the ban list command and BannedPlayersGUI) legitimately want the entry.

The claim map editor

/claimmap rebuilds all 45 tiles on every click — zoom, pan, mode toggle, and each cell edit. Per tile it resolves the dominant claim and the selected claim's coverage. Both were badly sized.

Sampling density. getDominantClaimInCell() probed every block for cells 20 wide or narrower. At the 20x20 zoom that is 400 getClaimAt() calls per tile, 18,000 per repaint — each of which, before the fix above, could sweep the whole claim table twice. Sampling is now capped at 7 probes per axis, so a tile costs roughly 64 lookups regardless of zoom, and a repaint around 2,900. The 50% coverage threshold that decides whether a claim owns a tile is unchanged; a claim too small to be found by the coarser grid was already too small to clear that threshold.

Coverage counting. getClaimCoverageInCell() is O(1) arithmetic for rectangular claims, which is the overwhelming majority. For genuinely non-rectangular shaped claims it ran a point-in-polygon ray cast per block: 40,000 per tile at the 200x200 zoom, up to 1.8M per repaint. Counting is now exact below 4,096 blocks of overlap and estimated from a strided sample above it, scaled back to the true area. The reflective probe fallback — used when a fork does not expose getBoundaryPolygon() — gets the same treatment at a lower threshold of 1,024, since each of its probes is a reflective call rather than arithmetic.

Reflective handle caching in GPBridge

GPBridge resolves GriefPrevention's API through Class.getMethod on every call. Paper's reflection remapper makes that expensive — the bridge already noted this in PolygonView, where the polygon accessors were hand-cached for exactly this reason.

claimContains() was the worst case: it called getClass().getMethods(), which copies the entire method array, then linear-scanned it — once per block probe, inside the coverage loops described above.

There is now a ClassValue-keyed cache of resolved handles, storing misses as well as hits so repeated absent-method lookups stay cheap. ClassValue ties entries to the class rather than to a static map, so a plugin reload does not pin GriefPrevention's old class loader. Applied to claimContains, getClaimId, getClaimCorners, getClaimWorld, getClaimAreaSafe, isOwner, isShapedClaim, resolveClaimBoundaryPolygon, and the min/max Y accessors — the handles reached from per-block and per-move loops.

Behaviour Changes

  • Wizard and description chat no longer reaches Discord. See the load-order section. Intended since v1.0.6, active from this release.
  • A null from GriefPrevention is now final. With the brute-force sweep gone, a lookup returns empty wherever GriefPrevention's own chunk index says there is no claim. If a fork's index is ever wrong, GPExpansion no longer papers over it — it agrees with the fork. This is the correct behaviour, but it is a change: the sweep could previously mask an indexing bug at ruinous cost.
  • Map tile classification is approximate at high zoom. Dominant-claim resolution and shaped-claim coverage are sampled rather than exhaustive above the thresholds above. Both were already approximate — getDominantClaimInCell has always been a sampler — this widens the stride. Expect no visible difference on rectangular claims, which are exact either way.
  • Ban geometry changes lag by up to 5 seconds. Resizing or deleting a claim that bans somebody takes up to 100 ticks to appear in the footprint index. Ban changes are immediate. The worst case is a claim expanded to cover ground it did not before, where enforcement on the new strip starts a few seconds late.

Compatibility

Built and verified against GriefPrevention3D 18.2.7.

The getClaimAt change is signature-driven, not fork-driven. GriefPrevention3D resolves to getClaimAt(Location, boolean, boolean, Claim) and upstream GriefPrevention to the three- or four-argument form; both skip the sweep. A fork exposing only getClaimAt(Location, boolean) keeps the old fallback behaviour unchanged.

Nothing in this release adds a compile-time dependency. The DiscordSRV hook is reflective and optional; with DiscordSRV absent, register() installs a PluginEnableEvent listener that never fires.

Notes

  • No API, command, permission, placeholder, or configuration changes.
  • Verified by a clean mvn package against GriefPrevention3D 18.2.7, and by tracing DataStore.getClaimAt and Claim.contains in the GriefPrevention3D source to confirm the ignoreHeight superset relation and the chunk-index guarantee. None of it has been run on a live server, and no after-profile was taken — the 4.50% is a before-state sampling figure from Spark, describing share of server-thread CPU time over the profiling window, not per-tick duration. The claims here are structural, that the work no longer happens, not measured TPS improvements. A comparable after-profile on the same server, at similar player count and movement load, is what would confirm the size of the win.
  • Still outstanding, carried over:
    • BanEnforcementListener's ejection path and /claim ban still fall back to getHighestBlockYAt(), so ejecting from a nether claim can deposit a player on the roof. Untouched by this release — the fix above changes when the ejection path runs, not where it puts you.
    • getSafeDestination() uses -1 as its "no ground found" sentinel, which collides with a genuine ground block at y=-1 in 1.18+ worlds.
    • ClaimDataStore remains a plain HashMap with no synchronisation. The footprint rebuild reads it from the global scheduler, which is the main thread on Paper and Purpur. On Folia it is a region thread, in common with the rest of the store's existing access pattern.
1.1.16Релиз26.1.1, 26.1.2, 26.2 · 31 июля 2026 г.

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: false to turn it off, or blank commands.accruals-received in lang.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. PlayerData clamps the total with Math.min(newTotal, accruedLimit), so the blocks are discarded on arrival. The existing notify-on-cap message only fires when a profile's cap drops below what the player already has, not in this steady state.
    • Any plugin listening at HIGHEST or later can still change the amount or cancel the event after GPExpansion has spoken. GPExpansion listens at HIGH; nothing in a default GriefPrevention + GPExpansion install sits behind it.

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 via runAtLocation;
  • an AtomicInteger counts completions, and the last slice to finish writes the .snap file 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() returns true once the per-chunk tasks are queued, so the index entry is written and /claim snapshot confirms 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 .snap is 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.DEFAULTS holds accruals.notify-on-accrue. addMissingDefaults() runs during load(), writes any key the admin's file does not contain(), saves, and logs Added missing config option: accruals.notify-on-accrue = true.
  • Messages.DEFAULTS holds commands.accruals-received. addMissingKeys() injects it, and ensureDefaultsInFile() additionally walks the jar's bundled lang.yml and 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 compile against GriefPrevention3D 18.2.7 and by tracing the event path through DeliverClaimBlocksTask and PlayerData.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:
    • BanEnforcementListener and /claim ban's ejection path fall back to getHighestBlockYAt(), so ejecting from a nether claim can still deposit a player on the roof.
    • getSafeDestination() uses -1 as its "no ground found" sentinel, which collides with a genuine ground block at y=-1 in 1.18+ worlds.
1.1.15Релиз26.1.1, 26.1.2, 26.2 · 27 июля 2026 г.

GPExpansion v1.1.15

Follow-up to v1.1.14. That release stopped /claim tp from putting players on top of the nether roof, but replaced it with two new failures: creative players ended up embedded inside the bedrock slab, and survival players were told the claim was unsafe and not teleported at all. Both come from the same faulty assumption in the new column scan.

Anyone running 1.1.14 should take this update — nether claims without a custom spawn are effectively unreachable on it.

Bug Fixes

The destination was the block above the terrain, which under a roof is the roof

getHighestTeleportY() walked down from the ceiling limit, skipped roof bedrock, stopped at the first solid block, and returned y + 1 — the position a player would stand in over open terrain.

That last step assumed the space above the ground is free. Under a bedrock roof it usually is not: nether terrain generates packed against the underside of the slab, so the first solid block found on the way down is netherrack at, say, y=124, and y + 1 is the bedrock at y=125. The function reported a destination inside the roof.

What happened next depended on game mode, which is why it presented as two unrelated bugs.

Creative and spectator bypass the safe-location search entirely while teleport.safe-location.staff-ignore-unsafe-location is true (the default) — the requested location is used as-is, unvalidated. Staff testing the fix were teleported straight into the bedrock, a couple of blocks below the roof, with a view of the inside of the slab.

Survival ran the validator, which correctly rejected the position — and then failed to recover, because 1.1.14's clamp left the search starting inside the slab:

  • getSafeDestination() clamped a roof-level request down to logicalHeight - 1 (y=127), so its ground scan began immediately under the bedrock.
  • The scan skipped roof bedrock and stopped at the first terrain below it, putting the candidate stand position back inside the slab.
  • The horizontal fallback searches that one Y level, where within the claim bounds there is nothing but more bedrock.
  • The final fallback searches upward, which under a roof only goes further into it.

Every branch failed, getSafeDestination() returned null, and the player got claim.teleport-unsafe.

What changed

  • getHighestTeleportY() requires head room. A candidate is accepted only when the two blocks above the ground are open, so the scan keeps falling through packed terrain until it reaches a real gap. Where a column has no opening anywhere — solid from the roof to the floor — it now falls back to the terrain surface below the slab instead of failing, leaving the nearby search something to work outward from.
  • getSafeDestination() snaps to that surface rather than clamping to logicalHeight - 1. Clamping was the survival-side bug: it dropped the search to just below the ceiling, which is the one place under a roof where the horizontal and upward fallbacks are both guaranteed to be useless.
  • isValidGround() and isOpenSpace() are now shared predicates. The ground rules — no lava, water, damaging blocks, beds, or roof bedrock — existed as an inline expression in the descent scan and were re-derived, slightly differently, in the new column scan. One definition now backs both, which is what let the two scans disagree in the first place. isOpenSpace() additionally excludes water and lava from counting as a place to stand, which the raw hollow check did not.

Behaviour Changes

  • Where a nether claim without a custom spawn resolves to. The destination is the first gap with head room scanning down from the roof — in practice the ceiling cavern of the nether, not necessarily the level the owner built at. Claims whose column is packed for hundreds of blocks will land further down than their build. Setting an explicit /claim setspawn remains the way to control this exactly.
  • Custom claim spawns are unaffected by any of this. A stored spawn Y is used directly and only goes through the ceiling logic if it sits above the roof.
  • Nothing changes in the overworld or the end. hasCeiling() is false there and none of these paths run.

Configuration

Nothing to migrate. config-version stays at 1.1.2; no keys were added, renamed, or given new defaults.

Notes

  • No API, permission, placeholder, or command changes. getHighestTeleportY() keeps its signature; only what it returns for a roofed column changed.
  • Both 1.1.14 regressions were reproduced in-game before the fix — the creative embedding visually, the survival refusal as claim.teleport-unsafe. The fix itself is verified by compilation and by tracing both paths, not by a fresh in-game teleport.
  • Still outstanding from 1.1.14, unchanged:
    • BanEnforcementListener and /claim ban's ejection path fall back to getHighestBlockYAt(), so ejecting from a nether claim can still deposit a player on the roof.
    • getSafeDestination() uses -1 as its "no ground found" sentinel, which collides with a genuine ground block at y=-1 in 1.18+ worlds.

Комментарии

Загружаем…