All Mods Resource Packs Data Packs Shaders Modpacks Plugins
ViewDistanceTweaks

ViewDistanceTweaks

by Unknown • Downloads: 0 • Followers: 0
Description
Gallery
Changelog
Versions

Release Downloads Servers License

Built with Java 21 Available for Paper Available for Purpur Available for Spigot Available for Bukkit Available for Folia

Watch us on Youtube Available on Codeberg

bStats

ViewDistanceTweaks

Sets view and simulation distance per world, on its own, based on what the server can handle at that moment.


The number in server.properties has to be safe enough for your worst minute, which means it's too low for every other minute. Ten people on at 3am and you're saving distance nobody asked you to save. Sixty people at an event and that same number is why the TPS meter is on the floor.

This moves it for you. It watches MSPT and how many chunks are actually loaded, gives distance back while there's headroom, and takes it away before players notice. Each world has its own floor and ceiling, so the Nether can sit at 6 while your main world runs at 16.

Drop the jar in and restart. The defaults do something sensible without any setup.


Requirements

  • Paper 1.21.x / 26.x or a fork (Purpur, Pufferfish, Leaves)
  • Folia works, tested on 1.21.11
  • PlaceholderAPI - optional, only if you want the placeholders
  • EssentialsX - optional, only to read AFK state if you turn AFK throttling on

It loads on Spigot and Bukkit, but a few parts need Paper's API and switch themselves off with a line in the console if it isn't there: per-player distances, pre-generation and pre-loading. The distance engine itself still works, it just falls back to a coarser tick-time reading.


How it decides

MSPT is sampled every tick. Every ticks-per-check (30 seconds by default) each world is looked at once and the answer is up, down, or leave it alone.

Four modes:

  • proactive - you give it a chunk budget (how many ticking and non-ticking chunks the server should carry in total) and it solves for the distance that fits
  • reactive - ignores chunk counts, just watches MSPT against two thresholds
  • mixed - runs both and takes whichever is more pessimistic. Right answer for most servers, and the default
  • off - never touches view or simulation distance. For servers that only want the pre-generation and pre-loading parts

On top of the mode itself:

It won't repeat an increase that already hurt. A linear regression over the last 30 minutes of chunk-count/MSPT pairs predicts what the next step up would cost. If that lands above the decrease threshold, the increase doesn't happen. That's what breaks the usual raise-lag-drop-raise loop.

It watches the spread, not just the average. A server can average a comfortable 30ms while single ticks bounce between 8 and 55, and nobody enjoys playing on that. Standard deviation across Paper's last 100 tick samples catches it, so above variance-threshold the server counts as overloaded even though the average looked fine.

Down can be faster than up. max-increase-step and max-decrease-step are separate. 1 and 4 is a common pair: climb one notch at a time, bail out in one go. revert-increase-if-overloaded also re-checks right after an increase and rolls it straight back if it hurt.

Simulation first, view second. With prioritize-simulation-distance on, view distance isn't touched until simulation distance is already at the world minimum. Simulation is the one that costs TPS, view is the one players see, so spend the cheap one first.

It can tell your worlds apart. The server publishes one MSPT figure for everything, so a single world melting down drags every other world's distance down with it - the quiet ones get punished for their neighbour. With per-world-mspt on, the tick is split between worlds and each is judged by the part it actually causes. Off by default; see below.


Per-world MSPT

One number for the whole server is the wrong shape for a per-world decision. If your nether farm is eating 30ms, the overworld didn't do anything wrong, and shrinking it doesn't help.

Turn on per-world-mspt.enabled and each world is judged by its own share of the tick instead:

fair share    = 1 / number of worlds with players
attribution   = this world's share / fair share      (clamped, 0.5x to 2.0x by default)
judged at     = server MSPT x attribution

So a world pulling exactly its weight is judged by the plain server figure, unchanged. A world pulling twice its weight is judged at twice the figure and shrinks first; a quiet one is judged at less and is left alone, or keeps growing while its neighbour gives ground.

Your thresholds keep meaning what they meant. This scales the figure the existing decrease-mspt-threshold is compared against - it does not replace it with a smaller per-world number. Nothing needs re-tuning, and on a single-world server the feature changes nothing at all.

Where the numbers come from depends on the platform:

  • Paper (and forks like Purpur) estimates. There's no per-world timing to read, so the tick is split by what each world made the server walk - entities, tickable block entities, loaded chunks. Reliable for ranking worlds; it will misjudge a world that's cheap by volume and expensive by content, like a few hundred hoppers feeding each other.
  • Folia measures region tick times directly, and that turns out to mean per-world MSPT does nothing there by default. A region that keeps up reports the tick rate itself, so every world reads the same and there is no split to act on; the figure only becomes informative once a region falls behind. Set prefer-exact-source: false to run the estimating proxy on Folia instead, which does tell worlds apart. A laggy region with no players near it isn't probed either way.

/vdt mspt shows the breakdown, which world is worst, and whether each figure is measured or estimated. /vdt simulate says so in its reason line when a world is being judged by an adjusted figure.

If the measurement is unavailable for any reason - still warming up, or the library failed to start - every world falls back to the plain server-wide figure, which is what the plugin did before this existed.


Placeholders

With PlaceholderAPI installed, everything the plugin knows is available on a scoreboard or in a chat format. The identifier is viewdistancetweaks - not vdt, which is the first thing everybody tries.

Server-wide

Placeholder Gives
%viewdistancetweaks_mspt% current MSPT, two decimals
%viewdistancetweaks_tps% current TPS, capped at 20
%viewdistancetweaks_mspt_stddev% how much recent ticks vary
%viewdistancetweaks_mode% the active adjustment mode
%viewdistancetweaks_enabled% whether the plugin is managing distances
%viewdistancetweaks_mspt_color% &a/&e/&c against your own thresholds
%viewdistancetweaks_tps_color% same idea for TPS
%viewdistancetweaks_mspt_stddev_color% same idea for the spread
%viewdistancetweaks_chunk_count% loaded chunks across every world
%viewdistancetweaks_ticking_chunk_count% ticking chunks across every world
%viewdistancetweaks_nonticking_chunk_count% non-ticking chunks across every world

Per world. Append the world name - %viewdistancetweaks_view_distance_world_nether%. Without a name, the server's first world is used.

Placeholder Gives
%viewdistancetweaks_sim_distance_<world>% current simulation distance
%viewdistancetweaks_view_distance_<world>% current view distance
%viewdistancetweaks_min_sim_distance_<world>% configured minimum
%viewdistancetweaks_max_sim_distance_<world>% configured maximum
%viewdistancetweaks_min_view_distance_<world>% configured minimum
%viewdistancetweaks_max_view_distance_<world>% configured maximum
%viewdistancetweaks_chunk_count_<world>% loaded chunks in that world
%viewdistancetweaks_ticking_chunk_count_<world>% ticking chunks in that world
%viewdistancetweaks_nonticking_chunk_count_<world>% non-ticking chunks in that world
%viewdistancetweaks_chunk_weight_<world>% configured chunk weight

Per-world MSPT. These need per-world-mspt switched on; without it they fall back to the server-wide figure or report that the feature is off, rather than failing.

Placeholder Gives
%viewdistancetweaks_worst_world% the world carrying the most of the tick
%viewdistancetweaks_worst_world_mspt% what that world costs, in ms
%viewdistancetweaks_world_mspt_<world>% that world's own contribution in ms
%viewdistancetweaks_world_mspt_share_<world>% its share of the tick, 0-100
%viewdistancetweaks_per_world_mspt_state% where the numbers come from, or why there are none

An unknown world reads as unknown rather than as a zero, so a typo in a scoreboard is visible instead of looking like a healthy server.


Safety nets

MSPT isn't the only thing worth reacting to. Everything here except the memory safeguard is off by default.

  • Memory safeguard - free heap under min-free-percent blocks any increase, whatever MSPT says. A server can idle at 20ms and still be one ring of chunks away from spending every tick in GC. On by default at 15%.
  • Chunk generation safeguard - counts chunks genuinely being generated, not ones read off disk. Past a per-second rate it stops increases and can clamp view distance until generation calms down. This is the someone-just-flew-an-elytra-into-fresh-terrain case.
  • AFK throttling - a player standing at spawn doesn't need 16 chunks. Their personal distances drop to the AFK values and what they free up goes back into the budget. Uses EssentialsX AFK state if it's installed so /afk counts immediately, otherwise Paper's idle timer.
  • Ping throttling - same idea, triggered by a bad connection instead of an idle one. A player whose ping stays above the threshold for a few checks in a row gets reduced personal distances too, so their packets stop queuing and everyone else's budget stops paying for it. If both throttles apply, the more restrictive distance wins.
  • Density scaling - twenty players at one farm load nearly the same chunks, but the proactive formula assumes twenty separate rings and starves the world for nothing. When enough players sit close together the world gets bonus distance, scaled by how much of its population is in that group.
  • Complexity weighting - an ocean chunk is free, a chunk with a 500-hopper sorter can eat a tick by itself, and (2d+1)² counts both as one. Samples a bounded number of chunks around players and derives an average multiplier, so a redstone-heavy world gets a smaller budget than one full of water.
  • Emergency relief - last resort. World is at minimum sim and view, MSPT is still over the line, nothing left to take. So it sheds thunderstorms and the daylight cycle instead. Both come back when MSPT recovers, on /vdt disable, and on shutdown. The world's original weather and gamerule state are remembered on the way in.
  • Client view distance cap - nobody sees further than their own client draws. If the highest setting among a world's players is 8, a server view distance of 16 pays for rings of chunks nobody receives. With client-view-distance-cap on, a world is capped at the highest reported client value plus a small headroom, never below its minimum. Off by default.
  • Conflict detection - if another plugin keeps overwriting a world's distances, the two would fight forever. After a few cycles of that, ViewDistanceTweaks stops managing that world, says so once in the console, and names the installed plugins that could be doing it. /vdt doctor shows it too. A manual /vdt viewdistance on that world takes it back.
  • Startup grace - holds a low distance for the first few minutes after a restart. Keeps a cold server from getting hit at max distance while it's still finding its feet.

Per-player distances

Two permission families set a distance for one player without touching the world:

viewdistancetweaks.fixed.<n>      view distance
viewdistancetweaks.simfixed.<n>   simulation distance

Values 2 to 32, highest wins if someone holds several, and taking the permission away hands them back to the world value without a relog. Both have to be switched on in the config first. They need different prefixes, otherwise one node would set both at once.

The view distance one is client-side and costs the server nothing. The simulation one genuinely does cost, so keep it for staff instead of selling it as a donor perk.

/vdt status --fixed shows who currently holds what, and whether it came from a permission, the AFK throttle or the ping throttle.


Presets and profiles

Two different things with annoyingly similar names, sorry about that.

/vdt preset <small-server|large-network|performance-first> writes into your config.yml. It's a starting point if you don't want to tune forty values by hand, not a final answer.

/vdt profile <name> lays a temporary overlay over whatever is configured, defined under profiles: in the config. Nothing is written to disk and normal puts it all back. It's there so you can move to event settings for two hours without editing config.yml in the middle of an event. max-view-distance-cap and max-simulation-distance-cap are hard ceilings applied after every other rule, so a cap of 8 really is 8.


Pre-generating terrain

Generating a chunk is the most expensive thing a server does, and the one everything else here can only react to. Building the terrain in advance removes the cause: what already exists gets read from disk, which is cheap, and the chunk-generation safeguard never has to trip.

Turn on idle-pregeneration and it runs whenever the last player logs off, pausing the moment somebody joins. Or drive it yourself:

/vdt pregen start world 500              centred on the world spawn
/vdt pregen start world 500 --here       centred on where you're standing
/vdt pregen schedule world 500 04:00     start daily at 4 AM server time, even with players on
/vdt pregen schedule cancel              drop the scheduled run
/vdt pregen status                       progress, rate and ETA
/vdt pregen stop                         pause for good, progress kept
/vdt pregen cancel --confirm             throw the progress away

schedule is for a maintenance window rather than an empty server: the START trigger switches from "whenever it's empty" to "at this clock time," but the run itself still answers to pause-above-mspt, min-free-memory-percent and pause-when-players-online once it's going, same as any other automatic run. It recurs daily until cancelled, and - like other command-driven state in this plugin - is not written to config.yml, so it needs re-arming after a restart.

Three things keep it from becoming the problem it solves. It throttles against the same rolling MSPT average the distance engine uses, so the two never disagree about whether the server is busy. It stops rather than slows when free heap runs low, because a slower path to an OutOfMemoryError is still an OutOfMemoryError. And it writes progress to disk, so a crash or a nightly reboot picks up where it left off.

It also excludes its own chunks from the chunk-generation safeguard. Without that, starting a run would make the plugin think players were outrunning the generator and clamp everyone's view distance. The plugin fighting itself.

Watch the radius. The workload is (2*radius+1)², so 250 is about 251,000 chunks and 1000 is over four million. If a world border is set, a radius that would reach past it is clamped down automatically (respect-world-border in config, on by default) - so entering something too big costs a log line, not four million wasted chunks nobody inside the border can reach.


Predictive pre-loading

Loads chunks along the path of a fast-moving player so arriving somewhere doesn't stall the tick.

Worth being clear about what this is: it makes chunks ready server-side. Nobody sees further, because the client still only receives what its own view distance covers. What you notice is the absence of a hitch, not a wider horizon.

It follows the movement vector rather than where the player is looking. During elytra flight the view swings around constantly while the flight path stays straight, and it's the path that decides which chunks the server will need. Look direction can be blended in at half weight, but it's off by default.

Generation is off by default too and should stay that way. With it on, this stops being a loader and becomes a second, uncontrolled terrain generator driven by whoever is flying fastest, which is the exact load spike the rest of the plugin exists to contain. Unexplored terrain is what pre-generation is for.


Configuration

Click to expand the full config.yml
# ============================================================
#   ViewDistanceTweaks - LucasTHCR Edition
#   Compatible with Paper 1.21.* - 26.*+
# ============================================================

# Plugin version - DO NOT CHANGE
version: 3

# Master switch. The plugin manages sim/view distance out of the box with the
# defaults below; set this to false if you want to configure everything first
# (or use /vdt disable in-game).
enabled: true

# ============================================================
#   UPDATES
# ============================================================
# Tell you when a newer release exists? One async request on startup, it never
# blocks the server, and nothing is downloaded. Turn it off if the machine has
# no outbound internet access. /vdt update always checks on demand regardless.
# (This was called update-checker before 2.8.3; the old name still works.)
update-notify: true

# Installs new releases on its own. OFF by default, you decide each update.
# Set enabled to true if you would rather not think about it again.
#
# This only ever touches ViewDistanceTweaks. No other plugin is looked at, let
# alone replaced: the only file it can write is named after this plugin's own
# jar.
#
# The running jar is never modified. The download goes into Bukkit's update
# folder (plugins/update/ by default, see update-folder in bukkit.yml) and the
# server installs it on the next START. So: downloaded now, active after your
# next restart. No plugin can safely swap its own jar underneath a running
# server, and this one does not pretend otherwise.
#
# Before anything is written, the download is verified against the SHA-512 hash
# Modrinth publishes for that file. On a mismatch it is discarded, not installed.
#
# Needs update-notify above to stay true, since this acts on its result.
# You can always fetch an update on demand with /vdt update download, whatever
# this is set to.
auto-update:
  enabled: false

  # Only accept releases that list the Minecraft version this server runs.
  # Leaving this true is strongly recommended. false lets the plugin install a
  # build that was never tested against your server version.
  require-matching-game-version: true

  # Which Modrinth release channels may be installed: release, beta, alpha.
  # Add "beta" if you want fixes early and accept the risk that comes with it.
  accepted-release-types:
    - release

# ============================================================
#   IDLE PRE-GENERATION (optional, off by default)
# ============================================================
# Builds terrain ahead of time, so players never wait for it. Generating a chunk
# is the most expensive thing a server does and the one this plugin can otherwise
# only react to. Terrain that already exists is read from disk instead, which is
# cheap, and the chunk-generation safeguard never has to trip at all.
#
# Three rules keep it from becoming the problem it is meant to solve:
#   * It throttles against the same rolling MSPT average the distance engine uses,
#     not an instant reading, so the two never disagree about whether the server
#     is busy.
#   * It STOPS on low memory rather than slowing down. A slower path to an
#     OutOfMemoryError is still an OutOfMemoryError.
#   * It writes progress to disk, so a crash or a nightly reboot resumes exactly
#     where it left off instead of silently losing hours of work.
#
# It also excludes its own chunks from the chunk-generation safeguard. Without
# that, starting a run would make the plugin think players were outrunning the
# generator and clamp everyone's view distance.
idle-pregeneration:
  enabled: false

  # Start automatically once the last player logs off. Set "world" to the world
  # you want built out; leave it empty and nothing starts on its own, but
  # /vdt pregen still works. For a fixed clock time instead of "whenever it's
  # empty", use /vdt pregen schedule <world> <radius> <HH:mm> - see /vdt pregen.
  start-when-empty: true
  world: ""
  # Radius around the world spawn, in chunks. 250 chunks is a 4000-block radius.
  # Mind the squaring: the workload is (2*radius+1)^2 chunks, so 250 is about
  # 251,000 chunks and 1000 is over four million. Match this to your world border
  # rather than picking a round number.
  radius: 250

  # Refuses any /vdt pregen start above this, so a mistyped radius cannot queue
  # a run that would take weeks.
  max-radius: 5000

  # How many chunk requests may be in flight at once, and how many ticks to wait
  # between batches. Higher = faster and heavier. The defaults are deliberately
  # gentle; raise them on a machine with headroom.
  chunks-per-batch: 4
  tick-interval: 5

  # Pause while the server is above this MSPT. Well below decrease-mspt-threshold
  # on purpose: pre-generation is optional work and should yield long before the
  # distance engine has to start defending TPS.
  pause-above-mspt: 35.0

  # Stop entirely while free heap is under this percentage. Higher than
  # memory-safeguard's own floor because mass generation allocates far faster
  # than a distance increase does.
  min-free-memory-percent: 30.0

  # Pause an AUTOMATIC run as soon as anyone logs in, and continue when the server
  # empties again.
  #
  # This does not apply to a run started with /vdt pregen: somebody who typed the
  # command while standing in the world is not asking to be told to log off first.
  # Manual runs stay throttled by pause-above-mspt and min-free-memory-percent, so
  # they still cannot run the server into the ground.
  pause-when-players-online: true

  # Clamp the radius to whatever the world border still allows around the chosen
  # centre, instead of generating chunks nobody inside the border can reach. Only
  # matters once a border smaller than the default (60,000,000 blocks) is set;
  # turn this off if you plan to widen the border later and want the terrain
  # ready ahead of time.
  respect-world-border: true

# ============================================================
#   PREDICTIVE PRE-LOADING (optional, off by default)
# ============================================================
# Loads chunks along the path of a fast-moving player, so arriving somewhere does
# not stall the tick.
#
# To be clear about what this is: it makes chunks ready SERVER-side. It does not
# let anyone see further. The client still receives only what its own view
# distance covers. The visible result is the absence of a hitch, not a wider
# horizon.
#
# Prediction follows the movement vector, not the look direction. During elytra
# flight the view swings around constantly while the flight path stays straight,
# and it is the path that decides which chunks the server will need.
predictive-preloading:
  enabled: false

  # Ticks between passes, and the speed a player must sustain to qualify.
  # 0.8 blocks per tick is roughly sprint speed; elytra flight is far above it.
  tick-interval: 10
  min-speed-blocks-per-tick: 0.8

  # How far along the path to look, and the cap on chunk requests per pass across
  # all players together.
  lookahead-chunks: 4
  max-chunks-per-cycle: 8

  # Skip a pass while the server is above this MSPT. Pre-loading is a comfort
  # feature and must be the first thing to go under pressure.
  pause-above-mspt: 40.0

  # Mix the look direction into the prediction at half weight. Helps when players
  # are about to turn, adds noise during sustained travel. Off by default.
  blend-look-direction: false

  # Allow pre-loading to GENERATE terrain that does not exist yet.
  # Leave this false. With it on, this stops being a loader and becomes a second,
  # uncontrolled generator driven by whoever is flying fastest, the exact load
  # spike the rest of the plugin exists to contain. Use idle-pregeneration for
  # unexplored terrain instead.
  allow-generation: false

# ============================================================
#   ADJUSTMENT
# ============================================================
# Adjustment mode:
#   proactive - Adjusts distances before performance suffers
#               (based on chunk count targets)
#   reactive  - Reacts to MSPT changes
#   mixed     - Combines both; prioritises reduction when overloaded (recommended)
#   off       - Disables automatic sim/view distance management entirely. Sim/view
#               distance is left exactly as-is; only /vdt simulationdistance|viewdistance
#               (temporary overrides) can still change it. Everything else keeps working
#               independently: idle pre-generation, predictive pre-loading, AFK
#               throttling, per-player fixed distances, MSPT tracking. Use this if you
#               only want the pre-generation/pre-loading subsystems and nothing else.
adjustment-mode: mixed

# How many ticks after plugin start should be waited before the first check runs?
# 2400 = 2 minutes, useful to let the server stabilise after a restart.
start-delay: 2400

# How many ticks between checks for whether distances need to be adjusted?
# 600 = every 30 seconds
ticks-per-check: 600

# How many consecutive checks must confirm an increase before it is applied?
#
# This is the main brake on how fast distances recover. With ticks-per-check at
# 600 (30s), each confirmation costs half a minute before anything grows, and it
# resets on any check that is not healthy.
#
# 3 confirmations and max-increase-step 2 means roughly +2 chunks every 90
# seconds, so a world walks 4 -> 16 in about nine minutes of good health.
# Earlier versions shipped 10 here with a step of 1, which is five minutes per
# single chunk and an hour for that same climb - safe, but slow enough that
# admins reasonably thought the plugin had stopped working.
#
# Raise it if your MSPT is spiky and you would rather be sure before growing;
# lower it only if you also trust mspt-prediction and revert-increase to catch
# a bad decision.
passed-checks-for-increase: 3

# How many consecutive checks must confirm a decrease?
# 1 = immediate reaction when overloaded (recommended)
passed-checks-for-decrease: 1

# Should a line be logged to the console when distances are changed?
log-changes: true

# Should a message be sent to players with the permission viewdistancetweaks.notify?
# Useful for admins who want to follow changes live.
announce-changes-in-chat: false

# Format of the announce message. Placeholders:
#   {world}   - world name
#   {oldSim}  - old simulation distance
#   {newSim}  - new simulation distance
#   {oldView} - old view distance
#   {newView} - new view distance
# Colours with & are supported.
announce-format: "&7[VDT] &f{world}: Sim &e{oldSim}&7→&a{newSim} &f| View &e{oldView}&7→&a{newView}"

# ============================================================
#   PROACTIVE MODE
# ============================================================
proactive-mode-settings:

  # Maximum number of ticking chunks globally (across all worlds).
  # Formula: (2*sim+1)^2 * number of players
  # Example: 20 players, Sim 8 -> (17)^2 * 20 = 5780
  global-ticking-chunk-count-target: 5780

  # Maximum number of non-ticking chunks globally (View minus Sim).
  # Formula: ((2*view+1)^2 - (2*sim+1)^2) * number of players
  # Example: 20 players, View 12, Sim 8 -> (625 - 289) * 20 = 6720
  global-non-ticking-chunk-count-target: 6720

  # NEW in 2.6: what a world with ZERO players should be driven to.
  #   min  - drop it to its configured minimum (recommended, default)
  #   max  - drive it to its configured maximum (behaviour before 2.6)
  #   keep - leave whatever it currently has
  # No chunks tick while a world is empty, so "max" costs nothing right up until
  # someone walks in, at which point they land on a fully maxed-out world and
  # max-decrease-step can only walk it back down one notch per check. That is the
  # exact load spike startup-grace exists to prevent, hence "min" as the default.
  empty-world-target: min

  # NEW in 2.7: weigh chunks by what is actually IN them instead of counting
  # them all the same. An ocean chunk costs almost nothing; a chunk holding a
  # 500-hopper sorting system can dominate a tick on its own, yet the plain
  # (2*d+1)^2 formula treats both as one unit, so a redstone-heavy world gets
  # the same budget as a world full of water.
  #
  # Inspecting chunks is itself work, so this SAMPLES a bounded number of
  # loaded chunks per world (round-robin) and derives an average multiplier,
  # refreshed once per ticks-per-check. It never scans every chunk.
  use-complexity-weighting: false

  # How many loaded chunks to sample per world per refresh. Higher = more
  # accurate but more work. 40 is plenty for a stable average.
  complexity-sample-chunks: 40

  # Base cost of an ordinary chunk before any tile entities are counted.
  complexity-default-weight: 1.0

  # Surcharge per tile entity of that material. Names are Bukkit Material
  # names. Anything not listed adds nothing.
  complexity-weights:
    HOPPER: 0.5
    SPAWNER: 2.0
    CHEST: 0.05
    TRAPPED_CHEST: 0.05
    BARREL: 0.05
    FURNACE: 0.1
    BLAST_FURNACE: 0.1
    SMOKER: 0.1
    BREWING_STAND: 0.1
    BEACON: 0.5
    CONDUIT: 0.5
    PISTON: 0.1
    STICKY_PISTON: 0.1

# ============================================================
#   REACTIVE MODE
# ============================================================
reactive-mode-settings:

  # Below which MSPT should an increase of the distance be considered?
  increase-mspt-threshold: 40.0

  # Above which MSPT should a decrease of the distance be considered?
  # A tick lasts at most 50ms (= 20 TPS); players notice lag from ~47ms.
  decrease-mspt-threshold: 47.0

  # If true: when the server is overloaded, simulation distance is reduced FIRST.
  # View distance is only reduced once simulation distance has already hit its
  # configured minimum for that world. Simulation distance has a bigger impact
  # on TPS, while reducing view distance causes visible chunk flicker for
  # players, so this avoids touching view distance unless it's really needed.
  # If false: simulation and view distance are reduced together (old behavior).
  # Default: false
  prioritize-simulation-distance: false

  # Maximum number of steps simulation/view distance may change by in a single
  # check cycle, in EACH direction. Setting both to 1 gives the old behaviour of
  # one chunk at a time.
  #   max-increase-step - limits how much distance can grow in one check
  #   max-decrease-step - limits how much distance can shrink in one check
  #
  # These are deliberately not the same. Growing is a comfort: two chunks at a
  # time is quick enough that recovery is visible within minutes, and a step that
  # turns out to be wrong is undone by revert-increase-if-overloaded on the very
  # next check. Shrinking is a defence, and four lets it bail out in one move
  # when the server is genuinely in trouble rather than crawling down while
  # players sit through it.
  #
  # Default: 2 up, 4 down
  max-increase-step: 2
  max-decrease-step: 4

  # After increasing simulation/view distance, re-check MSPT shortly after
  # (see below). If the server is immediately overloaded as a result, the
  # increase is rolled back on the next check instead of waiting through the
  # normal passed-checks-for-decrease delay. This prevents the "jump straight
  # to max, then crawl back down one step at a time" problem.
  revert-increase-if-overloaded: true

  mspt-tracker-settings:

    # Over how many ticks is the MSPT average calculated?
    # 1200 = 1 minute. Higher = more stable but slower to react.
    collection-period: 1200

    # MSPT prediction: Prevents increasing distances
    # even though we know from history that this would cause lag.
    mspt-prediction:
      enabled: true

      # How many minutes of history should be stored for the prediction?
      history-length: 30

      # Minimum number of data points before the prediction takes effect.
      # Prevents wrong decisions immediately after server start.
      min-samples: 5

    # MSPT VARIANCE: a server can have a perfectly fine *average* MSPT while
    # still spiking badly tick-to-tick (e.g. redstone clocks, chunk generation
    # bursts, plugin schedulers). Average-only checks miss this. This tracks
    # how spread out the recent tick times are (standard deviation) and can
    # treat a high-variance server as if it were overloaded, even if the
    # average MSPT is still below decrease-mspt-threshold.
    mspt-variance:
      enabled: true

      # How many of the most recent tick samples (from Paper's getTickTimes())
      # are used to compute the standard deviation. getTickTimes() holds up to
      # 100 samples (~5s at 20 TPS); this just controls how many of those are used.
      sample-window: 100

      # Standard deviation (in ms) above which the server is considered
      # "unstable" and treated as overloaded for decrease purposes, even if
      # average MSPT is fine. Typical stable servers sit around 1-4ms stddev;
      # noticeable stutter usually starts around 8-10ms+.
      variance-threshold: 8.0

      # Minimum number of samples required before variance is evaluated at all.
      # Prevents false positives right after server start.
      min-samples: 20

      # If true, high variance ALSO blocks increases (in addition to forcing
      # decreases), even if average MSPT looks fine. Recommended: true.
      block-increase-on-high-variance: true

      # Standard deviation is symmetric. It measures how spread out recent ticks
      # are around the average, regardless of direction. A server bouncing between
      # 4ms and 18ms (avg ~10ms, still totally fine) can trip the same stddev
      # threshold as one bouncing between 40ms and 55ms (avg ~47ms, actually
      # struggling). Variance is only treated as overload once the average MSPT
      # is at least this high. Below it, variance is ignored even if it exceeds
      # variance-threshold above.
      min-avg-mspt-to-trigger: 25.0

# ============================================================
#   PER-WORLD MSPT (optional)
# ============================================================
# The server reports one MSPT figure for everything. When a single world is
# responsible for the load, that one number drags every world down with it and
# all of them get throttled - including the quiet ones that were not the
# problem.
#
# With this on, the plugin splits the server tick between worlds and judges
# each world by its own share. The world causing the lag shrinks first; the
# quiet ones are left alone, or may even keep growing.
#
# IMPORTANT: this does NOT change what the thresholds above mean. A world
# carrying exactly its fair share of the tick is still judged against the same
# decrease-mspt-threshold as before. Only worlds that pull more (or less) than
# their share see an adjusted figure, so nothing has to be re-tuned.
# On a single-world server this feature changes nothing at all.
per-world-mspt:

  # Default: false. Turning this on changes which world gets throttled first.
  enabled: false

  # How many ticks between per-world cost readings. The server tick time itself
  # is read every tick regardless, so raising this does not make the plugin
  # slower to notice lag spikes - it only makes the split between worlds
  # refresh less often. Raise it on servers with very many worlds.
  # Default: 20 (once per second)
  sample-interval: 20

  # On Folia, measure region tick times directly instead of estimating from
  # world contents. Ignored on Paper, which has no per-region timing to read.
  #
  # Note what this costs: a measured Folia figure is a region's tick PERIOD, and
  # every region that is keeping up reports the tick rate itself. So the per-world
  # split comes out even no matter which world is busy, and there is nothing to
  # attribute - with this on, per-world MSPT has no effect on Folia until regions
  # actually fall behind. Set it to false to use the estimating proxy on Folia too,
  # which is less precise but can tell a busy world from an idle one.
  # Default: true
  prefer-exact-source: true

  # How far a world's figure may be scaled away from the server-wide one.
  # A factor of 1.0 means "this world carries exactly its fair share".
  #   min - floor, so a quiet world on a struggling server is not told
  #         everything is fine and allowed to grow without limit
  #   max - ceiling, so one world briefly holding the whole tick does not get
  #         slammed to its minimum distance over a momentary spike
  # Default: 0.5 and 2.0
  min-attribution-factor: 0.5
  max-attribution-factor: 2.0

  # Log a line whenever a world is judged by an adjusted figure. Useful when
  # first turning this on to see what it is actually doing; noisy afterwards.
  # Default: false
  log-attribution: false

# ============================================================
#   PER-PLAYER FIXED VIEW DISTANCE (optional)
# ============================================================
# Lets specific players (e.g. mods, streamers) keep a fixed CLIENT-SIDE view
# distance regardless of what the plugin sets for the world. This only affects
# what that individual player receives. It does NOT change the world's
# server-side view distance or the chunk budget the rest of this plugin manages.
#
# Grant a permission "<permission-prefix><value>" to set that player's fixed
# view distance, e.g. with permission-prefix "viewdistancetweaks.fixed." :
#   viewdistancetweaks.fixed.20   -> that player always gets view distance 20
# If a player has multiple such permissions, the highest value is used.
# Valid values are 2-32; anything higher is clamped to 32. Note that a player
# never sees more chunks than their own client render-distance setting allows.
# Taking the permission away again hands the player back to their world's
# normal, plugin-managed view distance, no relog needed.
per-player-fixed-view-distance:
  # Must be true for the viewdistancetweaks.fixed.<value> permissions to do
  # anything. Works independently of the "enabled" master switch at the top.
  enabled: false
  # Must be lowercase-safe; a trailing "." is added automatically if missing.
  permission-prefix: "viewdistancetweaks.fixed."

# ============================================================
#   PER-PLAYER FIXED SIMULATION DISTANCE (optional, NEW in 2.6)
# ============================================================
# Exactly the same idea as the fixed view distance above, but for simulation
# distance (Paper's Player#setSimulationDistance). Simulation distance controls
# how far around that player the world actually TICKS: mobs, redstone, crops.
#
# Careful: unlike view distance this genuinely costs server performance, so
# handing it out widely will hurt. It is meant for a handful of staff members,
# not for a donor rank.
#
#   viewdistancetweaks.simfixed.8   -> that player always gets simulation distance 8
#
# Valid values are 2-32; anything higher is clamped to 32. If a player holds
# several such permissions, the highest value is used. Taking the permission
# away hands the player back to their world's normal distance, no relog needed.
# Use a DIFFERENT permission-prefix than the view-distance feature above,
# otherwise a single permission would set both values at once.
per-player-fixed-simulation-distance:
  enabled: false
  permission-prefix: "viewdistancetweaks.simfixed."

# ============================================================
#   AFK PLAYER THROTTLING (optional, NEW in 2.7)
# ============================================================
# A player standing still at spawn does not need the same distances as one
# flying an elytra. When they go AFK their personal distances drop to the
# values below, and the chunks they free up go straight back into the global
# budget for players who are actually playing.
#
# AFK state comes from Paper's own idle timer. If EssentialsX is installed its
# AFK state is used instead, so someone who typed /afk counts immediately
# instead of having to wait out afk-time-minutes.
#
# Note: an explicit viewdistancetweaks.fixed.<n> permission WINS over this
# throttle. If you want staff throttled while AFK too, don't give them that
# permission.
afk-player-throttling:
  enabled: false
  afk-time-minutes: 5
  afk-simulation-distance: 4
  afk-view-distance: 6
  use-essentials-if-present: true

# ============================================================
#   PING THROTTLING (optional, NEW in 2.11)
# ============================================================
# A player with a bad connection cannot keep up with the chunks their distance
# already sends them - packets queue, the client falls further behind, and
# raising anyone else's distance only adds to what that connection has to
# carry. Reduced personal distances free the budget for players whose
# connection can actually use it. Same mechanism as afk-player-throttling:
# personal distances only, applied through Player#setViewDistance /
# #setSimulationDistance.
#
# A single high reading does not trip this. Checks run on the same ~5-second
# cadence as afk-player-throttling and the fixed-view overrides, and the
# threshold has to hold for checks-before-throttle of them in a row before
# anyone is throttled - so the default of 3 is about 15 seconds of sustained
# bad ping. Recovery is immediate: the next good sample lifts the throttle.
#
# If both this and afk-player-throttling apply to the same player, the smaller
# (more restrictive) distance wins - enabling one never raises what the other
# already set.
#
# Note: an explicit viewdistancetweaks.fixed.<n> permission WINS over this
# throttle, exactly like afk-player-throttling.
ping-throttling:
  enabled: false
  ping-threshold-ms: 200
  checks-before-throttle: 3
  ping-simulation-distance: 4
  ping-view-distance: 6

# ============================================================
#   CHUNK GENERATION SAFEGUARD (optional, NEW in 2.7)
# ============================================================
# mspt-variance notices the stutter that terrain generation causes, but not the
# cause. This counts chunks that are genuinely being GENERATED (not merely read
# from disk) and reacts to the cause directly.
#
# Generating a chunk runs the whole terrain pipeline and is what actually
# freezes the main thread when someone crosses unexplored terrain at speed.
chunk-generation-safeguard:
  enabled: false

  # Newly generated chunks per second, per world, above which the safeguard trips.
  chunks-per-second-threshold: 15

  # Block distance increases while generation is running. Growing view distance
  # mid-generation feeds the exact thing that is stalling the server and causes
  # the rubber-banding this is meant to avoid.
  lock-increase-during-gen: true

  # Additionally clamp view distance to this value while generating.
  # -1 disables the hard clamp and only uses lock-increase-during-gen above.
  emergency-view-distance: -1

# ============================================================
#   DENSITY-BASED SCALING (optional, NEW in 2.7)
# ============================================================
# 20 players scattered across a world load 20 separate rings of chunks.
# 20 players standing around the same community farm load almost the SAME
# chunks. The proactive formula assumes the first case and therefore starves a
# clustered world of view distance it could easily afford.
#
# When enough players sit within cluster-radius chunks of each other, this
# grants bonus distance proportional to how much of the world's population is
# in that cluster.
density-based-scaling:
  enabled: false
  # Radius in chunks within which players count as "together"
  cluster-radius: 8
  # Minimum players in one cluster before any bonus is granted
  cluster-player-threshold: 5
  # Maximum bonus distance, reached when everybody is in the same cluster
  max-bonus: 4

# ============================================================
#   CLIENT VIEW DISTANCE CAP (optional)
# ============================================================
# Nobody sees further than their own client draws. If the highest client setting
# in a world is 8, a server view distance of 16 loads, ticks and pays for rings
# of chunks that are never sent to anyone.
#
# With this on, a world's view distance is capped at the largest value its
# players' clients report, plus the headroom below. Never below that world's
# min-view-distance, and never applied to an empty world or to players whose
# client did not report a value.
#
# Off by default: it only helps if your players actually run low settings, and
# on a server where everyone uses 16+ it changes nothing at all.
client-view-distance-cap:
  enabled: false

  # Chunks on top of the highest reported setting. 1 means a player who raises
  # their slider one notch is not left staring at ungenerated terrain until the
  # next check. 0 caps exactly.
  headroom-chunks: 1

# ============================================================
#   EMERGENCY RELIEF (optional, NEW in 2.7)
# ============================================================
# Last resort. When a world already sits at its MINIMUM simulation and view
# distance and the server is STILL over decrease-mspt-threshold, there is
# nothing left to take away from distances. Rather than let the server keep
# sinking, shed two cheaper-to-lose sources of load instead: thunderstorms
# (extra light and weather updates) and the day/night cycle.
#
# Both are restored automatically once MSPT drops below recover-below-mspt,
# and on plugin disable. The world's ORIGINAL weather and doDaylightCycle
# setting are remembered, so a world that deliberately had the cycle off
# keeps it off afterwards.
emergency-relief:
  enabled: false
  clear-weather: true
  freeze-time: true
  # Deliberately lower than decrease-mspt-threshold, otherwise relief would
  # flap on and off around the same value that triggered it.
  recover-below-mspt: 40.0

# ============================================================
#   MEMORY SAFEGUARD (NEW in 2.7)
# ============================================================
# Distances don't only cost CPU. Every extra ring of chunks stays resident in
# the heap. A server can sit at a perfect 20ms MSPT and still be one increase
# away from spending every tick in garbage collection, at which point MSPT
# reacts far too late to be useful as the only signal.
#
# While free heap is below min-free-percent, ALL increases are blocked
# regardless of MSPT. Decreases always stay allowed.
#
# "Free" means free of LIVE data, measured against the -Xmx ceiling. It is the
# lowest heap usage seen in the last minute, not the usage right now: every
# collector deliberately lets garbage pile up before reclaiming it, so a single
# reading says more about where the GC cycle happens to be than about how much
# memory the server actually needs. Right after a collection, usage IS the live
# set, and that is what this compares against.
#
# FIXED in 2.11.1: the old reading also credited whatever the JVM was still
# allowed to grow by. Start with -Xms equal to -Xmx - what Aikar's flags and
# every hosting panel recommend - and the heap is committed up front, that
# credit is permanently zero, and the reading collapses to raw occupancy. On
# ZGC that blocked increases outright; on G1 it started once live data passed
# roughly half the heap. If you lowered min-free-percent to work around it,
# you can put it back.
memory-safeguard:
  enabled: true
  min-free-percent: 15.0

# ============================================================
#   PROFILES (optional, NEW in 2.7)
# ============================================================
# Named overlays for a handful of tuning values, switchable in-game with
# /vdt profile <name>, no config editing mid-event. "normal" is always
# available and means "the values configured above".
#
# Supported keys inside a profile:
#   decrease-mspt-threshold, increase-mspt-threshold,
#   max-increase-step, max-decrease-step,
#   passed-checks-for-increase, passed-checks-for-decrease,
#   global-ticking-chunk-count-target, global-non-ticking-chunk-count-target,
#   max-view-distance-cap, max-simulation-distance-cap
#
# The two *-cap keys are hard caps applied AFTER every other rule, so a cap of
# 8 genuinely means 8 and cannot be undone by never-reduce-below-players.
profiles:
  event-mode:
    decrease-mspt-threshold: 42.0   # react to load much earlier
    max-view-distance-cap: 8        # hard ceiling for the duration of the event
    passed-checks-for-increase: 20  # and be very reluctant to grow again

  night-mode:
    increase-mspt-threshold: 45.0   # be generous while the server is quiet
    max-increase-step: 2

# ============================================================
#   STARTUP GRACE PERIOD (optional)
# ============================================================
# If enabled, the plugin forces a low, fixed sim/view distance immediately when
# the plugin starts (before start-delay/ticks-per-check even apply), instead of
# whatever distance the world already had (which could be near max after a
# config change or a manual override, and cause instant overload/crash on a
# cold-started server). Once duration-ticks has passed, normal proactive/
# reactive/mixed adjustment takes back over as usual, and max-increase-step
# still limits how fast it's then allowed to climb back up.
startup-grace:
  enabled: false
  simulation-distance: 4
  view-distance: 6
  # 6000 ticks = 5 minutes
  duration-ticks: 6000

# ============================================================
#   WORLD SETTINGS
# ============================================================
# Any worlds not listed here inherit the "default" settings.
# You can override each world with its exact name.

world-settings:

  default:
    simulation-distance:
      # Exclude the world completely from management?
      # Chunks are still counted for global calculations (unless chunk-weight: 0).
      exclude: false
      min-simulation-distance: 4
      max-simulation-distance: 12

    view-distance:
      exclude: false
      min-view-distance: 6
      max-view-distance: 16

    # How much chunk "budget" does this world weigh?
    # 1.0 = normal. 0.5 = half as heavy (e.g. End, many empty chunks).
    # 0.0 = world has no influence on global calculations.
    chunk-weight: 1.0

    # NEW: Override how often THIS world is checked, independent of the global
    # ticks-per-check above. Leave unset (or <= 0) to inherit the global value.
    # Useful for a busy overworld that needs quick reactions vs. a quiet, mostly
    # empty nether/end that doesn't need checking every 30 seconds.
    # ticks-per-check: 200

    # NEW: Never lower the distance below the current player count in this world.
    # Useful for small servers with few players.
    never-reduce-below-players: false

    # NEW: This world gets 50% more chunk budget in proactive calculations.
    # Useful for the main world on servers with multiple dimensions.
    priority-world: false

  # Example: Nether world
  # world_nether:
  #   simulation-distance:
  #     min-simulation-distance: 4
  #     max-simulation-distance: 8
  #   view-distance:
  #     min-view-distance: 4
  #     max-view-distance: 10
  #   chunk-weight: 0.8
  #   priority-world: false

  # Example: End world (many empty chunks -> lower weight)
  # world_the_end:
  #   simulation-distance:
  #     min-simulation-distance: 4
  #     max-simulation-distance: 10
  #   view-distance:
  #     min-view-distance: 6
  #     max-view-distance: 14
  #   chunk-weight: 0.5

  # Example: Main world with priority
  # world:
  #   simulation-distance:
  #     min-simulation-distance: 6
  #     max-simulation-distance: 12
  #   view-distance:
  #     min-view-distance: 8
  #     max-view-distance: 16
  #   chunk-weight: 1.0
  #   priority-world: true

messages.yml has every string players and staff see, with & colours and {placeholder} substitution. It's copied into the data folder on first start. Every key also has a fallback inside the jar, so an older messages.yml keeps working instead of rendering blank lines, and anything missing gets listed in the console once at load.

Distances are in chunks, timings are in ticks, so ticks-per-check: 600 is 30 seconds. The --duration flag on the two override commands is the exception, that one takes minutes.

An older config.yml gets topped up on start with whatever a newer build added, and your previous file is kept as config.yml.bak. Nothing existing is rewritten.


Commands

Command What it does
/vdt status [--weight] [--fixed] Live state per world: distances, chunk counts, MSPT, variance, plus a line for each subsystem that is actually switched on. --weight adds chunk weight and complexity multiplier, --fixed lists per-player overrides
/vdt simulate [world] Dry run. Shows what the next check would decide and the reason for it, without changing anything
/vdt history The last 20 applied changes, each with the reason it was made. Answers "why is view distance 6 right now" after the fact
/vdt analyze Suggestions from this server's own history, e.g. a threshold that is never reached. Changes nothing
/vdt doctor One check of everything that commonly goes wrong: platform, thresholds, world limits, other plugins overwriting distances, per-world MSPT, free memory, updates. Paste it into a bug report
/vdt mspt [debug] MSPT, TPS and standard deviation. debug adds the raw Paper tick samples
/vdt viewdistance <n> [world] [--duration <min>] Manual override. Without --duration it holds until the next reload
/vdt simulationdistance <n> [world] [--duration <min>] Same, for simulation distance
/vdt profile [name] Switch tuning overlay, or show which one is active
/vdt preset <name> Write one of the bundled presets into config.yml
/vdt pregen <start\|pause\|resume\|stop\|cancel\|schedule\|status> Pre-generate terrain. start <world> [radius] [--here]; schedule <world> <radius> <HH:mm> or schedule cancel; stop keeps progress, cancel --confirm discards it
/vdt enable / /vdt disable Master switch without editing the config file
/vdt reload Re-read config.yml and messages.yml, drop manual overrides
/vdt update Ask Modrinth whether there is a newer version

Permissions

Everything defaults to OP, except the two numeric families at the bottom, which are never granted automatically.

Node Grants
viewdistancetweaks.command.vdt Access to /vdt at all
viewdistancetweaks.vdt.command.status /vdt status, /vdt history, /vdt mspt, /vdt analyze and /vdt doctor
viewdistancetweaks.vdt.command.simulate /vdt simulate
viewdistancetweaks.vdt.command.viewdistance /vdt viewdistance
viewdistancetweaks.vdt.command.simulationdistance /vdt simulationdistance
viewdistancetweaks.vdt.command.preset /vdt preset
viewdistancetweaks.vdt.command.profile /vdt profile
viewdistancetweaks.vdt.command.pregen /vdt pregen
viewdistancetweaks.vdt.command.reload /vdt reload, enable, disable, update
viewdistancetweaks.notify Receives the in-chat announcement when a distance changes
viewdistancetweaks.fixed.<n> Fixed view distance for that player
viewdistancetweaks.simfixed.<n> Fixed simulation distance for that player

Support

Made and maintained by LucasTHCR.

Questions, bug reports and feature ideas go to dc.gg/paperstream. I read everything there, and a bug report with your config.yml and the output of /vdt status saves us both a round of guessing.


License

Copyright (C) 2026 LucasTHCR

ViewDistanceTweaks is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3 as published by the Free Software Foundation.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.

Short version: use it, study it, change it, pass it on. Anything you distribute that's built on it has to be GPLv3 too and has to ship its source.

Releases up to and including 2.6-RELEASE were published under the MIT License; copies obtained under those terms stay governed by them.

All Versions

+ Add Version
Name Game version Platforms Published Downloads Actions
26.3 26.2 26.1.x +1
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.3 26.2 26.1.x +1
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.3 26.2 26.1.x +1
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.3 26.2 26.1.x +1
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.3 26.2 26.1.x +1
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.3 26.2 26.1.x +1
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Folia Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Paper Purpur Spigot
20h ago 1
26.2 26.1.x 1.21.x
Bukkit Paper Spigot
20h ago 1
### Added

  • /vdt doctor. One look at everything that commonly goes wrong, in a single list: platform and
scheduler, whether the plugin is enabled, MSPT thresholds that leave no gap, worlds with a
minimum above their maximum, worlds that are configured but not loaded, other plugins that keep
overwriting a world's distances (with the likely culprits), the per-world MSPT state, free heap,
PlaceholderAPI and whether an update is out. It reads only and changes nothing. Pasting its
output into a bug report answers most of the first questions. Permission: the same as
/vdt status.
  • /vdt analyze and /vdt doctor are listed in /vdt help.

### Changed

  • The readme and the project page now describe the client view distance cap and the conflict
detection from the 2.11 and 2.12 releases, and list the placeholders only once.

No config changes. messages.yml gains a doctor section; an existing file keeps working, the
new lines fall back to the bundled English text.

### Cap view distance at what the clients actually render

Nobody sees further than their own client draws. If the highest client setting in a world is 8, a server view distance of 16 loads, ticks and pays for rings of chunks that are never sent to anyone.

The new client-view-distance-cap section caps a world's view distance at the largest value its players' clients report, plus a small headroom. It never goes below that world's min-view-distance, simulation distance follows so view >= sim still holds, and an empty world is left alone. A client that never reported a value counts as unknown rather than as a cap of zero.

Off by default. It only pays on servers whose players run low settings, and changes nothing where everyone uses 16 or more. /vdt status shows the highest client value per world when it is on.

### A config upgrade that quietly did nothing

Found while building the above, and it would have affected every future option: the upgrader keeps a hand-maintained list of the sections it knows. A section missing from that list was never added to an existing config, while the config version marker was raised anyway, after which the upgrader stayed silent about it for good. The option then ran on its code default and nobody could find it in their file.

There is now a test comparing that list against the shipped config, so a new section cannot slip through again.

### Shorter comments

Roughly 1700 lines of explanatory comment came out of the source. The reasoning behind each fix lives in the commit that made it; what stays in the code is the short note that keeps a line from being rewritten wrongly. No behaviour changed.

### Upgrading

Replace the jar and restart. The new section is added to your existing config.yml automatically, switched off.

A follow-up to 2.11.6, from the same report.

### The warning now names the plugins that could be responsible

2.11.6 started saying when something else on the server overwrites view or simulation distance. Useful, but it left the obvious question open: which plugin? On the server this came from, answering that meant going through forty plugins by hand.

There is no way to catch the writer in the act, since Bukkit has no event for it and the call is long gone by the time the next check notices. What can be answered is which plugins are even able to do it: a call to setViewDistance leaves the method name in the calling class, and Bukkit API names survive obfuscation. So the warning is now followed by a second line:

``
Plugins installed here that CAN write view or simulation distance: ClearLag.
This lists what is capable of it, not what did it - a plugin whose feature is
turned off still appears, and so does one that only changes it per player.
Check their configs for a view-distance option before changing anything, and
do not remove a plugin on the strength of this line.
`

The wording is deliberate. The scan proves capability, never guilt: a plugin with the feature switched off still matches, and
Player#setViewDistance` is spelled the same as the world method, so per-player plugins match without ever touching a world. It exists to turn forty plugins into two worth checking, not to pick one out.

If nothing matches, it says so too, and points at server.properties and proxies instead.

The scan reads the jars once per server start, on a background thread, and only after a conflict has actually been detected. Servers that never hit one never pay for it.

### Upgrading

No config changes and no new keys. Replace the jar and restart.

One fix, from a live server report.

### The plugin now notices when another plugin overwrites it

If something else on the server also writes view or simulation distance, the plugin never knew. Every cycle it read the foreign value as the current state, wrote its own over it, logged an honest change, and did the same thing again 30 seconds later.

On the server this was reported from, that meant twenty worlds producing the same log line every 30 seconds, forever, and players reloading chunks on a timer because the value kept moving. Nothing in the log suggested the plugin was being overruled rather than simply doing its job, so finding the cause took hours. It turned out to be a lag-management plugin with its own view distance feature.

The plugin now remembers what it last set. If a world comes back changed three checks in a row, it says so once:

``
'world_nether': view distance set to 12 but found at 20 on the next check, 3 times in
a row. Something else on this server writes these values too ... Leaving this
world alone. Check your other plugins for a view-distance setting, then
/vdt reload to resume managing it.
`

Then it stops managing that world, which ends both the log spam and the chunk reloading.
/vdt status` marks the world so the state is visible after the warning has scrolled away. A reload, or setting that world by hand, resumes management.

Three checks rather than one on purpose: a single mismatch is an admin running a command or a world reloading, not a second plugin.

### Upgrading

No config changes and no new keys. Replace the jar and restart.

One fix, found while testing against Paper 26.3.

### An ampersand that is not a colour code keeps its ampersand

Colour codes are written with &, and every & in a message was turned into the section sign the client renders colours with, whether or not a colour code followed it. An ampersand written as a word therefore ate itself and left behind a character the client cannot resolve.

The built-in help showed it: /vdt mspt was described as "Show MSPT, TPS § variance". The same happened to any & you wrote in your own messages.yml or in announce-format.

Nothing ever reported this, because there is nothing to report. A stray section sign is not an error at runtime, it simply renders as a stray character.

Only & directly followed by a real code is translated now, which is what Bukkit itself does. One limit is unchanged and cannot be removed: a code letter is a code letter, so &d is light purple whether or not you meant to write "R&D".

### Paper 26.3

Tested against build 5 under Java 25. Clean start, no deprecation warnings, every command behaving correctly, and the distance engine climbing 10 to 12 across three worlds in three confirmations, exactly as the defaults intend.

### Upgrading

No config changes and no new keys. Replace the jar and restart.

One fix, found by reading the code rather than from a report.

### An operator pause on a pre-generation run undid itself

/vdt pregen pause and /vdt pregen stop both worked, reported success, and were then quietly reversed.

The per-tick driver tracks two things: whether a run is paused, and whether the plugin paused it itself. Anything that blocks generation - a player logging in, an MSPT spike, a dip in free heap - wrote both. So the first blocker to come along relabelled an operator's pause as the plugin's own, and the release that follows lifted it as soon as that blocker cleared. The run carried on generating terrain nobody had asked it to resume, and /vdt pregen status showed the blocker as the reason instead of the command that actually paused it.

The failure path is the worse half. When a chunk request is refused, the run stops rather than cancels, specifically so the request is not retried in a loop and the progress survives until someone looks at the cause. That containment ran through the same operator pause, so it did not hold: the run came back on its own and asked for the same failing chunk again.

An operator pause is now settled before any blocker is looked at.

### Upgrading

No config changes and no new keys. Replace the jar and restart.

Four fixes from an error-analysis pass over the whole plugin. Nothing here was reported from outside; all four came out of reading the code, and each one is covered by a test that was checked against the unfixed build first.

### The memory safeguard could go blind after a clock jump

2.11.1 rewrote this to judge free heap by the lowest usage seen in the last minute, which is the right measurement. It timed that window with System.currentTimeMillis(), which is a wall clock and can step in both directions: an NTP correction, a resumed VM, a host clock sync.

A backwards step is the dangerous one. The window stops accepting new samples and keeps only the branch that revises the current reading downwards, so it freezes holding an old trough that can fall but never rise. For the length of the jump the plugin reports free memory the server no longer has, and distance increases are allowed into a filling heap.

Every reading in there is a duration, never a date, so it now uses System.nanoTime(), which is monotonic within a JVM. The condition cannot arise.

### A release was invisible to the pre-release that shared its number

Version comparison read the numbers and stopped, so 2.11.0-RELEASE and 2.11.0-ALPHA came out equal. Anyone running an alpha or a beta was never told the finished version existed, and auto-update never staged it.

Ties now break the way semantic versioning breaks them: a pre-release ranks below the release with the same numbers. Anything unrecognised counts as a release, so this can only ever withhold an update, never invent one.

### Two quieter ones

chunk-generation-safeguard kept a counter per world and nothing removed it when a world was unloaded. Servers that create and discard worlds at runtime, minigame arenas and per-player plots, accumulated one entry per world for the lifetime of the process.

On Folia, a refused scheduler dispatch was logged and otherwise swallowed. The complexity sampler decides who publishes its result by counting outstanding work, so a task that never ran left it waiting forever and every world kept the multiplier it already had, which is 1.00x. With use-complexity-weighting on, the feature would have done nothing at all, without a line anywhere to say so.

### Upgrading

No config changes and no new keys. Replace the jar and restart.

First stable since 2.10.8. If you skipped the 2.11.0 alpha and the 2.11.1 beta, everything from both is in here.

### The memory safeguard was reading the wrong number

  • If your start script sets -Xms equal to -Xmx, this one affects you.** That is what Aikar's flags and essentially every hosting panel recommend, so it is most servers.

The free-heap figure was unused space in the current heap plus whatever the JVM was still allowed to grow by. Commit the heap in full at startup and that second term is zero forever, leaving raw occupancy. Occupancy counts garbage that has not been collected yet, and every modern collector deliberately lets the heap fill before reclaiming it. So the number described where the GC cycle happened to be, not how much memory your server needed.

That alone would be an occasional wrong reading. What made it permanent is that an increase needs passed-checks-for-increase clean checks in a row, and any veto resets that counter, so a minority of false readings is enough to pin a server at its minimum distance indefinitely.

It now measures the lowest heap usage seen in the last minute. Right after a collection, usage is the live set, so that figure is a real measurement of what the server needs.

On a 1GB ZGC heap the old build logged five vetoes, made zero distance changes in six minutes and finished where it started. The new build logged none, made eleven, and climbed from simulation distance 4 to 12. On a heap with room to spare, where the old reading happened to be right, both behave identically.

ZGC is affected from the start. G1 with Aikar's flags holds out longer, but goes the same way once live data passes roughly half the heap. **If you lowered min-free-percent to work around this, you can put it back to 15.**

### Per-world MSPT

The server reports one MSPT figure for everything, so a single struggling world used to drag every other world's distance down with it. Turn on per-world-mspt and the tick is split between worlds, each judged by the share it actually causes. Off by default.

### /vdt analyze

Reads the server's own change history and MSPT-versus-chunk-count samples, then suggests config values that fit what this specific server has shown. It only suggests, nothing is applied.

### Ping throttling

The same idea as AFK throttling, triggered by a bad connection instead of an idle player. A player whose ping stays above the threshold for a few checks running gets reduced personal distances. A single spike does not trigger it and recovery is immediate. If a player is both AFK and on a bad connection the more restrictive value applies, and an explicit viewdistancetweaks.fixed.<n> permission still beats both. Off by default.

### Pre-generation: world border and scheduling

Idle pre-generation clamps its radius to the world border by default, so a radius entered too large costs a log line instead of generating chunks nobody can reach. /vdt pregen schedule <world> <radius> <HH:mm> starts a run at a clock time rather than waiting for an empty server; every existing safety net still applies once it is running.

### Faster climb defaults

passed-checks-for-increase 10 to 3, max-increase-step 1 to 2, max-decrease-step 1 to 4. Climbing from 4 to 16 now takes about nine minutes of uninterrupted good health instead of an hour. **These apply to new installs only.** An existing config.yml already contains those keys, so your values are kept.

### Upgrading from 2.10.8

Close to a pure bugfix. The only block added to your config.yml is ping-throttling, and it ships disabled. Every other feature above is off by default as well, and your tuned values are preserved. /vdt status now labels the heap figure live, because that is what it measures.

This is a BETA. accepted-release-types defaults to release, so this build is not offered to existing installations unless an admin opts in by hand. It contains one fix that matters to almost every server, and it needs confirmation on a real one before it goes out as a stable.

  • If your start script sets -Xms equal to -Xmx, this fix is for you.** That includes Aikar's flags and the default of essentially every hosting panel.

### The memory safeguard was reading the wrong number

Reported by a user running ZGC with a fully committed heap: the safeguard blocked every distance increase, permanently, on a server with plenty of memory left.

The free-heap figure was unused space in the current heap plus whatever the JVM was still allowed to grow by. Commit the heap in full at startup and that second term is zero forever, leaving raw occupancy. Raw occupancy counts garbage that has not been collected yet, and every modern collector deliberately lets the heap fill before reclaiming it. So the number described where the GC cycle happened to be, not how much memory the server actually needed.

What made it total rather than occasional is that an increase needs passed-checks-for-increase consecutive clean checks, and any veto resets that counter. A minority of false readings is enough to pin a server at its minimum distance indefinitely.

It now measures the lowest heap usage seen in the last minute. Right after a collection, usage is the live set, so that figure is a real measurement of what the server needs rather than a snapshot of the GC cycle.

Measured on a 1GB ZGC heap: the old build logged five vetoes, made zero distance changes in six minutes and finished where it started. The new build logged none, made eleven, and climbed from simulation distance 4 to 12. On a heap with room to spare, where the old reading happened to be right, both behave identically.

ZGC is hit hardest and from the start. Aikar's G1 flags hold out longer, because they begin marking early, but they go the same way once live data passes roughly half the heap.

  • If you lowered min-free-percent to work around this, you can put it back to 15.**

### /vdt status says what it means

The free-heap line used to print a placeholder 100% until the first check ran, up to start-delay after every boot and reload, while showing the real figure in megabytes beside it on the same line. It now reports a measurement from the first second. The megabyte figure is labelled live, because that is what it is: heap held by live data, not by uncollected garbage.

### Why 2.11.1 and not 2.11.0

2.11.0 only ever went out as an alpha. The update checker compares the leading numeric part and ignores the suffix by design, so a 2.11.0 release would have compared equal to the 2.11.0 alpha, and every alpha install would have been told it was up to date forever. Going straight to 2.11.1 avoids that.

Everything from 2.11.0-ALPHA is included: per-world MSPT, /vdt analyze, ping throttling, pre-generation border clamping and scheduling, and the faster climb defaults.

[2.11.0-ALPHA] 2.11.0-ALPHA by Unknown on Oct 15, 2026
Download
This is an ALPHA. It sits in the alpha channel on purpose: accepted-release-types defaults to release, so this build will not be offered to existing installations unless an admin opts in by hand. Try it on a test server first.

### Per-world MSPT

The server reports one MSPT figure for everything, so a single struggling world used to drag every other world's distance down with it, including worlds that were not the problem. Turn on per-world-mspt and the tick is split between worlds, each judged by the share it actually causes. Off by default; the wiki has the details on what it changes and what it does not.

### /vdt analyze

Reads the server's own change history and MSPT-versus-chunk-count samples, then suggests config values that fit what this specific server has actually shown, instead of guessing from a preset. It only suggests. Nothing it finds is applied automatically.

### Faster climb defaults

The proactive engine used to need ten confirmed checks and moved one chunk at a time, which meant roughly an hour to climb from 4 to 16 chunks even on a server with headroom to spare. Defaults are now three checks and two chunks per step: ninety seconds per +2, so that same climb takes about nine minutes on a healthy server instead of one hour. ticks-per-check is unchanged: that number is about measurement quality, not reaction speed.

### Ping throttling

The same idea as the existing AFK throttling, triggered by a bad connection instead of an idle player. A player whose ping stays above the threshold for a few checks in a row gets reduced personal view and simulation distances, freeing that budget for players whose connection can actually use it. A single lag spike does not trigger it, and recovery is immediate once the connection improves. If a player is both AFK and on a bad connection, the more restrictive of the two distances applies. An explicit viewdistancetweaks.fixed.<n> permission still wins over both throttles, same as before.

### Pre-generation: world border and scheduling

Idle pre-generation now clamps its radius to the world border by default, so a radius entered too large costs a log line instead of generating chunks nobody inside the border can ever reach. Turn respect-world-border off if the plan is to pre-generate now and widen the border later.

/vdt pregen schedule <world> <radius> <HH:mm> starts a run at a fixed time of day instead of only when the server empties out, for servers where "empty" rarely happens. The usual safety nets (MSPT, free memory, players online) still apply once the run is going; only the start condition changes.

### Status

Ping throttling and the pre-generation changes above are new in this alpha and have not yet been run on a live server the way the rest of 2.11.0 has: per-world MSPT and the faster climb defaults were verified on Paper, Purpur and Folia under real player load before this release. Everything in this build passes the full test suite, including three runs with the test order randomized.


A follow-up to 2.10.7. Two of the three fixes here repair code that shipped yesterday, in the config upgrader that 2.10.7 rewrote — both found by testing that rewrite against awkward files rather than by reading it again.

If your config.yml was correctly topped up by 2.10.7, nothing here changes for you.

### The config upgrader did nothing at all on a file with Windows line endings

Java counts a carriage return as a line terminator, so the pattern matching each line left it unconsumed and failed on every single line of a CRLF file. The upgrader then found no blocks to look inside, added nothing, and reported nothing — an outcome indistinguishable from having had nothing to do.

Any config.yml last saved by a Windows editor was silently skipped by 2.10.7's whole reason for existing. The file is now read regardless of its line endings, and keeps them when it is written back.

### A comment after a block's name made that block invisible to the upgrader

A block written as a value — some-option: 4 — owns no child options, and adding some would contradict the value already there, so those are deliberately left alone. A trailing comment looked exactly the same to that check: proactive-mode-settings: # my note was read as holding a value, and the entire block was skipped.

That is precisely backwards. A file somebody has annotated by hand is a file somebody has tuned, which is the one most worth topping up carefully.

### Chunk complexity weights had no built-in default, so the feature could run and do nothing

Every option in this plugin falls back to its documented default when your file does not mention it. complexity-weights was the single exception: with the section absent the list of per-block surcharges stayed empty, so every chunk weighed the same, every world's multiplier came out at exactly 1.0, and use-complexity-weighting ran without changing anything — while /vdt status --weight reported a tidy 1.00x for every world.

Any config.yml written before that block existed was in that state, which is every config not yet topped up by 2.10.7. Turning the feature on did nothing and said nothing.

The thirteen shipped weights are now built in as well, so a missing section means the documented default like everywhere else. A section you have deliberately emptied still stays empty — that is an answer, not an omission — and if the feature is on with no weights at all, the log now says so instead of leaving you to wonder why nothing moved.

### Under the hood

The shipped config.yml and the built-in fallbacks are now compared against each other in the test suite, option by option. They are meant to be identical — otherwise a fresh install and an upgraded one quietly behave differently, depending only on when the server was first set up — and that is now checked rather than assumed. It is what turned up the complexity-weights gap above.

Six defects found by reading the code, plus one the fixes themselves would have introduced. Nothing here was reported by anyone, which is rather the point: every one of them is a case where the plugin did something other than what it said, quietly.

### The config upgrader could not add an option to a block your file already had

2.8.3 taught the plugin to top up an old config.yml instead of telling you to delete it. It worked one level deep: whole top-level blocks were appended, and anything added *inside* a block your file already declares was invisible to it.

Measured against a real 2.6 config, that left **17 options** behind — the entire chunk-complexity feature, which lives under proactive-mode-settings. Ten blocks were appended, the version marker was set to current, and the log said nothing about the seventeen it had not added. Because the marker was now current, the version check returned early on every later start too, so nothing ever mentioned it again.

Absent would have been bad enough. What made it worse: use-complexity-weighting reads false when the key is missing, so anyone who found the option in the documentation and typed it in by hand got a feature that does nothing at all — complexity-weights was missing as well, so every chunk weighed exactly 1.0, every world's multiplier came out 1.0, and /vdt status --weight reported 1.00x across the board with no explanation.

Missing options are now merged into the block that owns them, carrying their comments, at whatever indentation your file already uses, and each one is named in the log. Your own values and comments are untouched, a config.yml.bak is still taken first, and running the upgrade twice changes nothing the second time.

### Updates needed two restarts, and a long-running server never saw them at all

The update check ran exactly once, when the plugin enabled. A downloaded jar is staged in plugins/update/ and installed by the *next* restart — so every update cost two restarts: one to notice and stage it, one to apply it. And a server that stays up for weeks never noticed anything, because its single check happened before the release existed.

There is now a re-check every six hours. The jar is already waiting whenever the server does restart, so one restart is enough. update-notify still decides whether any of this runs, and switching it off takes effect on the next /vdt reload rather than at the next restart.

### The scheduler could only cancel its most recent repeating task

Found while adding the re-check above, which is the first thing this plugin ever needed a second repeating task for. The adapter kept a single task handle, so registering a second one overwrote the first, and shutdown then cancelled only the newer of the two — leaving the engine's per-tick driver running against a plugin that had just been disabled. That would have bitten anyone reloading with a plugin manager rather than restarting.

### /papi info credited the wrong author

The PlaceholderAPI expansion reported EmpireBlocks — the plugin's package name, not a person — while plugin.yml names LucasTHCR. Both now come from the same place, and the test suite compares them.

### Two messages said different things depending on where you read them

Every message exists twice: as a built-in default and in messages.yml. The file wins at runtime, so a line that fell behind there is what servers actually show.

help.update still advertised /vdt update long after /vdt update download existed, so the download argument was missing from the help output on every install. And status.fixed.disabled said "Both per-player override features are disabled" after AFK throttling had been added to the condition it describes — three features, two named.

The two copies are compared in the test suite now, text and all, not just checked for having the same keys.

### Smaller

Messages.get() carried a comment promising it would render what it could rather than throw inside a command handler, and then threw. No call site passes a mismatched replacement list today, which is exactly why the contradiction could sit there unnoticed; the code now does what the comment always claimed.

Six defects, all found by reading the code rather than from a report. Every one of them is a case where the plugin quietly did something other than what it said.

### Pre-generation could charge one run's chunks to the next one, on Folia

A chunk request is asynchronous, so requests are still outstanding when a run is cancelled or finishes. The token that identifies which run a callback belongs to was read inside the task that had already been handed to the chunk's region, instead of before handing it over. On Paper that work runs inline, so the two are the same instant and the guard held. On Folia there is a real gap, and a run that ended inside it left the task reading the token of the run that came *next*.

Those late callbacks then decremented the new run's in-flight count past zero — which widens its batch budget — counted chunks it had not built, and released positions from its outstanding set. That set is what the saved resume point is derived from, so the new run could skip ground nobody had generated, and a restart would not notice. The probe path had always captured the token at dispatch; the generation path does now too.

This only ever affected Folia. If you are on Paper, nothing here changes for you.

### /vdt status --fixed reported "disabled" while it was throttling players

The command was gated on per-player-fixed-view-distance and per-player-fixed-simulation-distance alone. AFK throttling sets its distances through exactly the same per-player machinery, so a server running only afk-player-throttling had players actively overridden while the one command whose job is to show that answered "this feature is disabled" and listed nothing at all.

The rows were already written for it — each entry records whether it came from a permission or from the throttle, and the output has a line for both — only the gate never let anyone reach them. This is the same shape as the sweep bug fixed in 2.7.1: a feature gated on an unrelated flag.

### /vdt pregen pause did nothing to an auto-paused run, and said it had worked

pause() returned early whenever the run was paused at all. That also covers the automatic pause the engine sets for players being online, MSPT over the limit, or free heap under the floor. So typing the command during any of those changed nothing, the reply still said "paused", and the moment the blocker cleared the engine lifted its own pause and the run carried straight on.

An operator pause is now what it claims to be: it takes over an automatic one, and only a pause somebody already set by hand is left alone. /vdt pregen stop never had this problem.

### Idle pre-generation rebuilt the same area forever

When an automatic run finished, the progress file was deleted and nothing was left on record saying the work was done. The next pass over an empty server therefore started the whole thing again on the same spawn centre and the same radius, and kept doing it for as long as nobody logged in.

Every lap re-probes the entire area, and every probe pulls its chunk into memory — 251,001 of them at the default radius of 250. Nothing reported it, because each individual lap looked like an ordinary run.

A completed automatic run is now recorded in pregen-done.json next to config.yml, and the trigger checks it against the area the config is currently asking for. Raising idle-pregeneration.radius, or pointing the feature at a different world, is new work and still runs; deleting the file starts the old area over. Manual /vdt pregen start runs are not recorded and are never blocked by it.

### Smaller things

The chunk-complexity weigher keeps two maps keyed by world name. forgetWorld() cleared the engine's own six when a world unloaded but not those, so a server that creates and discards worlds at runtime — minigame arenas, per-player plots — kept an entry per world for the lifetime of the process.

Predictive pre-loading drops its stored player positions when the MSPT or memory gate holds it back. Speed is measured as the distance since the last sample divided by one tick interval, so keeping the samples across a pause meant the first cycle afterwards charged the entire pause against a single interval: somebody who had walked off and stopped read as flying, along a path they had already finished.

Startup grace measures its end from the current tick instead of from tick zero. That was only correct for the one caller there is today, and silently wrong for any other.

### A mistyped profile name silently dropped the profile you were running

/vdt profile <name> tore the current overlay down before it looked the name up. Type evnt-mode instead of event-mode and the command answers "unknown profile" — but by then every value event-mode had set was already back to what config.yml says, including its max-view-distance-cap. Nothing said so. activeProfile still named the profile, so /vdt status went on reporting event-mode as in effect for the rest of the session, and only a /vdt reload put the real values back.

The name is checked first now. A switch that is rejected leaves the running profile exactly as it was.

### The log line and the chat announcement lied about excluded dimensions

A world with exclude-simulation-distance: true never has its simulation distance touched — the setter is skipped, which is the point. But the console line and the announce-changes-in-chat message were built from the numbers the engine had *asked* for, not the ones the world ended up holding. So a world sitting at simulation distance 12 was announced as "Sim: 12→6" while nothing had moved, and {newSim} in a custom announce-format carried the same wrong figure.

/vdt history reads the same event and always recorded the applied value, so the two contradicted each other on exactly the servers that exclude a dimension. Both now report what the world actually holds.

### Emergency relief could not engage while the memory safeguard was active

emergency-relief is the last thing checkWorld() does, and the increase veto above it returned outright. In proactive mode a world pinned at its minimum still computes a target above it, because the chunk budget says it could afford more — so the cycle counts as an increase, and the memory safeguard refuses it and returns.

That is the exact situation relief exists for: at the floor, still over the threshold, nothing left to give back. It was the one path that could never reach it, and on a server with a tight heap it is the path taken every single cycle. Lifting relief already had its own route out for the same reason; engaging now has one too.

### Smaller things

The version in plugin.yml is filled in from the build rather than written twice, so the number the update checker compares can no longer fall behind the jar it is in.

Three new tests cover the cases above. All 61 pass, and each of the new ones fails against 2.10.4.

### Complexity weighting was doing nothing on Paper

use-complexity-weighting measured every sample correctly and then threw the result away again. The sampling pass published from whichever sample finished last, and wrote a neutral 1.0 afterwards if none were left outstanding — which on Paper is always, because that work runs inline. Every world came out at 1.0 on every cycle, so the option had no effect at all on anything that is not Folia. It has been that way since 2.10.3.

If you have it switched on, this release is the one where it starts working. Worlds full of redstone will now genuinely get a smaller chunk budget than worlds full of water, which is what the setting always claimed to do.

The sample cap had the same root cause and never tripped either, so a world with more players than complexity-sample-chunks was oversampling.

### /vdt status told you nothing was throttled while it was throttling

The player snapshot that /vdt status and /vdt status --fixed read was only refreshed by the distance engine's own cycle, while AFK throttling deliberately runs before that cycle starts. Throughout start-delay, during the five minutes of startup grace, and for as long as the plugin was disabled, the throttle was changing player distances while status reported "AFK players throttled: 0" and --fixed listed nothing at all.

### Pre-generation could skip chunks across a restart

Progress was the probe frontier minus the number of chunks still outstanding. That only points at the right place if the unfinished chunks are the most recently probed ones, and they are not — probes come back out of order. A chunk left waiting at position 5 with the frontier at 100 saved a resume point of 99, and nothing ever built it. Progress now follows the lowest position that is genuinely unfinished, and no longer counts a chunk as done while it is still being generated.

### Smaller

  • A per-player fixed distance that happened to match its world's current distance was hidden from /vdt status --fixed. The reason behind a value is recorded now instead of guessed from the number.
  • The pre-loader takes each player's share of max-chunks-per-cycle before dispatching rather than inside the task, so on Folia forty players can no longer submit far more chunk requests than the cap allows.
  • The "already existed" counter is atomic, like the generated counter beside it — both are written from asynchronous chunk callbacks.
  • A stale probe callback no longer decrements a counter belonging to a run that already ended.
  • The daylight-cycle gamerule is resolved once at load instead of through a constant the Paper API has marked for removal, and emergency relief now names only the measures it actually took rather than always claiming both.

### Folia works

Tested against a real Folia server for the first time, and two features were freezing it outright.

world.isChunkGenerated() blocks inside CraftWorld on a future. On Folia that parks the global region thread on a result only that same thread could have produced, so the server stops ticking: the watchdog reported the global region gone after five seconds and it never came back. Idle pre-generation ran into it on its own the moment the last player logged off, because start-when-empty is on by default. Predictive pre-loading called the same method for any fast-moving player.

Both now ask through getChunkAtAsync(x, z, false), which answers the same question — the chunk if it exists, nothing if it would have to be generated — without holding up a thread and without generating anything. Chunk work is routed to the region that owns the chunk. On Paper every one of these paths runs inline exactly as before.

Player state is read from the thread that owns it. Density scaling and complexity weighting read every player's position from the check cycle, and a world's players can sit in as many regions as there are players, so there was no thread from which those loops could be correct. They now work from positions each region reports for its own players. The per-player distance sweep decided its value on one thread and applied it on another; the whole evaluation moved.

### Pre-generation progress is honest again

Probing ahead is bounded now. A run no longer reports 100% while thousands of chunks are still queued behind it, and the saved position follows the work that is finished rather than how far ahead the search has looked — a restart at that point used to drop everything still in the queue.

### Measured on Folia 1.21.11

Distances regulate, pre-generation runs and resumes, MSPT reads real values, and no watchdog incident occurs with every subsystem switched on.

### /vdt simulate now sees the same vetoes the engine applies

The memory safeguard and the chunk-generation lock both refuse a distance increase inside the check cycle, but neither existed in the preview /vdt simulate builds. On a server sitting under memory-safeguard.min-free-percent, the preview therefore announced an increase that the engine went on to reject — every cycle, with nothing to say why. Both now go through one shared method, so the reason the preview prints is the condition the engine acted on.

### /vdt mspt has a permission of its own

It was the only subcommand falling through to the base /vdt node, while status, history and simulate each required one. It reads the same figures as /vdt status and now shares that node, so no existing grant changes meaning.

### Smaller

  • Removed a duplicate update.ahead key from messages.yml.
  • plugin.yml now says that the reload permission also covers /vdt update.

## ViewDistanceTweaks v2.10.1
> Dynamic view distance and simulation distance adjustment per world, driven by what your server can actually carry at that moment.

Four fixes from a pass over the parts of the plugin the earlier reviews never reached. Nothing here changes how distances are chosen — these are all about a state that could not be left, a measurement that rested on an assumption, and two features that quietly did not work.

### Fixed

  • Emergency relief could stay engaged indefinitely.** Relief clears a world's weather and freezes its clock, so what matters most about it is that it ends. The only path back sat at the end of the per-world check behind six early returns, and two of them stay reachable while relief is active. A world that recovered its tick time while free heap was still low hit the memory-safeguard return every cycle and never reached the check — weather off, clock frozen, for as long as memory was tight. A temporary override did the same thing permanently, because that return is the first thing the check does. Lifting no longer depends on any of it.

  • The MSPT collection window rested on an assumption the API does not make.** It read tickTimes[length - 1] as the tick that had just finished. Paper's contract for that method is *"a sample of the servers last tick times"* — nothing is said about order. A ring buffer written modulo its length, which is the ordinary way to keep the last N of anything, would leave the same reading at that index for a hundred ticks at a stretch, and the window would have recorded it a hundred times over. It now averages the whole sample, which needs no assumption and is right either way.

  • Two downloads could overwrite each other.** The auto-updater is reachable from the startup check and from /vdt update download, both asynchronous, and both wrote the same partial file. The hash mismatch that followed was reported as *"corrupted or tampered with"* — which an admin reads as an attack rather than as the plugin racing itself. A second download is now refused cleanly. The partial file is also removed after any failure rather than only on that one branch, and a download URL that is not HTTPS is refused.

  • The EssentialsX AFK hook depended on load order.** It was resolved once during startup and never again, and EssentialsX was never declared as a soft dependency — so whether it was found at all came down to which plugin Bukkit happened to enable first. Switching afk-use-essentials on and running /vdt reload did nothing until the next restart, with no line to say so. It is declared now, and re-resolved on every reload.

### Also
  • The config shipped with the plugin now uses the same plain-ASCII punctuation as the project page, and the bStats link in the source names the registered project.

### Under the hood
43 tests. Each fix was checked against the previous code first — a test that passes before and after proves nothing.

### Dependencies

### Support
Join our Discord: **dc.gg/paperstream**

  • Author: LucasTHCR*

## ViewDistanceTweaks v2.10.0
> Dynamic view distance and simulation distance adjustment per world, driven by what your server can actually carry at that moment.

The first stable release since 2.8.3, and the one that finally **upgrades your config file instead of complaining about it**. It carries everything the 2.9 betas built and the fifteen bugs they turned up — including one that has been in the release line since 2.4.

### Your config.yml is topped up, not replaced

Bukkit writes the bundled config only when the file is missing, so a server that had ever started kept its original copy forever. Every option added since was simply absent, the feature behind it ran quietly on a code default, and the only advice the plugin could give was "rename the file and restart" — which costs you every value you had tuned. The version marker was never written back either, so the warning came back on every start and every reload with nothing you could do to satisfy it short of editing YAML by hand.

Missing blocks are now appended from the bundled file **with their comments intact**, and the marker moves up so the warning stops. Nothing existing is rewritten — YAML does not care what order top-level keys appear in, so appending cannot change the meaning of what is already there — and a copy is kept as config.yml.bak before the first write. Run against a real 2.8.2 config with hand edits, the only line that changed was version: 1 becoming version: 2. Your values, your comments, and keys this plugin has never heard of all survive.

### New since 2.8.3

  • Idle pre-generation.** Builds terrain ahead of time, so players never wait for it. Runs when the last player logs off, or on demand via /vdt pregen. It throttles against the same rolling MSPT average the distance engine uses, **stops** rather than slows when free heap runs low, and writes progress to disk so a crash or a nightly reboot resumes where it left off. Off by default.
  • Predictive pre-loading.** Loads chunks along the path of a fast-moving player, so arriving somewhere does not stall the tick. It makes chunks ready *server-side* — nobody sees further, you just stop feeling the hitch. Off by default.
  • adjustment-mode: off.** Never touches view or simulation distance at all, for servers that want only the pre-generation and pre-loading subsystems. Those run independently of the distance engine.
  • A readable Operating System chart on bStats**, because the built-in one drills down on the raw Linux kernel string and scatters 90% of servers across ~94 slices.

### Fixed since 2.8.3

  • An out-of-range distance stopped the engine.** /vdt viewdistance 53 was handed straight to a setter that accepts only 2 to 32 and threw. Worse, the override was stored *before* it was applied, so the next check re-applied it and threw again — taking override expiry, per-player overrides and every remaining world check with it, on every tick, until /vdt reload. Present in the release line since 2.4.
  • Per-world state was never cleaned up.** Check counters, cluster bonuses and pending reverts are keyed by world name and nothing removed them, so servers that create worlds at runtime grew those maps for the lifetime of the process.
  • Predictive pre-loading could stop the whole engine** when a tracked player's last-known world had been unloaded.
  • A failed chunk request deleted an entire pre-generation run** — hours of work, with a log line that blamed the wrong thing.
  • Pre-generation tripped the safeguard built to exclude it**, clamping view distance while the plugin generated terrain by itself.
  • /vdt history blamed tick load for changes that had nothing to do with it**, reporting decrease (mspt=1.9) on an idle server. It now names the bound that was actually exceeded.
  • The pre-generation ETA could be off by a factor of a hundred**, reporting 2m 20s for a run with hours left. It follows the generation rate now, and says so plainly while it cannot yet be known.
  • /vdt update reported the wrong version** when up to date, and **messages.yml shipped incomplete** — the whole /vdt pregen command surface existed only as code defaults and could not be customised.
  • Plus the chunk-complexity spiral, the diagonal pre-loading path, /vdt profile restoring properly, and several accounting fixes inside a pre-generation run.

### Under the hood
The test suite went from 9 to **38**. Every fix in this release was verified against the previous code first — a test that passes both before and after proves nothing.

### Dependencies

### Support
Join our Discord: **dc.gg/paperstream**

  • Author: LucasTHCR*

## ViewDistanceTweaks v2.9.4-BETA
> Dynamic view distance and simulation distance adjustment per world, driven by what your server can actually carry at that moment.

Three fixes to how a pre-generation run accounts for itself, one to what /vdt history says a decrease was for, and a readable operating-system chart.

### Fixed

  • Chunks from a finished run were charged to the next one.** Chunk requests are asynchronous, so some are always still outstanding when a run is cancelled or completes. Nothing reset the in-flight counter and the callback counted unconditionally, so those late arrivals landed on whichever run came after: it reported terrain it had never built, and the counter could fall below zero — which widened the next run's batch budget past what the config allows. Each run now carries a token, and callbacks that outlive their run are dropped.

  • The progress file was trusted as written.** A radius beyond the configured maximum — an old file kept after you lowered the limit, or one edited by hand — resumed a run that /vdt pregen start would have refused outright. The same bounds apply now however a run begins.

  • An idle server asked the disk for the progress file several times a second.** The automatic trigger called it on every pass, forever, to be told "no" each time. It looks once.

  • /vdt history blamed tick load for changes that had nothing to do with it.** A server idling at 1.9ms logged decrease (mspt=1.9, mode=reactive) while dropping every world from 10 to 6. The change was correct — the worlds sat above the maximum in config.yml and were brought into range — but on an idle server that line reads as a malfunction, and the history exists precisely to answer "why is my view distance 6". It now names the bound that was exceeded, and only for the value that actually exceeded it:

``
above the configured maximum (sim 10 > 6, view 10 > 8) — brought into range
`

### Added

  • A readable Operating System chart on bStats.** The built-in one drills down on os.version, which on Linux is the raw kernel string — so 90% of servers scatter across roughly 94 slices, the largest holding under 10%, while Windows sits tidily under "Windows 11". That chart is built by bStats and cannot be changed from a plugin, so this adds one beside it keyed on the kernel series: 6.8.0-137-generic and 6.8.0-124-generic` are two slices in the old chart and one population in reality. Unrecognised version strings are passed through rather than dropped.

### Dependencies

### Support
Join our Discord: **dc.gg/paperstream**

  • Author: LucasTHCR*

## ViewDistanceTweaks v2.9.2-BETA
> Dynamic view distance and simulation distance adjustment per world, driven by what your server can actually carry at that moment.

  • Update if you use /vdt viewdistance or /vdt simulationdistance.** A value above 32 did not just fail — it stopped the plugin until the next reload.

### Fixed
  • An out-of-range distance stopped the engine.** /vdt viewdistance 53 was handed straight to World#setViewDistance, which accepts only 2 to 32, and threw. The command checked the lower bound and never the upper one.

The throw was the smaller half of it. The override is registered *before* it is applied, so the rejected value stayed in the map: the next check cycle re-applied it, threw again, and because that call sits inside the tick handler it took everything queued behind it with it — override expiry, per-player overrides and every remaining world check. The plugin was effectively dead until /vdt reload cleared the override, with only a stack trace to say so.

Two more doors led to the same place. Per-world min/max-view-distance, min/max-simulation-distance and the startup-grace values were all read with a lower bound only, so a typo in config.yml reached the same setter from inside the check cycle. The clamp those needed already existed and was applied to the AFK values, the chunk-generation emergency distance and the profile caps — the per-world settings simply never used it.

applyDistances() now clamps as a last resort before the Bukkit setters, so no caller can put the engine in that state again. The command itself refuses an out-of-range value outright instead of clamping, so you are told what happened rather than quietly getting a different number than you typed.

  • The error message for an unparseable value said the opposite of what was wrong.** Typing 53# answered "Invalid value (minimum 2)". It now reports the value back along with both bounds.

  • Pre-generation tripped the safeguard that is meant to exclude it.** A generated chunk reaches the tracker twice, and the pre-gen claim was consumed by the first arrival before the chunk was marked as seen — so the second arrival found no claim and counted it. Every pre-generated chunk fed the chunk-generation safeguard, which then locked distance increases and pinned view distance to the emergency value while the plugin generated terrain by itself. Only affects servers with chunk-generation-safeguard.enabled: true.

  • The shipped messages.yml had fallen behind the code.** Every server logged "25 message key(s) are missing" on load, fresh installs included: the whole /vdt pregen command surface, the update-download messages and help.pregen existed only as code defaults, so none of them could be customised — the keys were not in the file to edit. The advice in that line made it worse, because deleting the file returned the same incomplete copy and discarded any customisation for nothing. The file is complete again, the log names the keys it is falling back on, and the regenerate hint is honest.

  • /vdt update reported the wrong version when up to date.** It printed the newest version in your accepted channels, while the log line directly above it printed the installed one — the same placeholder meaning two different things in two lines from one command. On a beta build with the default accepted-release-types, the newest accepted version is an *older* release, so a server on 2.9.2-BETA was told "up to date (2.8.3-RELEASE)". It reports the installed version now, and says plainly when that version is ahead of anything its channels will offer.

  • The pre-generation ETA could be off by a factor of a hundred.** It divided the remaining chunks by a rate that blends two operations differing by two orders of magnitude: checking whether a chunk exists is a disk lookup, building one is a generation pass. A resumed run crossing terrain it had already built reported 1029 chunks/s and an ETA of 2m 20s while it was genuinely generating at 11/s with hours of work left. /vdt pregen status now shows both rates separately and bases the ETA on generation alone — and says the ETA is not known yet while the run is still only skipping, instead of inventing a number.

### Under the hood
The bounds were already in the engine, respected only on the per-player path — where this exact class of bug was first noticed in 2.5, when an out-of-range permission value took that feature down. They are the same bounds for a world, and are named accordingly now. The regression test fails against the previous build with the exact message from the report.

### Dependencies

### Support
Join our Discord: **dc.gg/paperstream**

  • Author: LucasTHCR*

## ViewDistanceTweaks v2.9.1-BETA
> Dynamic view distance and simulation distance adjustment per world, driven by what your server can actually carry at that moment.

Bugfix release for the 2.9.0 subsystems. **If you run 2.9.0-BETA or 2.9.0-BETA2 with predictive pre-loading or idle pre-generation enabled, update.** Two of these can take the plugin, or a long pre-generation run, out entirely.

### Fixed
  • Predictive pre-loading could stop the whole engine.** A Location holds only a weak reference to its world, and reading that world back throws once the reference is cleared. The pre-loader read it off the sample it had stored for each player, so any player whose last-known world had since been unloaded — a minigame arena, a plot world — threw out of the tick handler and took everything after it with it: temporary override expiry, per-player overrides and every world check, on every tick, for the rest of the session. The only sign was a stack trace. forgetWorld() was written for exactly this case but clears the engine's per-world maps, and this one is keyed by player.

  • A failed chunk request deleted an entire pre-generation run.** Any error from the async chunk request called cancel(), and cancel() deletes the progress file outright — hours of generation on a large radius, gone, with a log line blaming the server jar whatever the real cause had been. Folia is the likely trigger, since getChunkAtAsync can refuse a call issued from the global region scheduler and that is where this runs. It now stops and keeps the progress, so /vdt pregen resume picks the run back up.

  • adjustment-mode: off plus startup-grace pinned distances permanently.** Grace forces distances down and relies on the normal check cycle to raise them again; under off that cycle returns before it computes anything, so nothing ever did. A server running both stayed at the grace values for the rest of its uptime. Grace is part of distance management, so off now skips it and says so in the log.

  • A diagonal flight paid for the same chunk twice.** Rounding a normalised movement vector lands consecutive steps on one chunk for any path that is not axis-aligned, and the duplicate could not be caught by the "already loaded" check because the first request had not completed yet. At 45° roughly a quarter of the lookahead budget went on chunks that were already on their way.

  • One refused chunk request no longer disables pre-loading for the session.** That branch is now reserved for a genuinely missing API; anything else drops the current path and tries again next cycle.

### Under the hood
The pre-loader's path has tests now. Both fail against the previous code: the diagonal case requested eight chunks for six distinct ones, and the unload case requested eight chunks for a world the server no longer had. That is the gap the earlier suite left — it drove pure arithmetic, and neither of these appears until the real method runs against a world.

### Dependencies

### Support
Join our Discord: **dc.gg/paperstream**

  • Author: LucasTHCR*

## ViewDistanceTweaks v2.9.0-BETA2
> Dynamic view distance and simulation distance adjustment per world, driven by what your server can actually carry at that moment.

Follow-up to the 2.9.0-BETA subsystems. One new config option, requested by testers running only idle pre-generation / predictive pre-loading without wanting the plugin to touch view/sim distance at all.

### Added
  • adjustment-mode: off.** Disables automatic sim/view distance management entirely — the world's current distance is left exactly as-is, and only /vdt simulationdistance//vdt viewdistance (temporary overrides) can still change it. Idle pre-generation, predictive pre-loading, AFK throttling, per-player fixed distances and MSPT tracking all keep working independently, since they never depended on the distance-management pipeline in the first place. Useful if you only want the pre-generation/pre-loading subsystems and nothing else.
  • /vdt simulate reports off mode honestly — it does not compute a target it will never apply.

### Dependencies

### Support
Join our Discord: **dc.gg/paperstream**

  • Author: LucasTHCR*

## ViewDistanceTweaks v2.9.0-BETA
> Dynamic view distance and simulation distance adjustment per world, driven by what your server can actually carry at that moment.

  • This is a beta.** Two new subsystems, both off by default. They have had limited testing on real hardware — if you run a server people care about, wait for the stable release or try this on a test instance first. Nothing changes for you unless you switch one of them on.

### Added

  • Idle pre-generation.** Builds terrain ahead of time, so players never wait for it. Generating a chunk is the most expensive thing a server does and the one everything else here can only react to; terrain that already exists is read from disk instead, and the chunk-generation safeguard never has to trip at all.

It runs automatically once the last player logs off, or on demand:

``
/vdt pregen start world 250 centred on the world spawn
/vdt pregen start world 250 --here centred on where you are standing
/vdt pregen status progress, rate and ETA
/vdt pregen stop pause for good, progress kept
/vdt pregen cancel --confirm throw the progress away
`

Three rules keep it from becoming the problem it solves. It throttles against the same rolling MSPT average the distance engine uses, not an instant reading, so the two never form competing control loops. It **stops** rather than slows when free heap runs low, because a slower path to an OutOfMemoryError is still an OutOfMemoryError. And it writes progress to disk, so a crash or a nightly reboot resumes exactly where it left off instead of quietly losing hours of work.

It also excludes its own chunks from the chunk-generation safeguard. Without that the plugin would fight itself: pre-gen creates terrain, the safeguard reads that as players outrunning the generator, and everyone's view distance gets clamped.

Mind the scale — a radius is entered in chunks but squares into the workload. Radius 250 is about 251,000 chunks; radius 1000 is over four million. Match it to your world border rather than picking a round number. The command warns you when a run is large.

  • Predictive pre-loading.** Loads chunks along the path of a fast-moving player, so arriving somewhere does not stall the tick.

To be precise about what this is: it makes chunks ready **server-side**. It does not let anyone see further, because the client still receives only what its own view distance covers. What you notice is the absence of a hitch, not a wider horizon.

Prediction follows the movement vector rather than the look direction. During elytra flight the view swings around constantly while the flight path stays straight, and it is the path that decides which chunks the server will need. Generation is off by default and should stay that way — with it on this stops being a loader and becomes a second, uncontrolled terrain generator driven by whoever is flying fastest.

### Fixed

  • Per-world state was never cleaned up.** Check counters, cluster bonuses, pending reverts and temporary overrides are all keyed by world name and nothing ever removed them. Servers that create worlds at runtime — minigame arenas, per-player plots — grew those maps for the lifetime of the process.
  • version: 1 had sat unread in config.yml since the first release.** Bukkit only writes the default config when the file is missing, so upgrading left every new key absent and the features behind them silently running on code defaults, with no way to notice. The version is now checked on load and missing settings are named individually.
  • The README documented no viewdistancetweaks.vdt.command.profile permission**, despite 2.8.2 splitting it off from the preset node.

### Changed

  • idle-pregeneration.enabled governs only the automatic zero-player trigger. /vdt pregen works regardless — refusing an explicit command because of a config key would be the plugin overruling an operator who had already said what they wanted.
  • pause-when-players-online likewise applies only to automatic runs. A manual run keeps going and stays throttled by MSPT and free memory.

### Under the hood

The project now has a test suite. The
never-reduce-below-players` bug fixed in 2.8.2 — which drove a struggling server to maximum distance — survived three careful code reads and fell out of a headless sweep in minutes. That sweep now lives in the repository: JUnit 5 driving the real distance calculation through dynamic proxies, covering the distance invariants across every mode and option combination, the profile overlay, the config key fallback, version ordering and the chunk arithmetic.

### Dependencies

### Support
Join our Discord: **dc.gg/paperstream**

  • Author: LucasTHCR*

## ViewDistanceTweaks v2.8.3
> Dynamic view distance and simulation distance adjustment per world, driven by what your server can actually carry at that moment.

A configuration change: auto-update is now off by default, and the notification setting has a name that says what it does.

### Changed
  • auto-update is off by default.** Installing a new version unattended is a decision an admin should make deliberately rather than inherit from a default. Set auto-update.enabled: true if you would rather not think about updates again — the feature itself is unchanged and still verifies every download against Modrinth's published SHA-512 before staging it.

  • This only affects new installations.** If you already have a config.yml from 2.8, 2.8.1 or 2.8.2, it contains enabled: true as a literal value and will keep updating automatically. Change it by hand if you want it off.
  • update-checker is now called update-notify**, because that is what it does: it tells you a new version exists, it never installs anything. The old key still works, so an existing config that switched it off stays quiet. Nothing to change unless you want to.
  • /vdt update download still fetches an update on demand whatever these settings say, so turning auto-update off costs you nothing in convenience.

### Dependencies

### Support
Join our Discord: **dc.gg/paperstream**

  • Author: LucasTHCR*

## ViewDistanceTweaks v2.8.2
> Dynamic view distance and simulation distance adjustment per world, driven by what your server can actually carry at that moment.

Bugfix release. The first item is serious if you use never-reduce-below-players — that option could drive a struggling server straight to maximum distance.

### Fixed
  • never-reduce-below-players forced distances UP instead of holding a floor.** It raised the target to the raw player count *after* the step limits and the min/max clamp had already been applied, so nothing downstream could contain it. A world with 20 players jumped from simulation distance 4 to its configured maximum of 12 in a single check — with max-increase-step: 1 — and did so at 120ms MSPT, in every adjustment mode. The more players a world held, the harder it pushed, which is exactly backwards from what an overloaded server needs.

It is a floor now, never a lift: it can hold a decrease back, but it can no longer manufacture an increase, and it can never exceed the current distance. The documented purpose is unchanged — a world sitting at 10 with 10 players still refuses to drop below 10 under load.
  • /vdt profile normal restored almost nothing, and profile switches accumulated.** Profiles overwrote the config values in place with nothing to fall back on. Switching event-mode → night-mode kept every event-mode value the second profile did not happen to mention, and normal reset only the two distance caps. A server therefore stayed on its event thresholds indefinitely after the event, with nothing to indicate it; only /vdt reload recovered. Every profile-writable value is now restored before an overlay is applied, so switching is stateless and normal means what the config and the command have always claimed.
  • /vdt status announced updates by string inequality.** Left over from the 2.8 rewrite, which moved everything else to a numeric comparison. Since getLatestVersion() now returns the newest *compatible* version rather than the newest overall, any difference was reported as an update — a server on a beta build with accepted-release-types at release was told to "update" to the older release it had already passed.
  • Chunk complexity sampling only ever looked in one direction.** The spiral that picks sample chunks around each player derived its offset as ring - abs(dx), which is never negative, so every sample landed in the same half-plane. A hopper array one chunk in the opposite direction was invisible, and with use-complexity-weighting: true that multiplier moves real chunk budget between worlds. The spiral now walks complete rings — indices 0 to 80 cover exactly the 9×9 block around the player, all four quadrants equally.
  • The auto-updater did not check where the update folder points.** An empty settings.update-folder in bukkit.yml resolves to the plugins folder itself, which would have staged the download on top of the running jar instead of beside it. It now refuses and says why.

### Verification
The distance calculation was executed headlessly across 577,155 parameter combinations — every adjustment mode, MSPT from 5 to 120, player counts, step sizes and option combinations — checking that view never falls below simulation distance, that both stay inside their configured bounds, that step limits hold, that profile caps are never exceeded, and that prioritize-simulation-distance never sheds view distance while simulation distance still has room. All hold.

### Dependencies

### Support
Join our Discord: **dc.gg/paperstream**

  • Author: LucasTHCR*

## ViewDistanceTweaks v2.8.1
> Dynamic view distance and simulation distance adjustment per world, driven by what your server can actually carry at that moment.

Bugfix release. Two of these affect anyone using per-world check intervals or the chunk generation safeguard, so updating is worthwhile even if 2.8 looked fine.

### Fixed
  • /vdt simulate did not predict what the engine actually does.** The preview summed each world's weight as chunk-weight × priority, while the live path also divides by the complexity multiplier. With use-complexity-weighting: true the two therefore worked from different totals, and the preview showed proactive targets that never happened — which is precisely what /vdt simulate exists to rule out.
  • Chunk generation was counted twice.** A newly generated chunk fires both ChunkPopulateEvent and ChunkLoadEvent with isNewChunk(), and both were counted. Every rate was roughly double the truth, so chunks-per-second-threshold: 15 actually tripped at around 7 real chunks per second, and /vdt status reported inflated numbers. The safeguard was consistently twice as aggressive as configured. Both listeners are still in place — the second one covers server paths where populate does not fire — but a chunk is now counted once per window.
  • A world with its own, shorter ticks-per-check ran its first check on placeholder values.** The shared snapshot (MSPT, weight sum, variance) was only refreshed one full global ticks-per-check after the start delay. Any world configured to check more often than that fired first and read the startup placeholders: an MSPT of 50, which sits above the default decrease threshold, so it cut distances on a perfectly healthy server, with a weight sum of 0.0001 feeding the proactive formula on top. This happened after every start, every /vdt reload and every /vdt enable. Servers on the default config were not affected.
  • /vdt profile required the preset permission.** Those are not the same level of trust: /vdt preset rewrites your config.yml on disk, while /vdt profile only lays a temporary overlay that normal undoes. Granting someone the in-event switch also granted them the config file. It now uses its own node, viewdistancetweaks.vdt.command.profile.

### Dependencies

### Support
Join our Discord: **dc.gg/paperstream**

  • Author: LucasTHCR*

## ViewDistanceTweaks v2.8
> Dynamic view distance and simulation distance adjustment per world, driven by what your server can actually carry at that moment.

This release adds an auto-updater and rewrites the update check that sits underneath it.

### Added
  • Auto-update.** The plugin now keeps itself current. It is **on by default**; set auto-update.enabled: false near the top of config.yml if you would rather approve every update yourself.

The running jar is never modified. A new release is downloaded into Bukkit's update folder (plugins/update/) under this plugin's own file name, and your server installs it on the next **start**. So it is downloaded now and active after your next restart. No plugin can safely swap its own jar underneath a running server, and this one does not pretend otherwise.

It only ever touches ViewDistanceTweaks. The single file it can write is named after this plugin's own jar, so it cannot replace or stage anything for another plugin.
  • Downloads are verified before they are installed.** Every file is checked against the SHA-512 hash Modrinth publishes for it, and discarded on a mismatch rather than moved into place. Size limits and the declared file size are enforced too.
  • auto-update.require-matching-game-version** (default true) only accepts releases that list the Minecraft version your server actually runs.
  • auto-update.accepted-release-types** (default release) controls which Modrinth channels may be installed. Add beta if you want fixes early.
  • /vdt update download** stages an update immediately, even with auto-update switched off, so you can test the whole path without editing the config and restarting.
  • update-checker** is now documented in config.yml. It has been read since it was introduced but appeared in no shipped config, so the only way to find it was reading the source.

### Changed
  • The update check was rewritten.** It used to pull the first version_number out of the raw JSON with indexOf and compare it to the running version with equals. That is enough to print a notice, but not to decide what to install: it ignored which Minecraft versions and loaders a release declares, treated any difference as "newer" so a rolled-back release looked like an update, and would have matched that string anywhere in a changelog.

It now parses properly, filters by loader, game version and release channel, and compares versions numerically, so **2.10 correctly sorts above 2.9** instead of below it.
  • Both the check and the download run entirely off the main thread.

### Dependencies

### Support
Join our Discord: **dc.gg/paperstream**

  • Author: LucasTHCR*

## ViewDistanceTweaks v2.7.1
> Dynamic view distance and simulation distance adjustment per world — lightweight, fully configurable, and built for performance-critical servers.

Bugfix release for the features introduced in 2.7. If you enabled **AFK throttling** or **chunk complexity weighting** in 2.7, please update — both were broken. Everything else in 2.7 was unaffected.

### Fixed
  • AFK player throttling did nothing unless another feature happened to be enabled.** The periodic sweep that applies it was gated on the two per-player fixed-distance options, which are off by default — so switching on *only* afk-player-throttling, the most obvious way to use it, silently had no effect at all.
  • Chunk complexity weighting worked in reverse.** The cost multiplier was applied so that an expensive world received a *larger* share of the chunk budget instead of a smaller one — roughly four times the real cost for a world with twice the per-chunk cost, while starving the cheap world that could actually have afforded it. Expensive chunks now correctly mean fewer of them.
  • Emergency relief could stay engaged forever.** Running /vdt disable while relief was active left that world's weather cleared and its daylight cycle frozen indefinitely; only a reload or restart brought them back. Relief is now released as soon as the plugin is switched off.
  • /vdt status --fixed did not list AFK-throttled players**, despite that command existing precisely to show which players the plugin is currently overriding. Each entry now also states whether the value came from a permission or from the AFK throttle.
  • Chunk generation tracking was not thread-safe on Folia**, where chunk events fire on region threads: the counter maps could be corrupted or throw ConcurrentModificationException mid-cycle.

### Performance
  • The cluster bonus is computed once per check cycle instead of three times, each of which ran an O(n²) pass over a world's players.
  • Complexity sampling no longer allocates an array of every loaded chunk (15k+ entries on a busy server) just to inspect 40 of them. It now samples around players, which is both cheaper and a better sample — the chunks that cost tick time are the ones near players.
  • Removed two unused methods left over from 2.7.

### Dependencies

### Support
Join our Discord: **dc.gg/paperstream**

  • Author: LucasTHCR*

## ViewDistanceTweaks v2.7
> Dynamic view distance and simulation distance adjustment per world — lightweight, fully configurable, and built for performance-critical servers.

  • Every new feature below is off by default.** Updating from 2.6 without touching your config changes nothing about how the plugin behaves.

### New
  • AFK player throttling** — a player standing still at spawn doesn't need the same distances as one flying an elytra. Idle players drop to configured minimums and the chunks they free up go back into the global budget for players who are actually playing. Uses Paper's own idle timer, or EssentialsX's AFK state when that plugin is present so /afk counts immediately. An explicit viewdistancetweaks.fixed.<n> permission wins over the throttle.
  • Chunk generation safeguard** — MSPT variance detection notices the stutter that terrain generation causes, but not the cause. This counts chunks that are genuinely being *generated* (not merely read from disk) and can lock increases and hard-clamp view distance while someone crosses unexplored terrain at elytra speed.
  • Density-based scaling** — twenty players scattered across a world load twenty separate rings of chunks; twenty players around one community farm load almost the same chunks. The flat per-player formula can't tell those apart and starves clustered worlds of distance they could easily afford. Players within a configurable radius now grant bonus distance, scaled by how much of the world's population is actually in that cluster.
  • Emergency relief** — last resort for a world that has run out of distance to give back. Once simulation *and* view distance sit at their minimum and the server is still overloaded, thunderstorms are cleared and the daylight cycle frozen. Both are restored once MSPT recovers, and each world's original weather and doDaylightCycle setting is remembered — a world you deliberately kept frozen stays frozen.
  • Memory safeguard** *(the one exception: on by default)* — distances don't only cost CPU, every extra ring of chunks stays resident in the heap. A server can sit at a perfect 20 ms MSPT and still be one increase away from spending every tick in garbage collection. While free heap is below the configured percentage, all increases are blocked regardless of MSPT; decreases always stay allowed.
  • Profiles** — named overlays for thresholds, step sizes and hard distance caps, switchable in-game with /vdt profile <name> instead of editing the config mid-event. Ships with event-mode and night-mode examples. Caps apply after every other rule, so a cap of 8 genuinely means 8.
  • Chunk complexity weighting** — an ocean chunk costs almost nothing; a chunk holding a 500-hopper sorting system can dominate a tick on its own, yet the old formula counted both as one unit. Chunks are now weighted by their tile entities. Inspecting chunks is itself work, so this samples a bounded number per world rather than scanning everything.

### Changed
  • Relicensed from MIT to **GPLv3**.
  • /vdt status gained lines for the active profile, free heap, throttled AFK players, active emergency relief, per-world chunk generation rate, cluster bonus and complexity multiplier — each shown only when the matching feature is enabled.
  • New /vdt profile [name] subcommand with tab-completion.

### Dependencies

### Support
Join our Discord: **dc.gg/paperstream**

  • Author: LucasTHCR*

ViewDistanceTweaks Icon
Download ViewDistanceTweaks
Select game version
Select platform