Adaptive per-player view and simulation distance for Paper and Folia.
A server usually 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. One jar, from 1.8.8 to 26.2, on Paper and Folia, with no dependencies.
Every Minecraft client sends the server a settings packet: once when the player joins, and again every time the player touches the video settings. ChunkScope intercepts that packet, takes the render distance out of it, and works out two numbers:
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
The 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.
Nothing is applied until a value genuinely changes. On join there is no previous value, so the first report always applies; after that, a player whose settings and activity are stable costs nothing.
A render distance change is applied the instant the settings packet arrives - through Paper's client options event on modern servers, and by reading the packet straight off the connection on 1.8.8. Nothing about the view or simulation distance is polled on a schedule.
There is one periodic task, and it exists for the only thing no packet can announce: "this player has
now been idle long enough to count as AFK". That is a statement about the passage of time, so
something has to look at the clock. It is configured under activity-check, and if you disable every
trigger it is not scheduled at all.
(One exception: on server software that does not fire the client options event - you will see code
CS-02 in the console - the client value has to be polled, and that same task does it. No current
Paper or Folia takes this path.)
The view and simulation distance each drop by a configurable amount (2 chunks by default, separately tunable), and are restored the moment the player does something real.
| Permission | Default | Effect |
|---|---|---|
chunkscope.unlimited.view |
false |
Ignore the server view-distance cap and use the player's own render distance directly. The simulation distance is still capped by the server and by the view distance. |
chunkscope.command |
op |
/chunkscope info, /chunkscope status |
chunkscope.reload |
op |
/chunkscope reload |
These are ordinary Bukkit permission nodes, so LuckPerms manages them with no extra setup:
lp group vip permission set chunkscope.unlimited.view true
lp user Steve permission set chunkscope.unlimited.view true
The Minecraft client never sends its simulation distance to the server. In any version.
The serverbound Client Information packet carries the locale, the render distance, chat mode, chat colours, skin parts, main hand, text filtering, server listings and particle status - and nothing else. The "Simulation Distance" slider in the video settings only affects the client's own single-player world; it is never transmitted.
Since there is no player-provided simulation distance to unlock, an "unlimited simulation distance" permission would have nothing to do, so it is deliberately not registered. The simulation distance is derived from the server value and the player's render distance instead, exactly as shown above.
| Command | What it does |
|---|---|
/chunkscope status |
Backend in use, server caps, whether simulation control is active |
/chunkscope info [player] |
The client value, the server caps, what is currently applied, and the activity verdict with its reason |
/chunkscope reload |
Re-read config.yml and re-apply to everyone |
Aliases: /cs, /cscope.
This is the part worth reading carefully, because a lot of plugins promise more here than is technically possible.
A plain "did the player move?" check is useless: the players most worth reducing are exactly the ones running a macro to look busy. So ChunkScope splits what a player does into two kinds of signal.
Hard signals prove a human is present and immediately restore full distances: breaking or placing a block, clicking in an inventory, opening a container, crafting, fishing, using or consuming an item, filling or emptying a bucket, dropping an item, chatting, running a command, dealing or taking damage.
Soft signals - movement and camera rotation - are not trusted on their own. They are recorded and analysed, and a player past the AFK timeout whose only signals look automated stays reduced:
- Confined movement - the whole recent path fits inside a 4-block box: jumping or shuffling in place.
- Mechanical rotation - the camera turns by a near-identical amount every sample. A person's aim wanders; a spinner macro does not.
- Short loops - the entire recent path only ever touches a handful of distinct blocks, which is the classic walk-in-a-circle pattern.
- Auto-clickers - arm swings arriving with almost no jitter, which no human hand produces.
Repeating the exact same chat message does not count as chatting, which is what a chat macro does and a person almost never does.
Every one of these is deliberately conservative: when in doubt, the player is treated as playing. A false negative costs nothing; a false positive costs a couple of chunks of render distance until the player's next real action.
Set detection.anti-afk-macro: false to go back to the naive behaviour where any movement counts.
Vanilla Minecraft sends no packet when its window is minimized, loses focus, or sits on the pause menu. The client keeps sending position updates exactly as if the player were playing. No server-side plugin can detect these reliably - any plugin claiming otherwise is guessing.
ChunkScope offers the two things that are actually possible, both off by default:
-
detection.experimental-packet-rate- some clients and launchers throttle their tick loop when backgrounded, which shows up as a sustained collapse in the rate of position and look updates. This is the closest a server-side plugin can get. It is experimental and can produce false positives: a player standing still on purpose, on a bad connection, or on a modded client that sends fewer updates, looks the same. The consequence of a false positive is bounded - the player loses the configured couple of chunks until their next real action - which is why it is offered at all, and why it ships disabled. Tune it underpacket-rate. -
detection.client-mod-bridge- a plugin message channel on which a companion client mod can report focus, minimize and pause state authoritatively. That is the only fully reliable way to know. No such mod exists yet; the channel is wired, documented and ready so that servers can switch it on the day one does. Enabling it with no mod installed changes nothing.Wire format, deliberately trivial so any mod can speak it - a single ASCII line on channel
chunkscope:state(orChunkScopeon servers that reject namespaced names):focused=true;paused=false
With both of those disabled, the minimized and screen-switch triggers have no independent effect
and only the AFK trigger ever fires. The config says so too.
ChunkScope is compiled to Java 8 bytecode, and that is the only copy in the jar. A Java 8 JVM running a 1.8.8 server loads it, and so does the Java 25 JVM under Folia 26.2 — a newer JVM reads older bytecode, never the other way round. Everything newer than the 1.8.8 API is reached by reflection at runtime, which is what lets one set of class files serve every version.
There is no second compiled copy of anything. 1.0.0 shipped a multi-release jar with the modern
backend compiled twice — once for Java 8 in the root, once for Java 21 under META-INF/versions/21 —
and 1.0.1 removes it, because it bought nothing measurable: the bytecode level does not make code
faster. The JIT compiles Java 8 and Java 21 bytecode to the same machine code, and the gains from a
newer JVM come from the JVM itself. All the duplicate 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.
CI verifies on every push that no versioned directory has crept back in and that every class in the jar is major version 52. That check exists because breaking it produces no compile error — a class file above 52 simply refuses to load on Java 8, and the plugin would fail to enable on 1.8.8 with nothing but a Bukkit log line to explain it.
One jar. It picks a backend at startup and tells you which one in the console.
Uses Paper's own PlayerClientOptionsChangeEvent - the server handing us the intercepted settings
packet - and applies the result through setViewDistance, setSendViewDistance and
setSimulationDistance. On Folia the call is posted to the region thread that owns the player, as
Folia requires. Both view and simulation distance are fully managed.
Installs one Netty handler per player which reads the view distance out of the client settings packet and drops outgoing chunk packets for chunks beyond that radius.
What this does and does not do, stated plainly: it removes the cost of serialising, compressing
and pushing chunk data down each player's connection - the expensive, per-player part - and it cuts
what the client has to hold and render. It cannot stop the server from ticking chunks it would tick
anyway, because these versions have no per-player chunk tracking to configure. These versions have
no simulation distance at all, so the simulation half of the plugin is inactive there and
/chunkscope status says so.
If ChunkScope cannot bind to the server internals it says so once in the console and goes inert - it will still read client values, but it will not touch the server. It never risks breaking your world to save a chunk.
Not required, and not a problem either. ChunkScope reads the settings packet at the server's own protocol level, so a modern client connecting to a 1.8.8 backend through a Velocity proxy has its packet translated down by Via with the render distance intact, and ChunkScope reads it as usual. It works with Via and without it.
The plugin is built so that the common case costs as close to nothing as possible.
Nothing polls. A render distance change is applied the instant the client's settings packet arrives. The only periodic task is the AFK check, because no packet can announce that a player has been idle for sixty seconds — and it skips any player whose verdict provably cannot have changed yet. A player who acted three seconds ago cannot be AFK for another fifty-seven, so they are not re-examined at all: no permission lookup, no solve, no map lookups. On a server where most players are active, that task is close to free.
Listeners are only registered when they will be used. PlayerMoveEvent fires around twenty times
a second per player; its position handler is registered only when the 1.8.8 backend is in use, and
the activity tracker only while some reduction trigger is enabled.
The packet path allocates nothing. On 1.8.8 the plugin sits in every player's Netty pipeline, so
its packet classification runs on every packet in both directions. It is resolved once per packet
class through a ClassValue and cached; the common case — a packet that is not a chunk packet —
costs one lookup and no allocation.
Memory is allocated when it is needed, not on join. The movement history used for macro detection is around 2.5 KB per player and is only ever read once that player is past the AFK timeout. It is allocated on first use and released again on any real action.
Nothing is applied until a value genuinely changes, and deciding that nothing changed does not allocate.
ChunkScope runs on server software it cannot compile against - Paper, Folia, and any fork of either, across every version since 1.8.8. When a capability is missing or renamed it never fails silently and never takes the server down with it: it degrades, and says exactly what happened with a stable code.
[CS-03] Could not reach this server internals - the failing step is named in the exception
below (CraftPlayer.getHandle, EntityPlayer.playerConnection, ...). ChunkScope will keep
reading client render distances but will apply nothing on this server, which leaves it
exactly as it was. This is the message to report if you are running a fork. If this is
unexpected, please report it at ... with this code (CS-03), the line above, and your server
version: Paper 1.8.8-R0.1 ..., Java 8 (OpenJDK 64-Bit Server VM).
| Code | Meaning | What the plugin does instead |
|---|---|---|
CS-01 |
No per-player view distance API | Uses the packet-filtering backend (normal on 1.8.8) |
CS-02 |
No client options event | Polls the client value on the activity interval |
CS-03 |
Server internals unreachable | Reads client values, applies nothing - server untouched |
CS-04 |
Packet handler could not be installed | That one player keeps the server default |
CS-05 |
Folia scheduler API mismatch | Falls back to the Bukkit scheduler |
CS-06 |
A distance setter was rejected | Leaves that distance as the server had it |
CS-07 |
Plugin message channel rejected | Client mod bridge stays inactive |
CS-08 |
Chunk resync failed | Distant chunks may need a relog to appear |
CS-09 |
No simulation distance on this version | Manages the view distance only (normal pre-1.18) |
CS-10 |
A task could not be scheduled | Falls back to the Bukkit scheduler |
Each code is logged once, not once per player. /chunkscope status lists everything that degraded
on your server along with the server and Java version, so a bug report is one copy-paste. If you are
running a fork and hit CS-03, that message names the exact reflection step that failed - please
open an issue with it.
Set debug: true in the config to log every decision: the values read from the client, the values
computed, and why a player was considered active or not.
- Download the latest
ChunkScope-*.jarfrom the releases page. - Drop it in
plugins/. - Start the server.
config.ymlis generated with every option documented inline. - Optionally grant
chunkscope.unlimited.viewto whoever should bypass the server cap.
No dependencies. Nothing to configure to get the default behaviour.
./gradlew buildThe jar lands in build/libs/ChunkScope-<version>.jar. Requires a JDK 17 or newer to build; the
output is Java 8 bytecode via --release 8, so it loads on 1.8.8 servers running a Java 8 JVM.
See src/main/resources/config.yml - every option is documented in
the file itself, including the trade-offs of the experimental ones.