Content generated with AI — it may contain mistakes.

Gameplay

Regions

Areas of the world with rules attached: shapes, policies, enter and exit events, enforcement, the selector and outlines.

A region is an area of the world with rules attached. Every plugin registers its own, and any plugin can ask what is at a location.

PluginRegions regions = Regions.of(this);
 
// After loading from your database, hand over the whole set at once.
regions.replaceAll(rows.stream()
        .map(row -> regions.region(row.id(),
                new WorldIdentity(row.worldId(), row.worldName()),
                Cuboid.blocks(row.minX(), row.minY(), row.minZ(), row.maxX(), row.maxY(), row.maxZ()),
                row.priority(),
                PolicySet.of(CommonRegionPolicies.PVP, row.pvp())))
        .toList());
 
// Ask, from any thread.
boolean pvp = Regions.resolve(player.getLocation(), CommonRegionPolicies.PVP).value();
ConceptWhat it is
RegionSnapshotOne region as it is now: id, owner, world, shape, priority and policies. Immutable.
PolicySetWhat a region declares. Not declaring a key is different from declaring its default.
RegionData / RegionCodecThe same region as values a database can store. The module never touches a database itself.

Shapes

Cuboid.blocks(0, 64, 0, 15, 79, 15);    // an arena: inclusive block corners, so 16 blocks a side
Cuboid.block(100, 70, 100);              // one block
new UnboundedYRectangle(0, 0, 64, 64);   // a claim, bedrock to sky
new Sphere(0, 64, 0, 16);
new HorizontalCylinder(0, 0, 100);       // round, no floor or ceiling
new Cylinder(0, 0, 12, 40, 80);          // round, between y 40 and 80
Polygon.prism(vertices, 60, 90);         // any outline, between two heights
Polygon.columns(vertices);               // any outline, every y

A Polygon is a polygon of blocks: a column belongs to it when the centre of the column is inside the outline or on it, which is why Polygon.Vertex.block(x, z) puts a vertex in the middle of a block. The outline may cross itself (even-odd rule), so a freehand selection is stored exactly as drawn. Up to 1024 vertices.

Shapes are never mutated. A zone that shrinks is a new snapshot passed to replace.

Priority and policies

Regions.at(...) returns the regions at a point highest priority first. Resolving a policy walks that order and takes the first region that declares it; a region that is silent on a key is skipped, and nobody declaring it means the key's own default.

world_rules   priority 0    pvp: true
claim         priority 10   build: false
mine          priority 20   pvp: false

Standing in all three: pvp is false (the mine), build is false (the claim, which the mine does not mask), interact is true (nobody declared it).

PolicyResolution carries the value and the region that supplied it (source(), null for the default). Policies are open, typed, namespaced keys, so a plugin can add its own with PolicyKey.of(id, type, default).

The common policies

CommonRegionPolicies, under the exylia namespace:

KeyTypeDefault
pvp, build, break, interactbooleantrue
entry, exit, item_drop, item_pickup, fall_damagebooleantrue
player_build_only, allowed_blocks_only, breakable_blocks_onlybooleanfalse
temporary_blocks, re_give_blocks, region_members_onlybooleanfalse
temporary_blocks_secondsinteger0 (never removed)
allowed_blocks, breakable_blocksMaterialSetempty

Queries

Regions.at(location);                 // every plugin's regions here
Regions.at(worldId, x, y, z);         // without building a Location
Regions.resolve(location, key);       // one policy, over everybody's regions
Regions.get(id);
 
regions.at(location);                 // only this plugin's
regions.resolve(location, key);       // only this plugin's answer

Global by default, because a claim has to see the arena it is inside. Queries are safe from any thread: the index is immutable and swapped in one assignment, so a lookup never sees a half-written region. It is keyed by world UUID, so a world unloaded and loaded again keeps its regions.

Boxes turns a snapshot into Bukkit terms: min / max (inclusive block corners), world, contains(region, location), volume, loaded (every chunk of the box loaded, checked without loading any) and playersInside.

Registering

regions.register(snapshot);    // fails if the id already exists
regions.replace(snapshot);     // fails if it does not
regions.unregister("arena");
regions.replaceAll(all);       // this plugin's whole set, atomically

replaceAll is what a reload calls: it replaces only the calling plugin's regions and leaves every other plugin's alone. A plugin can only touch regions it owns, and disabling it releases its regions, selections, outlines and enforcement.

Entering and leaving

@EventHandler
public void onRegionChange(PlayerRegionChangeEvent event) {
    if (!event.involves(regions)) return;
    for (RegionSnapshot left : event.exited(regions)) { ... }
    for (RegionSnapshot entered : event.entered(regions)) { ... }
}

One event per change, after it is committed, on the thread that owns the player. It is not cancellable. Pass your own PluginRegions: the unfiltered exited() and entered() carry every plugin's regions, and acting on them is how a game eliminates somebody for leaving a region it does not own.

CauseWhen
MOVEcrossed into another block
TELEPORTarrived somewhere else, portals and respawns included
WORLD_CHANGEchanged world
SYNCa poll found them somewhere no event reported (Folia teleports, packet moves, vehicles)
REGISTER, REPLACE, UNREGISTER, RELEASEa region changed under a player standing still

Every tracked player is polled four times a second, so anything the events miss is corrected within a quarter of a second. Joining inside a region does not fire an enter, and quitting does not fire an exit; check regions.at(...) on join if you need to.

Enforcing the policies

Declaring a policy refuses nothing by itself. A plugin that wants the common ones applied asks once:

regions.enforce();   // everyone except players in creative mode
 
// or choose who the rules hold, per region
regions.enforce((player, region) -> sessions.contains(player.getUniqueId()));

It is opt-in and owner-scoped: only this plugin's regions are judged, and another plugin's region, whatever its priority, neither enforces nor relaxes them. The audience is asked per region, runs inside the event (a region thread on Folia), and must be cheap.

PolicyRefuses
breakbreaking a block, filling a bucket from one
buildplacing a block (both halves of a door or bed), emptying a bucket
breakable_blocks_only + breakable_blockswhen on, only the listed materials can be broken
allowed_blocks_only + allowed_blockswhen on, only the listed materials can be placed; a bucket counts as its item
player_build_onlybreaking a block no player placed
interactusing clicked blocks, pressure plates and farmland, right-clicking entities that are not players
pvpa player hurting a player directly, or through arrows, TNT, crystals, harmful potions or tamed pets; checked at both the victim and the attacker
fall_damage, item_drop, item_pickupwhat the name says, at the player's position

Refusals happen at HIGH, so your own listener at HIGHEST can overrule one. entry, exit and region_members_only are not enforced, nor are explosions. stopEnforcing() ends it.

Who placed a block

In regions that declare player_build_only or temporary_blocks, the library records which blocks a player put there (placed blocks, poured fluids, blocks that formed from them, blocks that fell and landed). A plugin that does not use enforce asks the record itself:

if (regions.resolve(location, CommonRegionPolicies.PLAYER_BUILD_ONLY).value()
        && !regions.placedByPlayer(event.getBlock())) {
    event.setCancelled(true);
}

Temporary blocks

Always applied, with or without enforce: a block placed in a region declaring temporary_blocks disappears after temporary_blocks_seconds, and goes back to the player who placed it when the region also declares re_give_blocks.

PolicySet.of(CommonRegionPolicies.TEMPORARY_BLOCKS, true)
        .with(CommonRegionPolicies.TEMPORARY_BLOCKS_SECONDS, 10)
        .with(CommonRegionPolicies.RE_GIVE_BLOCKS, true)

The policy editor

One screen for editing a region's flags, a PolicySet in and the edited PolicySet out:

regions.policyEditor(arena.policies())
        .title("{primary}&lARENA FLAGS")
        .keys(CommonRegionPolicies.PVP, CommonRegionPolicies.BUILD, CommonRegionPolicies.BREAK)
        .defaults(ArenaDefaults.POLICIES)
        .locked(CommonRegionPolicies.PVP, "The game switches this by itself")
        .onSave(edited -> arenas.save(arena.withPolicies(edited)))
        .onCancel(() -> ArenaMenu.open(player))
        .open(player);
  • Boolean rows cycle Default → Allow → Deny. A right click puts any unlocked row back to Default.
  • MaterialSet rows open a list editor with the material picker; integer rows ask for a number.
  • Without keys(...) it shows every boolean common policy; up to 28 rows. Your own keys need describe(key, icon, name, description).
  • Nothing is written until save. Closing, leaving or the plugin disabling all end in onCancel.

Clearing a region

regions.get("arena").ifPresent(regions::clearEntities);
int removed = regions.clearEntities(arena, entity -> entity instanceof Monster);

By default it removes what a round leaves behind: dropped items, experience orbs, projectiles, minecarts, end crystals and fireworks. Armour stands, frames, paintings and mobs stay; players are never removed. It reads the world, so run it on the region's thread.

Selecting

regions.beginSelection(player).result()
        .thenAccept(result -> save(result.shape()));

That hands the player a golden axe in their main hand, draws the shape as they pick it, and answers only after they confirm. The tool is taken back however the selection ends. One selection per player across the whole server.

GestureBoxPolygonFreehandCircle / sphere
Left clickfirst corneradds a pointpen down / upcentre
Right clicksecond cornerremoves the last pointremoves the last pointa block on the edge
Shift + left clickconfirmconfirm (3+ points)confirm (3+ points)confirm
Shift + right clicknext shape on offer

With the freehand pen down, the block the player looks at becomes a point every two blocks; the pen lifts itself at 256 points.

SelectionOptions mine = SelectionOptions.builder()
        .modes(SelectionMode.CUBOID, SelectionMode.CIRCLE, SelectionMode.SPHERE)
        .build();
 
SelectionOptions claim = SelectionOptions.builder(SelectionHeight.FULL)  // golden hoe, every y
        .virtualSelector(true)
        .build();
Moderesult.shape()
CUBOIDCuboid, or UnboundedYRectangle at full height
POLYGON, FREEHANDPolygon, between the clicked heights or every y at full height
CIRCLECylinder, or HorizontalCylinder at full height
SPHERESphere; not allowed at full height

A plugin that only stores boxes keeps reading result.cuboid(): for other shapes it is the box around them.

Full height (SelectionHeight.FULL) is for claims: the outline is drawn at the player's height with a post at each corner, and the feedback counts columns instead of blocks.

Virtual selector puts the tool on the client only, drawn over the held slot by packets. Nothing can be dropped, stored or copied out of it, and throwing it away cancels the selection. Needs PacketEvents: without it beginSelection throws UnsupportedOperationException.

Options

BuilderDefault
selectorMaterialGOLDEN_AXE (GOLDEN_HOE with builder(FULL))
giveSelectortrue
requireConfirmationtrue (outlines always wait for it)
feedbacktrue: coordinates, size and a standing action bar
previewLines() / previewParticle(name)lines; previewParticle(null) draws nothing
modesCUBOID
selectorName, selectorLore{primary}&lREGION SELECTOR and the gesture lore
cancelInteractions, requireSameWorldtrue

session.confirm() accepts from a button of your own; cancelSelection(player) ends it. The feedback lines are the library's own, in its messages.yml under selection (placeholders %x%, %y%, %z%, %blocks%, %points%, %radius%, %mode%, %selector%).

Outlines

regions.visualize(player, "arena");
 
VisualizationOptions zone = VisualizationOptions.builder()
        .lineColor(0xA33B53)
        .untilClosed()
        .build();
handles.add(regions.visualize(player, "zone", zone));

Outlines, live selections included, are real lines: thin glowing display bars sent only to the viewer, visible through walls, moved only when the shape changes. particleName("WAX_ON") draws particles instead, and a server without PacketEvents falls back to particles (END_ROD) by itself. Shapes with no ceiling are drawn at the viewer's height.

SettingDefault
lineColorthe palette's primary
durationTicks200; untilClosed() lasts until the handle is closed, the viewer leaves, the region goes or the plugin disables
periodTicks10
viewDistance48 blocks, measured to the region's nearest edge

The region is looked up again every frame, so replacing a shrinking zone updates every open outline.

Storing regions

RegionData data = RegionCodec.encode(snapshot, myPolicyKeys);
RegionSnapshot back = RegionCodec.decode(data, myPolicyKeys);

Decoding a policy you did not pass in fails instead of being dropped. The one exception is a material name inside a block list that no longer exists, which is skipped and logged.

WorldGuard

For WorldGuard's own regions there is WorldGuardRegions (isIn(player, "spawn"), at(location), ids(world)), with no compile-time dependency: on a server without WorldGuard nothing is inside any region. The flags the library registers are on Displays and NPCs.

Something missing on this page? Tell us on Discord