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

Repository files navigation

ChunkScope

Adaptive per-player view and simulation distance for Paper and Folia.

Build

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.


How it decides

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.

It reacts to the packet, not to a timer

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.)

While a player is not playing

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.


Permissions

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

Why there is no chunkscope.unlimited.simulation

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.


Commands

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.


Detecting a player who is not playing

This is the part worth reading carefully, because a lot of plugins promise more here than is technically possible.

AFK, and anti-AFK macros

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.

Minimized window and switching away from the game

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 under packet-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 (or ChunkScope on 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.


One jar, Java 8 through Java 25

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.

Version support

One jar. It picks a backend at startup and tells you which one in the console.

Modern Paper and Folia (per-player distance API)

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.

1.8.8 and other versions without that API

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.

ViaVersion, ViaBackwards, ViaRewind

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.


Performance

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.


When something goes wrong

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.


Installation

  1. Download the latest ChunkScope-*.jar from the releases page.
  2. Drop it in plugins/.
  3. Start the server. config.yml is generated with every option documented inline.
  4. Optionally grant chunkscope.unlimited.view to whoever should bypass the server cap.

No dependencies. Nothing to configure to get the default behaviour.

Building from source

./gradlew build

The 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.

Configuration

See src/main/resources/config.yml - every option is documented in the file itself, including the trade-offs of the experimental ones.

License

MIT

About

Adaptive per-player view and simulation distance for Paper and Folia. Clamps every player's rendered and ticked chunks to their own client render distance, never above the server limit, with LuckPerms overrides and automatic reduction while a player is not actually playing. One jar, from 1.8.8 to 26.2.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages