-
-
Notifications
You must be signed in to change notification settings - Fork 0
Explanation The chunk version guard
Why FalcoAnvilLoader refuses to load a world it considers too old, why it used to hand back a
chunk of air instead, and where the boundary is drawn. This is the behaviour that surprises a server
that has been pointing the loader at a pre-21w43a world.
The values the guard compares against are on Reference Supported versions; how to replace or disable the guard is How-to Replace the version and unknown-entry policies.
Before Minecraft snapshot 21w43a, a chunk's data sits inside a Level compound. From that
snapshot on it sits directly on the root compound, under the key sections
(FalcoAnvilLoader.SECTIONS_KEY).
FalcoAnvilLoader only ever reads sections from the root. Before this guard existed, pointing it
at an older world found nothing there:
NbtReads.optionalList
returns an empty list for an absent key instead of failing, so every such chunk decoded to nothing
but air — no error, no log line — and a save could then have written that empty chunk over the real
one.
DefaultChunkVersionPolicy#check closes that gap
(DefaultChunkVersionPolicy)
and, as the loader's default ChunkVersionPolicy, runs on every load, right after the raw compound is
parsed and before anything is decoded from it — see Replaceable
policies below for how a caller can swap this check out for one of its own,
or turn it off entirely. It draws the line two independent ways:
-
Layout. A root compound that carries no
sectionsbut does carry aLevelcompound is the pre-21w43ashape, and is refused outright — regardless of whatDataVersionsays, or whether it says anything at all. -
Version. Once the layout is the current one, the stored
DataVersionhas to be at or aboveminimumDataVersion. The default,FalcoAnvilLoader.DEFAULT_MINIMUM_DATA_VERSION(2844), is the data version of21w43aitself — the snapshot the layout check above is keyed on, so the two lines agree on where the boundary is.
The version check treats an absent DataVersion and a broken one differently, on purpose:
-
Missing is not refused. A tool that writes
sectionson the root but never learned to stamp aDataVersionis not rejected for that omission; refusing it would make a whole category of externally-written worlds unreadable for a reason unconnected to the layout this guard actually checks. (Falco's own save path always stamps one — seedataVersion(int)below — this case is about worlds written by something else.) -
Wrong type or negative is refused. Both are a value somebody actually wrote, and neither
describes a version this loader can trust, so both are treated as a version below the floor rather
than as "nothing was written".
NbtReads.optionalIntegercannot tell "wrong type" apart from "absent" by itself — both fall back to the same default — soDefaultChunkVersionPolicy#checkchecks presence itself first. A negative value needs no separate branch at all: it simply fails the sameversion >= minimumDataVersioncomparison every other version is checked against. - Present, valid and below the floor is refused, whatever the number actually is.
minimumDataVersion(int) is the builder slot for the floor above, and it is deliberately kept apart
from dataVersion(int), the write side that stamps a version into every chunk this loader saves:
one is what a load has to clear, the other is what a save claims, and the two have no relationship to
each other that the API should imply by sharing a name.
FalcoAnvilLoader loader = FalcoAnvilLoader.builder()
.minimumDataVersion(FalcoAnvilLoader.DEFAULT_MINIMUM_DATA_VERSION) // read: reject below this
.dataVersion(4189) // write: stamp this
.build(Path.of("worlds", "lobby"), dimension);A refused chunk fails exactly like every other unreadable chunk described under
Error handling and world consistency: loadChunk throws
AnvilChunkException instead of returning null. Its cause is a ChunkDataException whose
reason() is the new
ChunkDataException.Reason.UNSUPPORTED_CHUNK_VERSION.
The guard itself converts nothing: a chunk it refuses is exactly as unreadable as it was before the
guard existed, and the loader now says so instead of quietly handing back air. A loader can be
asked to migrate such a chunk rather than refuse it — see How world migration works.
Migration runs before this guard for that reason, since lifting a chunk over the floor is what
turns it into one the guard accepts; the other order would reject every world worth rescuing. Without
a migration mode selected, nothing of the sort happens and this paragraph describes the whole
behaviour.
AnvilDiagnostics#reportUnsupportedChunkVersion(String)
throttles the log line per distinct stored version, the same way every other diagnostic in the class
does, and counts every occurrence whether or not it was logged. A chunk with no stored DataVersion
at all is filed under
AnvilDiagnostics.UNKNOWN_DATA_VERSION
("<none>") rather than under a literal "-1", because the two describe different worlds: one never
had a version stamped, the other had one stamped and rejected. But an absent DataVersion on a
current-layout chunk is accepted, as above — so UNKNOWN_DATA_VERSION only appears in this breakdown
for the layout case, where it is refused for a reason that has nothing to do with the version being
absent.
AnvilDiagnostics#unsupportedChunkVersions()
returns the breakdown by that string, sorted by key;
AnvilDiagnostics#chunksSkippedAsUnsupported()
returns the total across every version, alongside chunksSkippedAsPartial() and the other counters
this class already exposed.
This is a behaviour change from earlier releases. A world below the floor used to load as a chunk
of air, silently, and a save could then write that air back over the original data. It now fails the
load instead. A server that has been pointing this loader at a pre-21w43a world will see it start
throwing where it previously — incorrectly — succeeded.
The guard above covers a world in an old format. A chunk in the current one could reach the
caller as air by a second route, closed separately in
#49: one carrying Status: minecraft:full and
neither sections nor Level has nothing for the guard to object to — current layout,
current DataVersion — and the status check waves it through. NbtReads.optionalList answers the
absent key with an empty list, nothing is decoded from it, and the caller receives a chunk of air
that reports itself as loaded: chunksLoaded() is 1 and errors() is 0.
Such a chunk is now refused with
ChunkDataException.Reason.MISSING_OR_MISTYPED_KEY.
Three boundaries are deliberate:
-
It runs after the status check, not in the version policy. A chunk that is honestly unfinished
carries no
sectionseither, and for it that is the normal state of a world edge rather than a contradiction. Only a chunk claiming to be complete has to prove it; an unfinished one stays a skip. -
A chunk carrying
Levelis left to the version policy. That is the pre-1.18 layout and a different failure, and a caller who passedversionPolicy(null)asked for that check not to run — a promise this must not quietly take back. -
An empty
sectionslist is accepted. An empty list is a statement that this chunk has none; an absent key is the absence of a statement. Refusing the empty list would reject legitimately empty chunks written by other tools, a far larger decision than this defect calls for.
The check does not live in NbtReads.optionalList itself. That method exists precisely so an absent
optional key is not an error, other callers depend on it, and it cannot know what its caller
considers required.
Related: How-to Migrate a world from an older version · Reference Exceptions and faults · Explanation Scope and non-goals
Every published table lives on Reference Measured results, which owns them; a correction is made
there and nowhere else. What the ± after a JMH mean covers is defined once, in
Explanation What a measurement here means.
Wiki home · Repository · README and quick start · API documentation · Issues · Licence: AGPL-3.0
Getting started
How-to guides
- How-to Add Falco to your build
- How-to Load an Anvil world
- How-to Compute light for a loaded world
- How-to Keep chunk light up to date automatically
- How-to Use FalcoInstance instead of InstanceContainer
- How-to Migrate a world from an older version
four more
Reference
six more
Background
- Explanation Choosing between Falco and the built-in loader
- Explanation Scope and non-goals
- Explanation When light computation actually runs
- Explanation What a measurement here means
nine more
- Explanation Choosing between FalcoInstance and InstanceContainer
- Explanation How the Anvil loader is built
- Explanation How the light engine works
- Explanation How the concurrency design works
- Explanation How world migration works
- Explanation The chunk version guard
- Explanation Why a second Anvil loader
- Explanation Why a custom light engine
- Explanation Why falco-instance exists
- Explanation Comparing the light engine with Minestoms
- Explanation What the benchmarks establish
Project record
Working on Falco