Tickets and loading
Verified against Minecraft 26.3 · Part IV · A player walks one block east across a chunk boundary, and a column of chunks thirteen past the edge of view is asked for.
A player standing at the eastern edge of a chunk takes one step. On the
server, ChunkMap.move notices that the player’s section changed, and
before the tick is out a column of twenty-one chunks to the east has been
asked for. The nearest of them was already generated, lit and running its
scheduled ticks, and now gets its random ticks and its entities as well. The
furthest, thirteen chunks
past the ticket that
asked for it, will exist only as a ChunkHolder at level 44, allowed no
further than ChunkStatus.STRUCTURE_STARTS and never becoming a
LevelChunk at all. Nothing in that machinery ever asks for a
chunk because it is loaded — even ChunkLevel.isLoaded is a question
about a number. Everything asks for a chunk at a level, and the ticket
system decides what the level means. That is the whole design, and it has
one consequence a player can see: there are two graphs reading one ticket
store, and they answer different questions — so a chunk can be
FullChunkStatus.ENTITY_TICKING by every measure the holder knows and tick
nothing, because render distance is how far you can see and simulation
distance is how far the world is alive.
The cast
| class | what it decides | thread |
|---|---|---|
TicketStorage | which tickets exist on which chunk, and which survive a restart — it is the chunk_tickets SavedData | Server |
DistanceManager | owns both graphs, the two player-radius trackers and the four-slot throttle | Server |
LoadingChunkTracker · SimulationChunkTracker | the two graphs: ChunkTrackers over DynamicGraphMinFixedPoint, one level higher per ring | Server |
ChunkLevel | the number line — which level means which FullChunkStatus and which generation status | static |
ChunkHolder | one chunk’s level and its three futures | Server; futures complete on workers, confirmations hop back |
ChunkMap | the holders (an updating map and a visible clone), each player’s ChunkTrackingView, unloads | Server; workers read the visible map |
ServerChunkCache | the level’s ChunkSource: the tick slot, the synchronous ServerChunkCache.getChunk, and a BlockableEventLoop pinned to the Server thread | Server |
PlayerChunkSender | the per-player batches, paced by the client’s acknowledgements | Server |
From a ticket to a future
flowchart TD
SRC["a ticket source: a player, a portal, a pearl, the dragon, /forceload"]
TS["TicketStorage: one ticket on one chunk"]
LCT["LoadingChunkTracker floods out to level 45"]
SCT["SimulationChunkTracker floods out to level 33"]
UCS["ChunkMap.updateChunkScheduling makes the holders"]
UF["ChunkHolder.updateFutures arms the three futures"]
RANGE["DistanceManager.inBlockTickingRange,<br/>DistanceManager.inEntityTickingRange"]
ALIVE["the chunk is alive: it exists, and it ticks"]
SRC --> TS
TS -- "FLAG_LOADING" --> LCT
TS -- "FLAG_SIMULATION" --> SCT
LCT --> UCS
UCS --> UF
SCT --> RANGE
UF --> ALIVE
RANGE --> ALIVE
One ticket, two graphs, and a chunk that is only alive where both arms agree: the left arm decides what a chunk may become and the right one whether it does anything, and neither consults the other. A chunk the left arm carries to block-ticking level and the right one does not exists, is lit and is sent, and is inert.
The figure is the page. A ticket lands on one chunk; each graph the ticket’s flags name floods outward from it; the loading graph’s levels decide which holders exist and what futures they arm; the simulation graph’s levels decide what ticks. The rest of the page is one section per decision on that path — and the four statuses the left arm climbs are the next figure’s.
What a ticket asks for
A Ticket is a TicketType, a Ticket.ticketLevel and a countdown,
Ticket.ticksLeft. Its identity is the pair (type, level): there is no key
object and no owner, and re-adding an identical ticket only
Ticket.resetTicksLeft. The type is a record of a timeout and a set of
flags, registered in BuiltInRegistries.TICKET_TYPE, and the flags say what
the ticket does: TicketType.FLAG_LOADING feeds the loading graph,
TicketType.FLAG_SIMULATION the simulation graph, TicketType.FLAG_PERSIST
writes it to disk, TicketType.FLAG_KEEP_DIMENSION_ACTIVE stops the level’s
empty-tick countdown and TicketType.FLAG_CAN_EXPIRE_IF_UNLOADED lets its
countdown run even while the chunk it asked for is still loading. There are
exactly nine
types, and every reason a chunk is ever loaded is one of them:
| type | timeout | loads | simulates | keeps dimension active | persists | who adds it |
|---|---|---|---|---|---|---|
TicketType.PLAYER_LOADING | — | ✓ | DistanceManager.PlayerTicketTracker, one per chunk within the server’s view distance | |||
TicketType.PLAYER_SIMULATION | — | ✓ | ✓ | DistanceManager.addPlayer, the player’s own chunk | ||
TicketType.FORCED | — | ✓ | ✓ | ✓ | ✓ | /forceload, and a game test over its structure, via TicketStorage.updateChunkForced |
TicketType.PORTAL | 300 | ✓ | ✓ | ✓ | ✓ | Entity on portal travel, radius 3 |
TicketType.ENDER_PEARL | 40 | ✓ | ✓ | ✓ | ServerPlayer, the pearl’s chunk, radius 2 | |
TicketType.DRAGON | — | ✓ | ✓ | EnderDragonFight, radius 9 | ||
TicketType.PLAYER_SPAWN | 20 | ✓ | PrepareSpawnTask during configuration, radius 3 | |||
TicketType.SPAWN_SEARCH | 1 | ✓ | PlayerSpawnFinder | |||
TicketType.UNKNOWN | 1 | ✓ | a synchronous ServerChunkCache.getChunk that may generate, at the level for its target status |
Two things in that table are easy to read past. The ticket that keeps a
dimension alive is the player’s simulation ticket, not the loading
tickets: TicketStorage.shouldKeepDimensionActive feeds
ServerChunkCache.hasActiveTickets, which resets ServerLevel.emptyTime,
the counter that past 300 makes a dimension skip its entity loop, its block
entities and the dragon fight (the level
tick).
And only
two types come back after a restart: TicketStorage.packTickets writes the
types that TicketType.persist, so forced and portal tickets are in the
dimension’s chunk_tickets file and everything else evaporates. On
shutdown TicketStorage.deactivateTicketsOnClosing parks every ticket
except TicketType.UNKNOWN in TicketStorage.deactivatedTickets, the
persisting ones are written from there, and on the next start
TicketStorage.activateAllDeactivatedTickets replays what the file held
during MinecraftServer.prepareLevels.
Which players place the two player tickets at all is asked again on every
move. ChunkMap keeps its players in a PlayerMap that records each as
ignored or not (PlayerMap.ignorePlayer, PlayerMap.ignored), and
ChunkMap.move compares that record with a fresh ChunkMap.skipPlayer — a
spectator, unless GameRules.SPECTATORS_GENERATE_CHUNKS says otherwise. So,
with that rule off, entering or leaving spectator mode takes a player out of
the distance manager,
or puts them back, at their next move.
The number line
ChunkLevel is the scale every ticket is measured on, and it runs down: the
lower a chunk’s level, the further the server takes it. Its thresholds are
declared and its ceiling is derived. ChunkLevel.byStatus gives 31 for
FullChunkStatus.ENTITY_TICKING, 32 for FullChunkStatus.BLOCK_TICKING
and 33 for FullChunkStatus.FULL. Above 33 a chunk is
FullChunkStatus.INACCESSIBLE but still generating: the FULL step of
ChunkPyramid.GENERATION_PYRAMID needs a neighbourhood of eleven
(ChunkLevel.RADIUS_AROUND_FULL_CHUNK, computed from the pyramid), so
ChunkLevel.MAX_LEVEL is 44 and ChunkLevel.generationStatus maps
34 … 44 onto ever-earlier ChunkStatuses — but not one per level:
ChunkStatus.INITIALIZE_LIGHT at 34, ChunkStatus.TERRAIN at 35,
ChunkStatus.BIOMES at 36, and ChunkStatus.STRUCTURE_STARTS for all eight
of 37 … 44. ChunkStatus.FEATURES is on that list nowhere. Level 45 means no
holder. Change the pyramid and the loading radius changes with it.
Thirteen chunks past a level-31 ticket get a holder: two rings to reach level 33, and eleven more because FULL needs that many neighbours generated.
Two graphs, one store
Both graphs are ChunkTrackers over the same TicketStorage, but each
reads only the tickets whose flags name it, and each is asked a different
kind of question:
| the question | who asks it | which graph answers |
|---|---|---|
| does a holder exist, and how far may it generate | ChunkMap.updateChunkScheduling, GenerationChunkHolder.updateHighestAllowedStatus | loading |
| may this chunk be sent to a player | ChunkMap.onChunkReadyToSend, from the level-32 future | loading |
| do its blocks and fluids tick | ServerLevel.shouldTickBlocksAt | simulation |
| do its entities tick | the entity loop, through DistanceManager.inEntityTickingRange | simulation |
| which chunks does the level walk for random ticks | ChunkMap.forEachBlockTickingChunk, a wrapper over DistanceManager.forEachEntityTickingChunk | simulation |
| may mobs spawn here | DistanceManager.naturalSpawnChunkCounter, feeding ChunkMap.anyPlayerCloseEnoughForSpawning | neither: a fixed radius-8 tracker of its own |
The last row is the third radius, and it is the only one no setting moves:
the tracker’s radius of 8 is a constant. It is not the only gate, though —
ChunkMap.collectSpawningChunks keeps a candidate only if the holder has a
ticking chunk and some non-spectating player is within 128 blocks of it. And
the two graphs have different sizes: the loading graph
runs 0 … 45 and the simulation graph 0 … 33, where 33 is not a ticking level
but the tracker’s word for no simulation ticket at all —
SimulationChunkTracker.setLevel drops the entry at 33 and the map answers
33 for anything absent. A TicketType.PLAYER_LOADING ticket puts a chunk at level 31 in the
loading graph — FullChunkStatus.ENTITY_TICKING by holder status, all three
futures armed — and contributes nothing to the simulation graph, which is
why the far edge of a large render distance is generated, lit, sent and
inert.
The four statuses
The left arm of that figure climbs a ladder of four, and the ladder is not symmetric: going up it costs a future and coming down it costs nothing.
stateDiagram-v2
[*] --> INACCESSIBLE : level 44 or below, a holder is made
INACCESSIBLE --> FULL : level 33, ChunkHolder.fullChunkFuture succeeds
FULL --> BLOCK_TICKING : level 32, ChunkHolder.tickingChunkFuture succeeds
BLOCK_TICKING --> ENTITY_TICKING : level 31, ChunkHolder.entityTickingChunkFuture succeeds
ENTITY_TICKING --> BLOCK_TICKING : level above 31, immediate
BLOCK_TICKING --> FULL : level above 32, immediate
FULL --> INACCESSIBLE : level above 33, immediate
INACCESSIBLE --> [*] : level above 44, the holder is dropped
note left of INACCESSIBLE : every promotion waits for a future, every demotion is immediate
The four values of FullChunkStatus, not the ten of ChunkStatus, read as
two columns of arrows: going down the page costs a future and can take many
ticks, coming back up costs nothing and happens inside the one call. That
asymmetry
is why a chunk stops ticking the instant you walk away and starts again slowly.
ChunkHolder.updateFutures compares ChunkLevel.fullStatus of
ChunkHolder.oldTicketLevel (what the futures reflect) with that of
ChunkHolder.ticketLevel (what the graph last said). Each threshold crossed
upward arms one future — ChunkMap.prepareAccessibleChunk at 33,
ChunkMap.prepareTickingChunk at 32, ChunkMap.prepareEntityTickingChunk
at 31 — and a chunk that goes from 45 to 31 in one update arms all three at
once. Every one is wrapped by ChunkHolder.scheduleFullChunkPromotion, so
that success fires ChunkMap.onFullChunkStatusChange on the Server thread,
and chained into ChunkHolder.addSaveDependency, so the chunk cannot be
saved or unloaded mid-promotion. Under the hood each future is a
GenerationChunkHolder.scheduleChunkGenerationTask — the task claims its 529
before it runs
anything.
What those futures carry is a ChunkResult — a two-case result type of
ChunkResult.Success or ChunkResult.Fail, the second holding a string
supplier rather than an exception. Every one of the holder’s three futures is
completed with one, and ChunkHolder.UNLOADED_LEVEL_CHUNK is simply the shared
failure whose message is Unloaded level chunk. A chunk that never arrives is
a value the waiter is handed, not an error, until ServerChunkCache.getChunk,
asked to load or generate, turns it into one.
Demotion is the asymmetry. A threshold crossed downward completes the
matching future with ChunkHolder.UNLOADED_LEVEL_CHUNK and
ChunkHolder.demoteFullChunk fires the status change now, cancelling any
promotion still pending. Entities in a chunk whose level rose past 31 stop
ticking in the same update; nothing waits for a worker.
What each promotion means when it lands: at FULL,
PersistentEntitySectionManager.updateChunkStatus makes the chunk’s
entities Visibility.TRACKED and queues their data to load (Part VI). At
BLOCK_TICKING the continuation runs LevelChunk.postProcessGeneration,
ServerLevel.startTickingChunk — which is LevelChunk.unpackTicks, the
saved scheduled ticks becoming real — and ChunkMap.onChunkReadyToSend. At
ENTITY_TICKING, PersistentEntitySectionManager.startTicking puts the
entities on the EntityTickList.
When the graphs run
All of it on the Server thread, in three slots (and a few one-off callers — a save, a load with a radius, the load counter at start-up and at each join):
- The tick.
ServerChunkCache.tick, from the level tick, runsTicketStorage.purgeStaleTicketsand thenServerChunkCache.runDistanceManagerUpdates. - Idle time. When the server’s own queue has nothing to run and the
server is ahead of its schedule, blocked or sprinting,
MinecraftServer.pollTaskInternaloffers the levels’ServerChunkCache.MainThreadExecutor.pollTaska turn, one after another until one runs something; that poll runs the distance updates first and, if they did any work, returns at once. - A synchronous ask.
ServerChunkCache.getChunkfrom anywhere on the Server thread checks a four-entry cache, thenServerChunkCache.getChunkFutureMainThreadadds aTicketType.UNKNOWNticket and blocks on the future it arms.
The second slot has a priority buried in it: the light schedule and the one
queued chunk task happen only on a poll where the graphs were already
settled, so propagation does not share the queue with chunk work — it starves
it until quiescent. The third has an ordering: if ServerChunkCache.chunkAbsent,
the distance updates run synchronously so that the holder exists inside the
call, and BlockableEventLoop.managedBlock then waits. The Server thread does
not simply sleep on a chunk: it runs the chunk source’s own tasks while it
waits, and off-thread callers are bounced to the Server thread and joined.
Inside DistanceManager.runAllUpdates the order is fixed: the spawn
counter, the simulation tracker, the player ticket tracker, the loading
tracker, and then two passes over DistanceManager.chunksToUpdateFutures
— GenerationChunkHolder.updateHighestAllowedStatus for every changed
holder first, ChunkHolder.updateFutures for every holder second — because
a holder’s range future depends on its neighbours’ allowed status. Nothing
here adds a ticket from a worker. The throttle’s
ThrottlingChunkTaskDispatcher is built over a TaskScheduler wrapping
DistanceManager.mainThreadExecutor, so the ticket task it releases runs on
the Server thread; only the dispatcher’s own priority-queue bookkeeping
runs on the worker pool.
The walk east
View distance 10, simulation distance 10.
sequenceDiagram
participant SGPL as ServerGamePacket<br/>ListenerImpl
participant CM as ChunkMap
participant DM as DistanceManager
participant TS as TicketStorage
participant CH as ChunkHolder
participant PCS as PlayerChunkSender
SGPL->>CM: move: the section changed
CM->>DM: removePlayer(old) then addPlayer(new)
DM->>TS: PLAYER_SIMULATION ticket moves, level 21
CM->>SGPL: ClientboundSetChunk<br/>CacheCenterPacket
Note over DM: DistanceManager.<br/>runAllUpdates, this tick<br/>or the next idle poll
DM->>DM: the simulation graph floods, entity range 10, block range 11
DM->>DM: DistanceManager.<br/>PlayerTicketTracker:<br/>21 in, four at a time,<br/>21 out at once
DM->>TS: PLAYER_LOADING added at 31 east as the throttle lets each through, removed west as each is released
DM->>CM: the loading graph floods out to level 44
DM->>CH: updateFutures: each eastern column a level lower, the new one 32 to 31
CH-->>CM: a later tick, at BLOCK_TICKING: onChunkReadyToSend
CM->>PCS: markChunkPendingToSend, per player in view
CH-->>DM: ENTITY_TICKING done, the throttle slot freed
PCS->>SGPL: send: batch start, nearest first, batch finish
One step east, from the packet to a chunk on the wire: everything above the note bar happens inside handling one packet, where, of its chunks, the client hears only the new centre and a forget for each delivered chunk that left the view, and everything below it waits for the distance manager to be asked to run its updates. The two dotted arrows are the only two things that come back, and both come back late.
The move is ServerGamePacketListenerImpl.handleMovePlayer →
ServerGamePacketListenerImpl.handlePlayerPositionChange →
ServerChunkCache.move → ChunkMap.move, which updates every
ChunkMap.TrackedEntity for the player and then compares
ServerPlayer.getLastSectionPos with the new SectionPos.
DistanceManager.removePlayer finds the old chunk’s
DistanceManager.playersPerChunk set empty and removes the
TicketType.PLAYER_SIMULATION ticket; DistanceManager.addPlayer mirrors
it at DistanceManager.getPlayerTicketLevel, 31 minus the simulation
distance. TicketStorage.addTicket compares the new ticket’s level against the
lowest each graph the flags name already had, and tells the registered
TicketStorage.ChunkUpdated listener only when the new one is lower; the
removal path is the one that recomputes a minimum. Either way the listener
only queues the change; the flood happens in
DistanceManager.runAllUpdates.
The simulation graph settles first and needs no futures and no IO: level 31
out to distance 10, 32 at 11, and from that moment
DistanceManager.inEntityTickingRange and
DistanceManager.inBlockTickingRange answer differently for the far
western edge, whose block ticking has already stopped. Then
DistanceManager.PlayerTicketTracker.runAllUpdates floods its own radius-32
graph. For each chunk newly within DistanceManager.PlayerTicketTracker.haveTicketFor
it submits a task to the dispatcher at priority = distance; for each chunk
that left, it releases the slot with a continuation that removes the
TicketType.PLAYER_LOADING ticket. The dispatcher lets four through at
a time; each adds a ticket at DistanceManager.PLAYER_TICKET_LEVEL, 31, and
records the key in DistanceManager.ticketsToRelease. The loading graph
floods from it — 31 at the chunk, 32 and 33 in the rings, then 34 … 44
eleven chunks further east — and LoadingChunkTracker.setLevel →
ChunkMap.updateChunkScheduling creates a ChunkHolder for every chunk
whose level dropped to 44 or below, or resurrects one from
ChunkMap.pendingUnloads. Finally ChunkMap.promoteChunkMap publishes the
new holders to the visible map and ServerChunkCache.clearCache.
The release is what makes sprinting outrun the loader by design.
DistanceManager.runAllUpdates does not wait for the entity-ticking
future: on a pass where nothing else needed updating, it hangs a
continuation on each pending key’s future and clears the set. A busy tick
defers every release, and the slot frees whenever the already-attached
future completes, so at most four TicketType.PLAYER_LOADING tickets are in flight at once, and
they are the four nearest waiting.
The west unloads without a timeout. The removed loading tickets raise every
western column a level: those crossing 31, 32 and 33 demote at once, their
futures completing with ChunkHolder.UNLOADED_LEVEL_CHUNK, and the one pushed
past 44 goes to ChunkMap.toDrop → the next
ChunkMap.tick with the time supplier runs ChunkMap.processUnloads →
ChunkMap.scheduleUnload, save and ServerLevel.unload
(a chunk nobody needs any
more). If a ticket re-adopts
the chunk first,
ChunkMap.updateChunkScheduling pulls it back out of
ChunkMap.pendingUnloads and the unload task finds nothing to do.
Which chunks a player is owed, and what makes one eligible
Sending is Part IX’s (chunks arrive on a loop the client paces has the batches, the acknowledgement limit and the rate the client asks for). What belongs here is the other half: which chunks are in a player’s set at all, and what makes one eligible to go — because both are answers the ticket system gives.
| the moment | what the ticket system decides | the gate |
|---|---|---|
| the player crosses a section boundary | the two crescents: which chunks entered the view and which left | ChunkMap.updateChunkTracking returns early on the same centre and view distance, and ChunkMap.applyChunkTrackingView sends ClientboundSetChunkCacheCenterPacket only when the centre moved |
| a chunk enters the view | ChunkMap.markChunkPendingToSend, but only for a chunk that already exists | ChunkMap.getChunkToSend wants a ticking chunk; a fresh one waits for its promotion instead |
| a chunk reaches BLOCK_TICKING | ChunkMap.onChunkReadyToSend makes it pending for every player whose view holds it | ChunkHolder.sendSync, which starts complete; the one thing that delays it is ChunkMap.waitForLightBeforeSending, whose single caller is EnderDragonFight after building the exit portal |
| a chunk leaves the view | ClientboundForgetLevelChunkPacket | only if it was not still pending, and only to a living player — you cannot forget what was never delivered |
| a block changes in a chunk not yet delivered | nothing at all | ChunkMap.isChunkTracked is false while the chunk sits in PlayerChunkSender.pendingChunks; the full chunk will carry it |
| the settings change | a radius moves, for every player at once | PlayerList.setViewDistance and PlayerList.setSimulationDistance, the second swapping every simulation ticket’s level through TicketStorage.replaceTicketLevelOfType |
The third row is the join between the two systems, and it is why this table is here: a chunk becomes sendable at the loading graph’s level 32, the number at which its blocks would tick, but ticking asks the simulation graph, so beyond simulation distance a player is sent chunks that do not tick.
Two shapes hide in that table. The view is a rounded square, not a disc:
ChunkTrackingView.isWithinDistance subtracts a buffer of two from each
axis before squaring, so at view distance 10 it reaches eleven chunks
along the axes and nine on the diagonal. And view distance shapes what is
sent, not what is loaded: ChunkMap.getPlayerViewDistance clamps a
player’s request to the server’s, but the ticket tracker’s radius comes from
the server view distance through DistanceManager.updatePlayerTickets.
Singleplayer skips the size cap and not the pacing, which is Part IX’s to
explain.
When a ticket dies
| the ticket | dies when |
|---|---|
| no timeout — player, forced, dragon | its source removes it: the player leaves the chunk, /forceload remove (or a game-test batch finishing, which unforces every forced chunk in its level), the fight ends |
timed and TicketType.canExpireIfUnloaded — only TicketType.UNKNOWN | the countdown runs every tick regardless, so it can expire before the chunk it asked for loads; ServerChunkCache.addTicketAndLoadWithRadius refuses such types for that reason |
| timed, everything else — portal, pearl, spawn | the countdown runs only while there is no holder at all or the holder ChunkHolder.isReadyForSaving; a portal ticket never expires under a chunk still loading, and one over a chunk nothing tracks expires normally |
| the server stops | every type but TicketType.UNKNOWN is parked; only the persisting types reach disk, and only they are replayed on the next start |
TicketStorage.purgeStaleTickets runs from ServerChunkCache.tick and
applies exactly those rules — once a tick, unless the level is frozen and
chunk ticking is on, in which case it does not run at all.
Questions players ask
Why does turning my render distance down not help the server? Because the server loads to its view distance, not yours. Your request only clamps what you are sent.
Why does the world load in a square? Because the graph floods in Chebyshev rings, and every ring is a square. What you are sent is not quite one: the tracking view is a square with its corners cut by the buffer-of-two test above.
Why do mobs spawn where I did not expect? The spawn set is a fixed radius of eight around each player, on a tracker that reads neither graph and no setting.
Why does a portal keep its chunks after I have gone? A
TicketType.PORTAL ticket lasts 300 ticks, persists across restarts with
its remaining Ticket.ticksLeft, and only counts down while its chunk is
saveable.
Do spectators load chunks? By default, yes:
GameRules.SPECTATORS_GENERATE_CHUNKS is on. Turn it off and a spectator
places no player ticket — they are sent the chunks other tickets already hold
at block-ticking level and, once in the world, load none, not even from disk.
For a 1.21-era reader. There is no LIGHT, PLAYER, START or POST_TELEPORT ticket, and there is no forced-chunks file:
TicketStorage(inworld/level, notserver/level) is the saved data now. The two graphs are separate top-level classes, not inner classes ofDistanceManager, and the wholeChunkHoldergeneration half is a superclass,GenerationChunkHolder.
Where to look
Start where a ticket does: TicketType · TicketStorage.addTicket ·
ChunkLevel.byStatus. Then the two graphs and the pass that runs them:
LoadingChunkTracker · SimulationChunkTracker ·
ChunkTracker.computeLevelFromNeighbor · DistanceManager.runAllUpdates.
Then what a level change does to one chunk: ChunkMap.updateChunkScheduling ·
ChunkHolder.updateFutures · ChunkMap.prepareTickingChunk. And two of the
ways in from outside: ChunkMap.move for a player,
ServerChunkCache.getChunk for a synchronous ask.
Rules: names, never code · how the system works, not how the code reads ·
newest version only · every backticked name passes tools/verify_names.py.