Skip to content
This repository was archived by the owner on Aug 20, 2026. It is now read-only.

Releases: adrielorihuela/ChunkScope

ChunkScope 1.1.0 - spawn chunk control

Pre-release

Choose a tag to compare

@adrielorihuela adrielorihuela released this 13 Aug 03:00

This version lives on the feature/spawn-chunks branch, not on main.
It is marked as a pre-release so that v1.0.1 is the recommended download. 1.1.0 adds spawn chunk control on top of 1.0.0, but it does not include the optimisations in 1.0.1 and still carries the duplicated multi-release bytecode.

---Adds spawn chunk control: a second saving, independent of everything in 1.0.0, and this one has nothing to do with players.

Before Minecraft 1.21.9, a server permanently kept a block of chunks around each world's spawn point loaded and ticking — forever, whether or not anybody was there. Every entity, hopper and redstone clock inside that block was processed on every tick of an otherwise empty world. ChunkScope now frees them by default.

Configuration

spawn-chunks:
  keep-loaded: false          # false frees them; true holds `radius` chunks around each spawn
  radius: 2                   # (2 * radius + 1)^2 chunks, so 2 = 5x5 = 25, 10 = 21x21 = 441
  keep-loaded-in-worlds: []   # worlds that keep theirs even when keep-loaded is false

Fully reversible from the config: set keep-loaded: true and /chunkscope reload.

Read this before you measure it

Minecraft 1.21.9 removed spawn chunks from the game. On Paper and Folia 26.x, setKeepSpawnInMemory is documented as "No longer functional since 1.21.9, the vanilla server does not have the concept of spawn chunks anymore", and the spawnChunkRadius game rule no longer exists. A dimension is now only kept active by player activity, force-loaded chunks, active portals or ender pearls in flight.

So on a modern server there is nothing left to free, and keep-loaded: false correctly changes nothing. ChunkScope reports that with code CS-11 rather than letting it look like a silent failure. The real saving is on 1.8.8 through 1.21.8.

Setting keep-loaded: true still works everywhere, 26.x included, because ChunkScope holds the radius itself with plugin chunk tickets.

What breaks if you free them

Anything at a world's spawn that relied on always being loaded stops running while no player is nearby — AFK farms, mob spawners, redstone clocks, item sorters, portal contraptions. That is exactly the load being reclaimed, but it is a behaviour change your players may notice. If one world needs its spawn kept alive, name it under keep-loaded-in-worlds rather than turning the feature off.

How it is applied

Whichever mechanism the running server actually supports, best first. /chunkscope status reports which one took effect.

Server version Mechanism Radius honoured
1.20.5 – 1.21.8 spawnChunkRadius game rule yes, exactly
up to 1.20.4 setKeepSpawnInMemory, plus chunk tickets for a custom radius yes on 1.13.2+
1.13.2 and up plugin chunk tickets — what makes true work on 1.21.9+ yes, exactly

Applied on startup, on world load (so worlds created later by a multiverse-style plugin are covered) and on /chunkscope reload. On Folia, world level changes go through the global region scheduler and chunk tickets through the region owning each chunk, as Folia requires.

Also in this release

  • /chunkscope status now reports the spawn chunk state and the mechanism in use.
  • Two new diagnostic codes: CS-11 (nothing to control on this version — a note, not a warning) and CS-12 (a change failed for a world).
  • Seven new unit tests covering the config parsing, including the per-world exception list and radius clamping.
  • The CI multi-release check no longer hardcodes the jar name, so a version bump cannot silently disable the check that protects 1.8.8 support.

Unchanged from 1.0.0

Still one jar, still no dependencies, still 1.8.8 to 26.2 on Paper and Folia. The multi-release layout is intact: Java 8 classes in the jar root, Java 21 classes in META-INF/versions/21. All the per-player view and simulation distance behaviour is untouched.

Full documentation in the README.

ChunkScope 1.0.1 - one code path, and faster

Choose a tag to compare

@adrielorihuela adrielorihuela released this 13 Aug 21:37

One jar, one code path, and a faster one. Same features as 1.0.0 — nothing was removed from what the plugin does.

The duplicate is gone

1.0.0 shipped a multi-release jar: the modern backend was compiled twice, once to Java 8 in the jar root and once to Java 21 under META-INF/versions/21, with the JVM picking whichever it could use. 1.0.1 removes that.

It bought nothing measurable, and it is worth being blunt about why: the bytecode level does not make code faster. The JIT compiles Java 8 and Java 21 bytecode to the same machine code; the gains from a newer JVM come from the JVM itself, not from how the class file was stamped. All the second copy removed was a handful of reflective calls on a path that runs a few times per player per session — in exchange for a class that had to be kept behaviourally identical to its twin forever.

Compatibility is unchanged: Java 8 bytecode loads on a Java 8 JVM running 1.8.8 and on the Java 25 JVM under Folia 26.2. A newer JVM reads older bytecode; the reverse is never true. CI now verifies on every push that no versioned directory has crept back and that every class in the jar is major version 52 — a check worth having, because breaking it produces no compile error, just a plugin that silently refuses to load on 1.8.8.

What the removal was traded for

Six optimisations, in order of how much they actually matter.

Packet classification no longer allocates. This is the big one. On 1.8.8 the plugin sits in every player's Netty pipeline, so its packet check ran on every packet in both directions — tens of thousands per second on a busy server. It called getClass().getSimpleName(), which computes and allocates a new String on every call, to answer a question whose result never changes for a given class. It is now resolved once per packet class through a ClassValue and cached; a packet that is not a chunk packet leaves the write path having cost one lookup and zero allocations.

The periodic check no longer calls into the backend. It asked for each player's client render distance every interval, which on the modern backend is a reflective call. It never needed to — the settings event already pushes that value — so it is cached and read from the cache.

Players who cannot have changed are skipped entirely. A player who performed a real action three seconds ago cannot be AFK for another fifty-seven. The tracker now reports when a verdict could next change, and until then that player costs nothing: no permission lookup, no solve, no map lookups. On a server where most players are active, the periodic task is close to free. (The window is not used when the experimental packet-rate heuristic or the client mod bridge is on, since either can flip a verdict at any moment.)

Deciding that nothing changed no longer allocates. The solver gained a packed form, so the comparison happens on an int and the object is only built on a real change.

Movement history is allocated when it is needed, not on join. Around 2.5 KB per player of sample arrays, written twenty times a second and only ever read once that player is past the AFK timeout with macro detection on — which for most players never happens. Now allocated on first use and released on any real action.

Two smaller ones. EntityDamageByEntityEvent extends EntityDamageEvent, so registering handlers for both meant every entity hurt anywhere on the server — mobs included — was dispatched twice to do one job; one handler now covers both sides of a fight. And the move handler read event.getTo() five times per event, twenty times a second per player.

Per-player state also collapsed from two concurrent maps into one.

About the file size

Being straight about this: the jar is 64,978 bytes, against 64,731 for 1.0.0 — 247 bytes larger. Removing the duplicated class saved 4,680 bytes and the optimisation code added 4,927 back. The duplication you objected to is genuinely gone, but the result is the same size as 1.0.0 rather than smaller. It is considerably smaller than 1.1.0's 73,042.

Not in this release

Spawn chunk control from 1.1.0 is not here. It lives on the feature/spawn-chunks branch, and v1.1.0 is now marked as a pre-release.

Verification

./gradlew clean build green with 23 unit tests, including new coverage for the packet classifier (which pins the ordering bug worth guarding: PacketPlayOutMapChunk is a prefix of PacketPlayOutMapChunkBulk, and mistaking a bulk packet for a single chunk would drop a whole batch) and for the config defaults.

Jar inspected: no META-INF/versions, no Multi-Release attribute, all 27 classes major version 52.

Not verified on a live server — as with 1.0.0, that part is yours. /chunkscope status reports the backend and any degraded capabilities.

Full documentation in the README.

ChunkScope 1.0.0

Choose a tag to compare

@adrielorihuela adrielorihuela released this 12 Aug 23:31

Adaptive per-player view and simulation distance for Paper and Folia. One jar, from 1.8.8 to 26.2, no dependencies.

A server renders and ticks chunks up to whatever view-distance says, for every player, regardless of what those players can actually see. A player whose client is set to 4 chunks still costs the server 10, 12 or 16 chunks of work and bandwidth. A player who walked away from the keyboard costs exactly as much as one who is playing.

ChunkScope reads the render distance each client reports, clamps that player's server-side view and simulation distance to it, and shrinks both further while the player is not actually playing.

What it does

Every Minecraft client sends a settings packet on join and on every video settings change. ChunkScope acts on that packet and works out:

view       = min(player render distance, server view-distance)
simulation = min(server simulation-distance, player render distance)
simulation = min(simulation, view)          <- never above the view distance, ever

That last line is unconditional. Simulating chunks a player cannot see is exactly the waste this plugin exists to remove, so nothing — not even a permission — lets the simulation distance climb past the view distance.

It reacts to the packet, not to a timer. A render distance change is applied the instant the packet arrives. The only periodic task is the AFK check, and only because no packet can announce "this player has now been idle for sixty seconds".

Features

  • Per-player clamping to the client's own render distance, never above the server cap.
  • Reduction while a player is not playing — 2 chunks off each distance by default, configurable separately, restored the moment they do something real.
  • Anti-AFK macro detection — movement and camera rotation are analysed, not trusted. Confined movement, mechanically constant rotation, short walk loops and jitter-free auto-clickers all read as automated. Breaking blocks, inventories, crafting, fishing, chat, commands and damage are treated as proof a human is present.
  • LuckPerms-ready permissions — chunkscope.unlimited.view lifts the server cap for a player or group.
  • /chunkscope status | info [player] | reload with tab completion.
  • Diagnostic codes — every degraded capability is reported once with a stable code (CS-01…CS-10) plus your server and Java version, so a bug report is one copy-paste.
  • Fully documented config.yml, including the trade-offs of the experimental options.

One jar, Java 8 through Java 25

Ships as a multi-release jar, so a single file gives every server the best code path it can run:

Contents Compiled for Loaded by
jar root Java 8 (class file 52) Java 8 JVMs — a classic 1.8.8 server
META-INF/versions/21/ Java 21 (class file 65) Java 21+ JVMs — modern Paper and Folia

Same class, same public API, same behaviour; the versioned one is compiled against the real Paper API instead of reaching it by reflection. You install one file. There is nothing to choose.

Version support

  • Modern Paper and Folia — uses PlayerClientOptionsChangeEvent and setViewDistance / setSendViewDistance / setSimulationDistance, applied on the player's own region thread as Folia requires. Both distances fully managed.
  • 1.8.8 and other versions without that API — a per-player Netty handler reads the view distance from the client settings packet and drops outgoing chunk packets beyond that radius, cutting the serialise/compress/send cost charged per player. These versions have no simulation distance at all, so that half is inactive and /chunkscope status says so.
  • ViaVersion / ViaBackwards / ViaRewind — not required, not a problem. ChunkScope reads the packet at the server's own protocol level, so a modern client reaching a 1.8.8 backend through a proxy works either way.

If ChunkScope cannot bind to a server's internals it logs CS-03 and goes inert — it never risks breaking a world to save a chunk.

Installation

  1. Drop ChunkScope-1.0.0.jar into plugins/.
  2. Start the server. config.yml is generated with every option documented inline.
  3. Optionally grant chunkscope.unlimited.view to whoever should bypass the server cap.

No dependencies, and nothing to configure for the default behaviour.

Two things worth knowing

  • The client never sends its simulation distance, in any Minecraft version. The Client Information packet has no such field; the video-settings slider only affects the client's own singleplayer world. The simulation distance is therefore derived, and there is deliberately no "unlimited simulation distance" permission.
  • Vanilla sends nothing when the window is minimized or unfocused. The minimized and screen-switch triggers are honoured through an experimental packet-rate heuristic (off by default, false positives documented) or a plugin-message bridge ready for a future client mod. No server-side plugin can detect these reliably.

Full documentation in the README.